@fin.cx/skr 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  2. package/dist_ts/00_commitinfo_data.js +9 -0
  3. package/dist_ts/index.d.ts +6 -0
  4. package/dist_ts/index.js +7 -1
  5. package/dist_ts/plugins.d.ts +8 -1
  6. package/dist_ts/plugins.js +10 -2
  7. package/dist_ts/skr.api.d.ts +70 -0
  8. package/dist_ts/skr.api.js +348 -1
  9. package/dist_ts/skr.classes.account.d.ts +28 -0
  10. package/dist_ts/skr.classes.account.js +87 -4
  11. package/dist_ts/skr.classes.journalentry.js +56 -4
  12. package/dist_ts/skr.classes.ledger.js +5 -1
  13. package/dist_ts/skr.export.accounts.d.ts +53 -0
  14. package/dist_ts/skr.export.accounts.js +111 -0
  15. package/dist_ts/skr.export.balances.d.ts +59 -0
  16. package/dist_ts/skr.export.balances.js +205 -0
  17. package/dist_ts/skr.export.d.ts +110 -0
  18. package/dist_ts/skr.export.js +315 -0
  19. package/dist_ts/skr.export.ledger.d.ts +95 -0
  20. package/dist_ts/skr.export.ledger.js +164 -0
  21. package/dist_ts/skr.export.pdf.d.ts +82 -0
  22. package/dist_ts/skr.export.pdf.js +548 -0
  23. package/dist_ts/skr.invoice.adapter.d.ts +98 -0
  24. package/dist_ts/skr.invoice.adapter.js +476 -0
  25. package/dist_ts/skr.invoice.booking.d.ts +102 -0
  26. package/dist_ts/skr.invoice.booking.js +578 -0
  27. package/dist_ts/skr.invoice.entity.d.ts +287 -0
  28. package/dist_ts/skr.invoice.entity.js +2 -0
  29. package/dist_ts/skr.invoice.mapper.d.ts +69 -0
  30. package/dist_ts/skr.invoice.mapper.js +401 -0
  31. package/dist_ts/skr.invoice.storage.d.ts +140 -0
  32. package/dist_ts/skr.invoice.storage.js +529 -0
  33. package/dist_ts/skr.postingkeys.d.ts +56 -0
  34. package/dist_ts/skr.postingkeys.js +196 -0
  35. package/dist_ts/skr.security.d.ts +65 -0
  36. package/dist_ts/skr.security.js +319 -0
  37. package/dist_ts/skr.types.d.ts +19 -0
  38. package/dist_ts/skr03.data.js +3 -1
  39. package/dist_ts/skr04.data.js +3 -1
  40. package/package.json +17 -12
  41. package/readme.hints.md +54 -1
  42. package/readme.md +207 -16
  43. package/ts/00_commitinfo_data.ts +8 -0
  44. package/ts/index.ts +6 -0
  45. package/ts/plugins.ts +22 -1
  46. package/ts/skr.api.ts +485 -0
  47. package/ts/skr.classes.account.ts +106 -3
  48. package/ts/skr.classes.journalentry.ts +78 -3
  49. package/ts/skr.classes.ledger.ts +4 -0
  50. package/ts/skr.export.accounts.ts +154 -0
  51. package/ts/skr.export.balances.ts +270 -0
  52. package/ts/skr.export.ledger.ts +249 -0
  53. package/ts/skr.export.pdf.ts +601 -0
  54. package/ts/skr.export.ts +443 -0
  55. package/ts/skr.invoice.adapter.ts +581 -0
  56. package/ts/skr.invoice.booking.ts +760 -0
  57. package/ts/skr.invoice.entity.ts +351 -0
  58. package/ts/skr.invoice.mapper.ts +486 -0
  59. package/ts/skr.invoice.storage.ts +710 -0
  60. package/ts/skr.postingkeys.ts +252 -0
  61. package/ts/skr.security.ts +405 -0
  62. package/ts/skr.types.ts +27 -0
  63. package/ts/skr03.data.ts +2 -0
  64. package/ts/skr04.data.ts +2 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * DATEV Posting Keys (Buchungsschlüssel) for German Accounting
3
+ *
4
+ * Posting keys control automatic VAT booking and are automatically checked
5
+ * in German tax audits (Betriebsprüfungen). Using incorrect posting keys
6
+ * can have serious tax consequences.
7
+ *
8
+ * Reference: DATEV Buchungsschlüssel-Verzeichnis
9
+ */
10
+
11
+ import type { TPostingKey, IPostingKeyRule } from './skr.types.js';
12
+
13
+ /**
14
+ * Posting key definitions with validation rules
15
+ */
16
+ export const POSTING_KEY_RULES: Record<TPostingKey, IPostingKeyRule> = {
17
+ 3: {
18
+ key: 3,
19
+ description: 'Zahlungseingang mit 19% Umsatzsteuer',
20
+ vatRate: 19,
21
+ requiresVAT: true,
22
+ disablesVATAutomatism: false,
23
+ allowedScenarios: ['domestic_taxed']
24
+ },
25
+ 8: {
26
+ key: 8,
27
+ description: '7% Vorsteuer',
28
+ vatRate: 7,
29
+ requiresVAT: true,
30
+ disablesVATAutomatism: false,
31
+ allowedScenarios: ['domestic_taxed']
32
+ },
33
+ 9: {
34
+ key: 9,
35
+ description: '19% Vorsteuer',
36
+ vatRate: 19,
37
+ requiresVAT: true,
38
+ disablesVATAutomatism: false,
39
+ allowedScenarios: ['domestic_taxed']
40
+ },
41
+ 19: {
42
+ key: 19,
43
+ description: '19% Vorsteuer bei innergemeinschaftlichen Lieferungen',
44
+ vatRate: 19,
45
+ requiresVAT: true,
46
+ disablesVATAutomatism: false,
47
+ allowedScenarios: ['intra_eu']
48
+ },
49
+ 40: {
50
+ key: 40,
51
+ description: 'Steuerfrei / Aufhebung der Automatik',
52
+ vatRate: 0,
53
+ requiresVAT: false,
54
+ disablesVATAutomatism: true,
55
+ allowedScenarios: ['tax_free', 'export', 'reverse_charge']
56
+ },
57
+ 94: {
58
+ key: 94,
59
+ description: '19% Vorsteuer/Umsatzsteuer bei Erwerb aus EU oder Drittland (Reverse Charge)',
60
+ vatRate: 19,
61
+ requiresVAT: true,
62
+ disablesVATAutomatism: false,
63
+ allowedScenarios: ['reverse_charge', 'intra_eu', 'third_country']
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Validate posting key for a journal entry line
69
+ */
70
+ export function validatePostingKey(
71
+ postingKey: TPostingKey,
72
+ accountNumber: string,
73
+ amount: number,
74
+ vatAmount?: number,
75
+ taxScenario?: string
76
+ ): { isValid: boolean; errors: string[]; warnings: string[] } {
77
+ const errors: string[] = [];
78
+ const warnings: string[] = [];
79
+
80
+ // Get posting key rule
81
+ const rule = POSTING_KEY_RULES[postingKey];
82
+ if (!rule) {
83
+ errors.push(`Invalid posting key: ${postingKey}`);
84
+ return { isValid: false, errors, warnings };
85
+ }
86
+
87
+ // Validate VAT requirement
88
+ // Skip VAT amount requirement if:
89
+ // 1. Posting TO a VAT account (the line itself IS the VAT)
90
+ // 2. Posting TO a debtor/creditor account (receivable/payable settlement - VAT was already recorded)
91
+ const isVATAccount = accountNumber === '1571' || accountNumber === '1771' || accountNumber === '1576';
92
+ const accountNum = parseInt(accountNumber);
93
+ const isDebtorCreditorAccount = (accountNum >= 10000 && accountNum <= 69999) || (accountNum >= 70000 && accountNum <= 99999);
94
+
95
+ if (rule.requiresVAT && !vatAmount && !isVATAccount && !isDebtorCreditorAccount) {
96
+ errors.push(
97
+ `Posting key ${postingKey} requires VAT amount, but none provided. ` +
98
+ `Description: ${rule.description}`
99
+ );
100
+ }
101
+
102
+ // Validate VAT rate if specified
103
+ if (rule.vatRate && vatAmount && rule.vatRate > 0) {
104
+ const expectedVAT = Math.round(amount * rule.vatRate) / 100;
105
+ const tolerance = 0.02; // 2 cent tolerance for rounding
106
+
107
+ if (Math.abs(vatAmount - expectedVAT) > tolerance) {
108
+ warnings.push(
109
+ `VAT amount ${vatAmount} does not match expected ${expectedVAT.toFixed(2)} ` +
110
+ `for posting key ${postingKey} (${rule.vatRate}%)`
111
+ );
112
+ }
113
+ }
114
+
115
+ // Validate tax scenario
116
+ if (rule.allowedScenarios && taxScenario) {
117
+ if (!rule.allowedScenarios.includes(taxScenario)) {
118
+ errors.push(
119
+ `Posting key ${postingKey} is not valid for tax scenario '${taxScenario}'. ` +
120
+ `Allowed scenarios: ${rule.allowedScenarios.join(', ')}`
121
+ );
122
+ }
123
+ }
124
+
125
+ // Validate automatism disabling
126
+ if (rule.disablesVATAutomatism && vatAmount && vatAmount > 0) {
127
+ warnings.push(
128
+ `Posting key ${postingKey} disables VAT automatism but VAT amount is provided. ` +
129
+ `This may cause incorrect tax reporting.`
130
+ );
131
+ }
132
+
133
+ return {
134
+ isValid: errors.length === 0,
135
+ errors,
136
+ warnings
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Get posting key description
142
+ */
143
+ export function getPostingKeyDescription(postingKey: TPostingKey): string {
144
+ const rule = POSTING_KEY_RULES[postingKey];
145
+ return rule ? rule.description : `Unknown posting key: ${postingKey}`;
146
+ }
147
+
148
+ /**
149
+ * Get appropriate posting key for a transaction
150
+ */
151
+ export function suggestPostingKey(params: {
152
+ vatRate: number;
153
+ taxScenario?: string;
154
+ isPayment?: boolean;
155
+ }): TPostingKey {
156
+ const { vatRate, taxScenario, isPayment } = params;
157
+
158
+ // Tax-free or reverse charge scenarios
159
+ if (taxScenario === 'tax_free' || taxScenario === 'export') {
160
+ return 40;
161
+ }
162
+
163
+ // Reverse charge
164
+ if (taxScenario === 'reverse_charge' || taxScenario === 'third_country') {
165
+ return 94;
166
+ }
167
+
168
+ // Intra-EU with VAT
169
+ if (taxScenario === 'intra_eu' && vatRate === 19) {
170
+ return 19;
171
+ }
172
+
173
+ // Payment with 19% VAT
174
+ if (isPayment && vatRate === 19) {
175
+ return 3;
176
+ }
177
+
178
+ // Input VAT based on rate
179
+ if (vatRate === 19) {
180
+ return 9;
181
+ }
182
+
183
+ if (vatRate === 7) {
184
+ return 8;
185
+ }
186
+
187
+ // Default to tax-free if no VAT
188
+ if (vatRate === 0) {
189
+ return 40;
190
+ }
191
+
192
+ // Fallback to 19% input VAT
193
+ return 9;
194
+ }
195
+
196
+ /**
197
+ * Validate all posting keys for consistency
198
+ */
199
+ export function validatePostingKeyConsistency(lines: Array<{
200
+ postingKey: TPostingKey;
201
+ accountNumber: string;
202
+ debit?: number;
203
+ credit?: number;
204
+ vatAmount?: number;
205
+ }>): { isValid: boolean; errors: string[]; warnings: string[] } {
206
+ const errors: string[] = [];
207
+ const warnings: string[] = [];
208
+
209
+ // Check for mixing tax-free and taxed transactions
210
+ const hasTaxFree = lines.some(line => line.postingKey === 40);
211
+ const hasTaxed = lines.some(line => [3, 8, 9, 19, 94].includes(line.postingKey));
212
+
213
+ if (hasTaxFree && hasTaxed) {
214
+ warnings.push(
215
+ 'Journal entry mixes tax-free (key 40) and taxed transactions. ' +
216
+ 'Verify this is intentional.'
217
+ );
218
+ }
219
+
220
+ // Check for reverse charge consistency
221
+ const hasReverseCharge = lines.some(line => line.postingKey === 94);
222
+ if (hasReverseCharge) {
223
+ const reverseChargeLines = lines.filter(line => line.postingKey === 94);
224
+ if (reverseChargeLines.length % 2 !== 0) {
225
+ errors.push(
226
+ 'Reverse charge (posting key 94) requires both input and output VAT entries. ' +
227
+ 'Found odd number of reverse charge lines.'
228
+ );
229
+ }
230
+ }
231
+
232
+ return {
233
+ isValid: errors.length === 0,
234
+ errors,
235
+ warnings
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Check if posting key requires automatic VAT booking
241
+ */
242
+ export function requiresAutomaticVAT(postingKey: TPostingKey): boolean {
243
+ const rule = POSTING_KEY_RULES[postingKey];
244
+ return rule ? !rule.disablesVATAutomatism : false;
245
+ }
246
+
247
+ /**
248
+ * Get all valid posting keys
249
+ */
250
+ export function getAllPostingKeys(): TPostingKey[] {
251
+ return Object.keys(POSTING_KEY_RULES).map(k => Number(k) as TPostingKey);
252
+ }
@@ -0,0 +1,405 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as path from 'path';
3
+ import * as crypto from 'crypto';
4
+ import * as https from 'https';
5
+
6
+ export interface ISigningOptions {
7
+ certificatePem?: string;
8
+ privateKeyPem?: string;
9
+ privateKeyPassphrase?: string;
10
+ timestampServerUrl?: string;
11
+ includeTimestamp?: boolean;
12
+ }
13
+
14
+ export interface ISignatureResult {
15
+ signature: string;
16
+ signatureFormat: 'CAdES-B' | 'CAdES-T' | 'CAdES-LT';
17
+ signingTime: string;
18
+ certificateChain?: string[];
19
+ timestampToken?: string;
20
+ timestampTime?: string;
21
+ }
22
+
23
+ export interface ITimestampResponse {
24
+ token: string;
25
+ time: string;
26
+ serverUrl: string;
27
+ hashAlgorithm: string;
28
+ }
29
+
30
+ export class SecurityManager {
31
+ private options: ISigningOptions;
32
+ private logger: plugins.smartlog.ConsoleLog;
33
+
34
+ constructor(options: ISigningOptions = {}) {
35
+ this.options = {
36
+ timestampServerUrl: options.timestampServerUrl || 'http://timestamp.digicert.com',
37
+ includeTimestamp: options.includeTimestamp !== false,
38
+ ...options
39
+ };
40
+ this.logger = new plugins.smartlog.ConsoleLog();
41
+ }
42
+
43
+ /**
44
+ * Creates a CAdES-B (Basic) signature for data
45
+ */
46
+ public async createCadesSignature(
47
+ data: Buffer | string,
48
+ certificatePem?: string,
49
+ privateKeyPem?: string
50
+ ): Promise<ISignatureResult> {
51
+ const cert = certificatePem || this.options.certificatePem;
52
+ const key = privateKeyPem || this.options.privateKeyPem;
53
+
54
+ if (!cert || !key) {
55
+ throw new Error('Certificate and private key are required for signing');
56
+ }
57
+
58
+ try {
59
+ // Parse certificate and key
60
+ const certificate = plugins.nodeForge.pki.certificateFromPem(cert);
61
+ const privateKey = this.options.privateKeyPassphrase
62
+ ? plugins.nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
63
+ : plugins.nodeForge.pki.privateKeyFromPem(key);
64
+
65
+ // Create PKCS#7 signed data (CMS)
66
+ const p7 = plugins.nodeForge.pkcs7.createSignedData();
67
+
68
+ // Add content
69
+ if (typeof data === 'string') {
70
+ p7.content = plugins.nodeForge.util.createBuffer(data, 'utf8');
71
+ } else {
72
+ p7.content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
73
+ }
74
+
75
+ // Add certificate
76
+ p7.addCertificate(certificate);
77
+
78
+ // Add signer
79
+ p7.addSigner({
80
+ key: privateKey,
81
+ certificate: certificate,
82
+ digestAlgorithm: plugins.nodeForge.pki.oids.sha256,
83
+ authenticatedAttributes: [
84
+ {
85
+ type: plugins.nodeForge.pki.oids.contentType,
86
+ value: plugins.nodeForge.pki.oids.data
87
+ },
88
+ {
89
+ type: plugins.nodeForge.pki.oids.messageDigest
90
+ },
91
+ {
92
+ type: plugins.nodeForge.pki.oids.signingTime,
93
+ value: new Date().toISOString()
94
+ }
95
+ ]
96
+ });
97
+
98
+ // Sign the data
99
+ p7.sign({ detached: true });
100
+
101
+ // Convert to PEM
102
+ const pem = plugins.nodeForge.pkcs7.messageToPem(p7);
103
+
104
+ // Extract base64 signature
105
+ const signature = pem
106
+ .replace(/-----BEGIN PKCS7-----/, '')
107
+ .replace(/-----END PKCS7-----/, '')
108
+ .replace(/\r?\n/g, '');
109
+
110
+ const result: ISignatureResult = {
111
+ signature: signature,
112
+ signatureFormat: 'CAdES-B',
113
+ signingTime: new Date().toISOString(),
114
+ certificateChain: [cert]
115
+ };
116
+
117
+ // Add timestamp if requested
118
+ if (this.options.includeTimestamp && this.options.timestampServerUrl) {
119
+ try {
120
+ const timestampResponse = await this.requestTimestamp(signature);
121
+ result.timestampToken = timestampResponse.token;
122
+ result.timestampTime = timestampResponse.time;
123
+ result.signatureFormat = 'CAdES-T';
124
+ } catch (error) {
125
+ this.logger.log('warn', `Failed to obtain timestamp: ${error}`);
126
+ }
127
+ }
128
+
129
+ return result;
130
+ } catch (error) {
131
+ throw new Error(`Failed to create CAdES signature: ${error}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Requests an RFC 3161 timestamp from a TSA
137
+ */
138
+ public async requestTimestamp(dataHash: string | Buffer): Promise<ITimestampResponse> {
139
+ try {
140
+ // Create hash of the data
141
+ let hash: Buffer;
142
+ if (typeof dataHash === 'string') {
143
+ hash = crypto.createHash('sha256').update(dataHash).digest();
144
+ } else {
145
+ hash = crypto.createHash('sha256').update(dataHash).digest();
146
+ }
147
+
148
+ // Create timestamp request (simplified - in production use proper ASN.1 encoding)
149
+ const tsRequest = this.createTimestampRequest(hash);
150
+
151
+ // Send request to TSA
152
+ const response = await this.sendTimestampRequest(tsRequest);
153
+
154
+ return {
155
+ token: response.toString('base64'),
156
+ time: new Date().toISOString(),
157
+ serverUrl: this.options.timestampServerUrl!,
158
+ hashAlgorithm: 'sha256'
159
+ };
160
+ } catch (error) {
161
+ throw new Error(`Failed to obtain timestamp: ${error}`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Creates a timestamp request (simplified version)
167
+ */
168
+ private createTimestampRequest(hash: Buffer): Buffer {
169
+ // In production, use proper ASN.1 encoding library
170
+ // This is a simplified placeholder
171
+ const request = {
172
+ version: 1,
173
+ messageImprint: {
174
+ hashAlgorithm: { algorithm: '2.16.840.1.101.3.4.2.1' }, // SHA-256 OID
175
+ hashedMessage: hash
176
+ },
177
+ reqPolicy: null,
178
+ nonce: crypto.randomBytes(8),
179
+ certReq: true
180
+ };
181
+
182
+ // Convert to DER-encoded ASN.1 (simplified)
183
+ return Buffer.from(JSON.stringify(request));
184
+ }
185
+
186
+ /**
187
+ * Sends timestamp request to TSA server
188
+ */
189
+ private async sendTimestampRequest(request: Buffer): Promise<Buffer> {
190
+ return new Promise((resolve, reject) => {
191
+ const url = new URL(this.options.timestampServerUrl!);
192
+
193
+ const options = {
194
+ hostname: url.hostname,
195
+ port: url.port || 443,
196
+ path: url.pathname,
197
+ method: 'POST',
198
+ headers: {
199
+ 'Content-Type': 'application/timestamp-query',
200
+ 'Content-Length': request.length
201
+ }
202
+ };
203
+
204
+ const req = https.request(options, (res) => {
205
+ const chunks: Buffer[] = [];
206
+
207
+ res.on('data', (chunk) => chunks.push(chunk));
208
+ res.on('end', () => {
209
+ const response = Buffer.concat(chunks);
210
+ if (res.statusCode === 200) {
211
+ resolve(response);
212
+ } else {
213
+ reject(new Error(`TSA server returned status ${res.statusCode}`));
214
+ }
215
+ });
216
+ });
217
+
218
+ req.on('error', reject);
219
+ req.write(request);
220
+ req.end();
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Verifies a CAdES signature
226
+ */
227
+ public async verifyCadesSignature(
228
+ data: Buffer | string,
229
+ signature: string,
230
+ certificatePem?: string
231
+ ): Promise<boolean> {
232
+ try {
233
+ // Add PEM headers if not present
234
+ let pemSignature = signature;
235
+ if (!signature.includes('BEGIN PKCS7')) {
236
+ pemSignature = `-----BEGIN PKCS7-----\n${signature}\n-----END PKCS7-----`;
237
+ }
238
+
239
+ // Parse the PKCS#7 message
240
+ const p7 = plugins.nodeForge.pkcs7.messageFromPem(pemSignature);
241
+
242
+ // Prepare content for verification
243
+ let content: plugins.nodeForge.util.ByteStringBuffer;
244
+ if (typeof data === 'string') {
245
+ content = plugins.nodeForge.util.createBuffer(data, 'utf8');
246
+ } else {
247
+ content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
248
+ }
249
+
250
+ // Verify the signature
251
+ const verified = (p7 as any).verify({
252
+ content: content,
253
+ detached: true
254
+ });
255
+
256
+ return verified;
257
+ } catch (error) {
258
+ this.logger.log('error', `Signature verification failed: ${error}`);
259
+ return false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Generates a self-signed certificate for testing
265
+ */
266
+ public async generateSelfSignedCertificate(
267
+ commonName: string = 'SKR Export System',
268
+ validDays: number = 365
269
+ ): Promise<{ certificate: string; privateKey: string }> {
270
+ const keys = plugins.nodeForge.pki.rsa.generateKeyPair(2048);
271
+ const cert = plugins.nodeForge.pki.createCertificate();
272
+
273
+ cert.publicKey = keys.publicKey;
274
+ cert.serialNumber = '01';
275
+ cert.validity.notBefore = new Date();
276
+ cert.validity.notAfter = new Date();
277
+ cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + validDays);
278
+
279
+ const attrs = [
280
+ { name: 'commonName', value: commonName },
281
+ { name: 'countryName', value: 'DE' },
282
+ { name: 'organizationName', value: 'SKR Export System' },
283
+ { shortName: 'OU', value: 'Accounting' }
284
+ ];
285
+
286
+ cert.setSubject(attrs);
287
+ cert.setIssuer(attrs);
288
+
289
+ cert.setExtensions([
290
+ {
291
+ name: 'basicConstraints',
292
+ cA: true
293
+ },
294
+ {
295
+ name: 'keyUsage',
296
+ keyCertSign: true,
297
+ digitalSignature: true,
298
+ nonRepudiation: true,
299
+ keyEncipherment: true,
300
+ dataEncipherment: true
301
+ },
302
+ {
303
+ name: 'extKeyUsage',
304
+ serverAuth: true,
305
+ clientAuth: true,
306
+ codeSigning: true,
307
+ emailProtection: true,
308
+ timeStamping: true
309
+ },
310
+ {
311
+ name: 'nsCertType',
312
+ client: true,
313
+ server: true,
314
+ email: true,
315
+ objsign: true,
316
+ sslCA: true,
317
+ emailCA: true,
318
+ objCA: true
319
+ },
320
+ {
321
+ name: 'subjectAltName',
322
+ altNames: [
323
+ { type: 2, value: commonName }
324
+ ]
325
+ }
326
+ ]);
327
+
328
+ // Self-sign certificate
329
+ cert.sign(keys.privateKey, plugins.nodeForge.md.sha256.create());
330
+
331
+ // Convert to PEM
332
+ const certificatePem = plugins.nodeForge.pki.certificateToPem(cert);
333
+ const privateKeyPem = plugins.nodeForge.pki.privateKeyToPem(keys.privateKey);
334
+
335
+ return {
336
+ certificate: certificatePem,
337
+ privateKey: privateKeyPem
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Creates a detached signature file
343
+ */
344
+ public async createDetachedSignature(
345
+ dataPath: string,
346
+ outputPath: string
347
+ ): Promise<void> {
348
+ const data = await plugins.smartfile.fs.toBuffer(dataPath);
349
+ const signature = await this.createCadesSignature(data);
350
+
351
+ const signatureData = {
352
+ signature: signature.signature,
353
+ format: signature.signatureFormat,
354
+ signingTime: signature.signingTime,
355
+ timestamp: signature.timestampToken,
356
+ timestampTime: signature.timestampTime,
357
+ algorithm: 'SHA256withRSA',
358
+ signedFile: path.basename(dataPath)
359
+ };
360
+
361
+ await plugins.smartfile.memory.toFs(
362
+ JSON.stringify(signatureData, null, 2),
363
+ outputPath
364
+ );
365
+ }
366
+
367
+ /**
368
+ * Verifies a detached signature file
369
+ */
370
+ public async verifyDetachedSignature(
371
+ dataPath: string,
372
+ signaturePath: string
373
+ ): Promise<boolean> {
374
+ try {
375
+ const data = await plugins.smartfile.fs.toBuffer(dataPath);
376
+ const signatureJson = await plugins.smartfile.fs.toStringSync(signaturePath);
377
+ const signatureData = JSON.parse(signatureJson);
378
+
379
+ return await this.verifyCadesSignature(data, signatureData.signature);
380
+ } catch (error) {
381
+ this.logger.log('error', `Failed to verify detached signature: ${error}`);
382
+ return false;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Adds Long-Term Validation (LTV) information
388
+ */
389
+ public async addLtvInformation(
390
+ signature: ISignatureResult,
391
+ ocspResponse?: Buffer,
392
+ crlData?: Buffer
393
+ ): Promise<ISignatureResult> {
394
+ // Add OCSP response and CRL data for long-term validation
395
+ const ltv = {
396
+ ...signature,
397
+ signatureFormat: 'CAdES-LT' as const,
398
+ ocsp: ocspResponse?.toString('base64'),
399
+ crl: crlData?.toString('base64'),
400
+ ltvTime: new Date().toISOString()
401
+ };
402
+
403
+ return ltv;
404
+ }
405
+ }
package/ts/skr.types.ts CHANGED
@@ -9,6 +9,18 @@ export type TSKRType = 'SKR03' | 'SKR04';
9
9
 
10
10
  export type TTransactionStatus = 'pending' | 'posted' | 'reversed';
11
11
 
12
+ /**
13
+ * DATEV posting keys (Buchungsschlüssel) for German accounting
14
+ * These keys control automatic VAT booking and are checked in tax audits
15
+ */
16
+ export type TPostingKey =
17
+ | 3 // Payment with 19% VAT
18
+ | 8 // 7% input VAT
19
+ | 9 // 19% input VAT
20
+ | 19 // 19% input VAT (intra-EU)
21
+ | 40 // Tax-free (disables VAT automatism)
22
+ | 94; // 19% input/output VAT (reverse charge)
23
+
12
24
  export type TReportType =
13
25
  | 'trial_balance'
14
26
  | 'income_statement'
@@ -16,6 +28,18 @@ export type TReportType =
16
28
  | 'general_ledger'
17
29
  | 'cash_flow';
18
30
 
31
+ /**
32
+ * Posting key validation rule
33
+ */
34
+ export interface IPostingKeyRule {
35
+ key: TPostingKey;
36
+ description: string;
37
+ vatRate?: number; // Expected VAT rate (if applicable)
38
+ requiresVAT: boolean; // Whether VAT entry is required
39
+ disablesVATAutomatism: boolean; // Whether this key disables automatic VAT
40
+ allowedScenarios?: string[]; // Allowed tax scenarios (e.g., 'reverse_charge')
41
+ }
42
+
19
43
  export interface IAccountData {
20
44
  accountNumber: string;
21
45
  accountName: string;
@@ -25,6 +49,7 @@ export interface IAccountData {
25
49
  description?: string;
26
50
  vatRate?: number;
27
51
  isActive?: boolean;
52
+ isAutomaticAccount?: boolean; // Automatikkonto (e.g., 1400, 1600) - cannot be posted to directly
28
53
  }
29
54
 
30
55
  export interface ITransactionData {
@@ -53,6 +78,7 @@ export interface IJournalEntryLine {
53
78
  credit?: number;
54
79
  description?: string;
55
80
  costCenter?: string;
81
+ postingKey: TPostingKey; // REQUIRED: DATEV posting key for VAT automation control
56
82
  }
57
83
 
58
84
  export interface ITrialBalanceEntry {
@@ -136,6 +162,7 @@ export interface ITransactionFilter {
136
162
  export interface IDatabaseConfig {
137
163
  mongoDbUrl: string;
138
164
  dbName?: string;
165
+ invoiceExportPath?: string; // Optional path for invoice storage
139
166
  }
140
167
 
141
168
  export interface IReportParams {