@fin.cx/skr 1.2.0 → 1.2.2

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 (58) hide show
  1. package/{npmextra.json → .smartconfig.json} +12 -6
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +21 -15
  5. package/dist_ts/index.js +16 -16
  6. package/dist_ts/plugins.d.ts +15 -5
  7. package/dist_ts/plugins.js +42 -6
  8. package/dist_ts/skr.api.js +20 -13
  9. package/dist_ts/skr.classes.account.d.ts +36 -3
  10. package/dist_ts/skr.classes.account.js +366 -210
  11. package/dist_ts/skr.classes.chartofaccounts.d.ts +1 -1
  12. package/dist_ts/skr.classes.chartofaccounts.js +13 -6
  13. package/dist_ts/skr.classes.journalentry.d.ts +9 -4
  14. package/dist_ts/skr.classes.journalentry.js +430 -309
  15. package/dist_ts/skr.classes.ledger.js +7 -1
  16. package/dist_ts/skr.classes.reports.js +4 -1
  17. package/dist_ts/skr.classes.transaction.d.ts +9 -4
  18. package/dist_ts/skr.classes.transaction.js +315 -244
  19. package/dist_ts/skr.export.accounts.js +3 -2
  20. package/dist_ts/skr.export.balances.js +4 -2
  21. package/dist_ts/skr.export.js +6 -3
  22. package/dist_ts/skr.export.ledger.js +4 -3
  23. package/dist_ts/skr.export.pdf.js +6 -3
  24. package/dist_ts/skr.invoice.adapter.d.ts +2 -0
  25. package/dist_ts/skr.invoice.adapter.js +22 -10
  26. package/dist_ts/skr.invoice.booking.js +52 -25
  27. package/dist_ts/skr.invoice.mapper.js +84 -82
  28. package/dist_ts/skr.invoice.storage.js +10 -5
  29. package/dist_ts/skr.postingkeys.d.ts +56 -0
  30. package/dist_ts/skr.postingkeys.js +196 -0
  31. package/dist_ts/skr.security.js +24 -21
  32. package/dist_ts/skr.types.d.ts +18 -0
  33. package/dist_ts/skr03.data.js +3 -1
  34. package/dist_ts/skr04.data.js +3 -1
  35. package/license.md +21 -0
  36. package/package.json +31 -23
  37. package/readme.hints.md +54 -1
  38. package/readme.md +215 -632
  39. package/readme.plan.md +1 -1
  40. package/ts/00_commitinfo_data.ts +8 -0
  41. package/ts/index.ts +43 -15
  42. package/ts/plugins.ts +52 -15
  43. package/ts/skr.api.ts +9 -5
  44. package/ts/skr.classes.account.ts +144 -23
  45. package/ts/skr.classes.chartofaccounts.ts +8 -3
  46. package/ts/skr.classes.journalentry.ts +117 -24
  47. package/ts/skr.classes.ledger.ts +4 -0
  48. package/ts/skr.classes.reports.ts +22 -2
  49. package/ts/skr.classes.transaction.ts +40 -22
  50. package/ts/skr.export.pdf.ts +4 -3
  51. package/ts/skr.invoice.adapter.ts +31 -11
  52. package/ts/skr.invoice.booking.ts +54 -30
  53. package/ts/skr.invoice.storage.ts +3 -2
  54. package/ts/skr.postingkeys.ts +252 -0
  55. package/ts/skr.security.ts +23 -22
  56. package/ts/skr.types.ts +26 -0
  57. package/ts/skr03.data.ts +2 -0
  58. package/ts/skr04.data.ts +2 -0
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { JournalEntry } from './skr.classes.journalentry.js';
3
3
  import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
4
+ import { suggestPostingKey } from './skr.postingkeys.js';
4
5
  import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
5
6
  import type {
6
7
  IInvoice,
@@ -130,10 +131,11 @@ export class InvoiceBookingEngine {
130
131
  };
131
132
  } catch (error) {
132
133
  this.logger.log('error', `Failed to book invoice: ${error}`);
134
+ const errorMessage = error instanceof Error ? error.message : String(error);
133
135
  return {
134
136
  success: false,
135
137
  confidence: 0,
136
- errors: [`Booking failed: ${error.message}`]
138
+ errors: [`Booking failed: ${errorMessage}`]
137
139
  };
138
140
  }
139
141
  }
@@ -196,14 +198,16 @@ export class InvoiceBookingEngine {
196
198
  lines.push({
197
199
  accountNumber,
198
200
  credit: Math.abs(amount),
199
- description: this.getAccountDescription(accountNumber, group)
201
+ description: this.getAccountDescription(accountNumber, group),
202
+ postingKey: 9 // 19% input VAT for expenses
200
203
  });
201
204
  } else {
202
205
  // Regular invoice: debit expense account
203
206
  lines.push({
204
207
  accountNumber,
205
208
  debit: Math.abs(amount),
206
- description: this.getAccountDescription(accountNumber, group)
209
+ description: this.getAccountDescription(accountNumber, group),
210
+ postingKey: 9 // 19% input VAT for expenses
207
211
  });
208
212
  }
209
213
  }
@@ -221,14 +225,16 @@ export class InvoiceBookingEngine {
221
225
  lines.push({
222
226
  accountNumber: controlAccount,
223
227
  debit: totalAmount,
224
- description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`
228
+ description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`,
229
+ postingKey: 40 // Tax-free for control account
225
230
  });
226
231
  } else {
227
232
  // Regular invoice: credit vendor account
228
233
  lines.push({
229
234
  accountNumber: controlAccount,
230
235
  credit: totalAmount,
231
- description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`
236
+ description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`,
237
+ postingKey: 40 // Tax-free for control account
232
238
  });
233
239
  }
234
240
 
@@ -257,14 +263,16 @@ export class InvoiceBookingEngine {
257
263
  lines.push({
258
264
  accountNumber,
259
265
  debit: Math.abs(amount),
260
- description: this.getAccountDescription(accountNumber, group)
266
+ description: this.getAccountDescription(accountNumber, group),
267
+ postingKey: 9 // 19% output VAT for revenue
261
268
  });
262
269
  } else {
263
270
  // Regular invoice: credit revenue account
264
271
  lines.push({
265
272
  accountNumber,
266
273
  credit: Math.abs(amount),
267
- description: this.getAccountDescription(accountNumber, group)
274
+ description: this.getAccountDescription(accountNumber, group),
275
+ postingKey: 9 // 19% output VAT for revenue
268
276
  });
269
277
  }
270
278
  }
@@ -282,14 +290,16 @@ export class InvoiceBookingEngine {
282
290
  lines.push({
283
291
  accountNumber: controlAccount,
284
292
  credit: totalAmount,
285
- description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`
293
+ description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`,
294
+ postingKey: 40 // Tax-free for control account
286
295
  });
287
296
  } else {
288
297
  // Regular invoice: debit customer account
289
298
  lines.push({
290
299
  accountNumber: controlAccount,
291
300
  debit: totalAmount,
292
- description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`
301
+ description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`,
302
+ postingKey: 40 // Tax-free for control account
293
303
  });
294
304
  }
295
305
 
@@ -325,20 +335,23 @@ export class InvoiceBookingEngine {
325
335
 
326
336
  const amount = Math.abs(vatBreak.taxAmount);
327
337
  const description = `VAT ${vatBreak.vatCategory.rate}%`;
328
-
338
+ const vatRate = vatBreak.vatCategory.rate;
339
+ // Select posting key based on VAT rate: 8 for 7%, 9 for 19%
340
+ const postingKey = vatRate === 7 ? 8 : 9;
341
+
329
342
  if (direction === 'input') {
330
343
  // Input VAT (Vorsteuer)
331
344
  if (reverseDirection) {
332
- lines.push({ accountNumber: vatAccount, credit: amount, description });
345
+ lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
333
346
  } else {
334
- lines.push({ accountNumber: vatAccount, debit: amount, description });
347
+ lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
335
348
  }
336
349
  } else {
337
350
  // Output VAT (Umsatzsteuer)
338
351
  if (reverseDirection) {
339
- lines.push({ accountNumber: vatAccount, debit: amount, description });
352
+ lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
340
353
  } else {
341
- lines.push({ accountNumber: vatAccount, credit: amount, description });
354
+ lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
342
355
  }
343
356
  }
344
357
  }
@@ -404,12 +417,14 @@ export class InvoiceBookingEngine {
404
417
  {
405
418
  accountNumber: inputVATAccount,
406
419
  debit: amount,
407
- description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`
420
+ description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`,
421
+ postingKey: 94 // Reverse charge posting key
408
422
  },
409
423
  {
410
424
  accountNumber: outputVATAccount,
411
425
  credit: amount,
412
- description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`
426
+ description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`,
427
+ postingKey: 94 // Reverse charge posting key
413
428
  }
414
429
  );
415
430
  }
@@ -462,24 +477,27 @@ export class InvoiceBookingEngine {
462
477
  {
463
478
  accountNumber: controlAccount,
464
479
  debit: fullAmount,
465
- description: `Payment to ${invoice.supplier.name}`
480
+ description: `Payment to ${invoice.supplier.name}`,
481
+ postingKey: 3 // Payment with VAT
466
482
  },
467
483
  {
468
484
  accountNumber: '1000', // Bank account (would be configurable)
469
485
  credit: paymentAmount,
470
- description: `Bank payment ${payment.endToEndId || payment.paymentId}`
486
+ description: `Bank payment ${payment.endToEndId || payment.paymentId}`,
487
+ postingKey: 40 // Tax-free for bank account
471
488
  }
472
489
  );
473
-
490
+
474
491
  // Book skonto if taken
475
492
  if (skontoAmount > 0) {
476
493
  const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
477
494
  lines.push({
478
495
  accountNumber: skontoAccounts.skontoAccount,
479
496
  credit: skontoAmount,
480
- description: `Skonto received`
497
+ description: `Skonto received`,
498
+ postingKey: 40 // Tax-free for skonto
481
499
  });
482
-
500
+
483
501
  // VAT correction for skonto
484
502
  if (rules.skontoMethod === 'gross') {
485
503
  const effectiveRate = this.calculateEffectiveVATRate(invoice);
@@ -488,7 +506,8 @@ export class InvoiceBookingEngine {
488
506
  {
489
507
  accountNumber: skontoAccounts.vatCorrectionAccount,
490
508
  credit: vatCorrection,
491
- description: `Skonto VAT correction`
509
+ description: `Skonto VAT correction`,
510
+ postingKey: 40 // Tax-free for correction
492
511
  }
493
512
  );
494
513
  }
@@ -499,24 +518,27 @@ export class InvoiceBookingEngine {
499
518
  {
500
519
  accountNumber: '1000', // Bank account
501
520
  debit: paymentAmount,
502
- description: `Payment from ${invoice.customer.name}`
521
+ description: `Payment from ${invoice.customer.name}`,
522
+ postingKey: 40 // Tax-free for bank account
503
523
  },
504
524
  {
505
525
  accountNumber: controlAccount,
506
526
  credit: fullAmount,
507
- description: `Customer payment ${payment.endToEndId || payment.paymentId}`
527
+ description: `Customer payment ${payment.endToEndId || payment.paymentId}`,
528
+ postingKey: 3 // Payment with VAT
508
529
  }
509
530
  );
510
-
531
+
511
532
  // Book skonto if granted
512
533
  if (skontoAmount > 0) {
513
534
  const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
514
535
  lines.push({
515
536
  accountNumber: skontoAccounts.skontoAccount,
516
537
  debit: skontoAmount,
517
- description: `Skonto granted`
538
+ description: `Skonto granted`,
539
+ postingKey: 40 // Tax-free for skonto
518
540
  });
519
-
541
+
520
542
  // VAT correction for skonto
521
543
  if (rules.skontoMethod === 'gross') {
522
544
  const effectiveRate = this.calculateEffectiveVATRate(invoice);
@@ -525,7 +547,8 @@ export class InvoiceBookingEngine {
525
547
  {
526
548
  accountNumber: skontoAccounts.vatCorrectionAccount,
527
549
  debit: vatCorrection,
528
- description: `Skonto VAT correction`
550
+ description: `Skonto VAT correction`,
551
+ postingKey: 40 // Tax-free for correction
529
552
  }
530
553
  );
531
554
  }
@@ -552,10 +575,11 @@ export class InvoiceBookingEngine {
552
575
  };
553
576
  } catch (error) {
554
577
  this.logger.log('error', `Failed to book payment: ${error}`);
578
+ const errorMessage = error instanceof Error ? error.message : String(error);
555
579
  return {
556
580
  success: false,
557
581
  confidence: 0,
558
- errors: [`Payment booking failed: ${error.message}`]
582
+ errors: [`Payment booking failed: ${errorMessage}`]
559
583
  };
560
584
  }
561
585
  }
@@ -735,4 +759,4 @@ export class InvoiceBookingEngine {
735
759
 
736
760
  return Array.from(accounts);
737
761
  }
738
- }
762
+ }
@@ -224,7 +224,8 @@ export class InvoiceStorage {
224
224
  return contentHash;
225
225
  } catch (error) {
226
226
  this.logger.log('error', `Failed to store invoice: ${error}`);
227
- throw new Error(`Invoice storage failed: ${error.message}`);
227
+ const errorMessage = error instanceof Error ? error.message : String(error);
228
+ throw new Error(`Invoice storage failed: ${errorMessage}`);
228
229
  }
229
230
  }
230
231
 
@@ -707,4 +708,4 @@ export class InvoiceStorage {
707
708
 
708
709
  this.logger.log('info', `Updated metadata for invoice: ${contentHash}`);
709
710
  }
710
- }
711
+ }
@@ -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
+ }
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
2
2
  import * as path from 'path';
3
3
  import * as crypto from 'crypto';
4
4
  import * as https from 'https';
5
+ import * as nodeForge from 'node-forge';
5
6
 
6
7
  export interface ISigningOptions {
7
8
  certificatePem?: string;
@@ -57,19 +58,19 @@ export class SecurityManager {
57
58
 
58
59
  try {
59
60
  // Parse certificate and key
60
- const certificate = plugins.nodeForge.pki.certificateFromPem(cert);
61
+ const certificate = nodeForge.pki.certificateFromPem(cert);
61
62
  const privateKey = this.options.privateKeyPassphrase
62
- ? plugins.nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
63
- : plugins.nodeForge.pki.privateKeyFromPem(key);
63
+ ? nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
64
+ : nodeForge.pki.privateKeyFromPem(key);
64
65
 
65
66
  // Create PKCS#7 signed data (CMS)
66
- const p7 = plugins.nodeForge.pkcs7.createSignedData();
67
+ const p7 = nodeForge.pkcs7.createSignedData();
67
68
 
68
69
  // Add content
69
70
  if (typeof data === 'string') {
70
- p7.content = plugins.nodeForge.util.createBuffer(data, 'utf8');
71
+ p7.content = nodeForge.util.createBuffer(data, 'utf8');
71
72
  } else {
72
- p7.content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
73
+ p7.content = nodeForge.util.createBuffer(data.toString('latin1'));
73
74
  }
74
75
 
75
76
  // Add certificate
@@ -79,17 +80,17 @@ export class SecurityManager {
79
80
  p7.addSigner({
80
81
  key: privateKey,
81
82
  certificate: certificate,
82
- digestAlgorithm: plugins.nodeForge.pki.oids.sha256,
83
+ digestAlgorithm: nodeForge.pki.oids.sha256,
83
84
  authenticatedAttributes: [
84
85
  {
85
- type: plugins.nodeForge.pki.oids.contentType,
86
- value: plugins.nodeForge.pki.oids.data
86
+ type: nodeForge.pki.oids.contentType,
87
+ value: nodeForge.pki.oids.data
87
88
  },
88
89
  {
89
- type: plugins.nodeForge.pki.oids.messageDigest
90
+ type: nodeForge.pki.oids.messageDigest
90
91
  },
91
92
  {
92
- type: plugins.nodeForge.pki.oids.signingTime,
93
+ type: nodeForge.pki.oids.signingTime,
93
94
  value: new Date().toISOString()
94
95
  }
95
96
  ]
@@ -99,7 +100,7 @@ export class SecurityManager {
99
100
  p7.sign({ detached: true });
100
101
 
101
102
  // Convert to PEM
102
- const pem = plugins.nodeForge.pkcs7.messageToPem(p7);
103
+ const pem = nodeForge.pkcs7.messageToPem(p7);
103
104
 
104
105
  // Extract base64 signature
105
106
  const signature = pem
@@ -237,14 +238,14 @@ export class SecurityManager {
237
238
  }
238
239
 
239
240
  // Parse the PKCS#7 message
240
- const p7 = plugins.nodeForge.pkcs7.messageFromPem(pemSignature);
241
+ const p7 = nodeForge.pkcs7.messageFromPem(pemSignature);
241
242
 
242
243
  // Prepare content for verification
243
- let content: plugins.nodeForge.util.ByteStringBuffer;
244
+ let content: nodeForge.util.ByteStringBuffer;
244
245
  if (typeof data === 'string') {
245
- content = plugins.nodeForge.util.createBuffer(data, 'utf8');
246
+ content = nodeForge.util.createBuffer(data, 'utf8');
246
247
  } else {
247
- content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
248
+ content = nodeForge.util.createBuffer(data.toString('latin1'));
248
249
  }
249
250
 
250
251
  // Verify the signature
@@ -267,8 +268,8 @@ export class SecurityManager {
267
268
  commonName: string = 'SKR Export System',
268
269
  validDays: number = 365
269
270
  ): Promise<{ certificate: string; privateKey: string }> {
270
- const keys = plugins.nodeForge.pki.rsa.generateKeyPair(2048);
271
- const cert = plugins.nodeForge.pki.createCertificate();
271
+ const keys = nodeForge.pki.rsa.generateKeyPair(2048);
272
+ const cert = nodeForge.pki.createCertificate();
272
273
 
273
274
  cert.publicKey = keys.publicKey;
274
275
  cert.serialNumber = '01';
@@ -326,11 +327,11 @@ export class SecurityManager {
326
327
  ]);
327
328
 
328
329
  // Self-sign certificate
329
- cert.sign(keys.privateKey, plugins.nodeForge.md.sha256.create());
330
+ cert.sign(keys.privateKey, nodeForge.md.sha256.create());
330
331
 
331
332
  // Convert to PEM
332
- const certificatePem = plugins.nodeForge.pki.certificateToPem(cert);
333
- const privateKeyPem = plugins.nodeForge.pki.privateKeyToPem(keys.privateKey);
333
+ const certificatePem = nodeForge.pki.certificateToPem(cert);
334
+ const privateKeyPem = nodeForge.pki.privateKeyToPem(keys.privateKey);
334
335
 
335
336
  return {
336
337
  certificate: certificatePem,
@@ -402,4 +403,4 @@ export class SecurityManager {
402
403
 
403
404
  return ltv;
404
405
  }
405
- }
406
+ }
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 {
package/ts/skr03.data.ts CHANGED
@@ -159,6 +159,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [
159
159
  accountType: 'asset',
160
160
  skrType: 'SKR03',
161
161
  description: 'Trade receivables',
162
+ isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use debtor accounts (10000-69999)
162
163
  },
163
164
  {
164
165
  accountNumber: '1500',
@@ -199,6 +200,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [
199
200
  accountType: 'liability',
200
201
  skrType: 'SKR03',
201
202
  description: 'Trade payables',
203
+ isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use creditor accounts (70000-99999)
202
204
  },
203
205
  {
204
206
  accountNumber: '1700',