@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,760 @@
1
+ import * as plugins from './plugins.js';
2
+ import { JournalEntry } from './skr.classes.journalentry.js';
3
+ import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
4
+ import { suggestPostingKey } from './skr.postingkeys.js';
5
+ import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
6
+ import type {
7
+ IInvoice,
8
+ IInvoiceLine,
9
+ IBookingRules,
10
+ IBookingInfo,
11
+ TTaxScenario,
12
+ IPaymentInfo
13
+ } from './skr.invoice.entity.js';
14
+
15
+ /**
16
+ * Options for booking an invoice
17
+ */
18
+ export interface IBookingOptions {
19
+ autoBook?: boolean;
20
+ confidenceThreshold?: number;
21
+ bookingDate?: Date;
22
+ bookingReference?: string;
23
+ skipValidation?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Result of booking an invoice
28
+ */
29
+ export interface IBookingResult {
30
+ success: boolean;
31
+ journalEntry?: JournalEntry;
32
+ bookingInfo?: IBookingInfo;
33
+ confidence: number;
34
+ warnings?: string[];
35
+ errors?: string[];
36
+ }
37
+
38
+ /**
39
+ * Automatic booking engine for invoices
40
+ * Creates journal entries from invoice data based on SKR mapping rules
41
+ */
42
+ export class InvoiceBookingEngine {
43
+ private logger: plugins.smartlog.ConsoleLog;
44
+ private skrType: TSKRType;
45
+ private mapper: SKRInvoiceMapper;
46
+
47
+ constructor(skrType: TSKRType) {
48
+ this.skrType = skrType;
49
+ this.mapper = new SKRInvoiceMapper(skrType);
50
+ this.logger = new plugins.smartlog.ConsoleLog();
51
+ }
52
+
53
+ /**
54
+ * Book an invoice to the ledger
55
+ */
56
+ public async bookInvoice(
57
+ invoice: IInvoice,
58
+ bookingRules?: Partial<IBookingRules>,
59
+ options?: IBookingOptions
60
+ ): Promise<IBookingResult> {
61
+ try {
62
+ // Get complete booking rules
63
+ const rules = this.mapper.mapInvoiceToSKR(invoice, bookingRules);
64
+
65
+ // Calculate confidence
66
+ const confidence = this.mapper.calculateConfidence(invoice, rules);
67
+
68
+ // Check if auto-booking is allowed
69
+ if (options?.autoBook && confidence < (options.confidenceThreshold || 80)) {
70
+ return {
71
+ success: false,
72
+ confidence,
73
+ warnings: [`Confidence score ${confidence}% is below threshold ${options.confidenceThreshold || 80}%`]
74
+ };
75
+ }
76
+
77
+ // Validate invoice before booking
78
+ if (!options?.skipValidation) {
79
+ const validationErrors = this.validateInvoice(invoice);
80
+ if (validationErrors.length > 0) {
81
+ return {
82
+ success: false,
83
+ confidence,
84
+ errors: validationErrors
85
+ };
86
+ }
87
+ }
88
+
89
+ // Build journal entry
90
+ const journalEntry = await this.buildJournalEntry(invoice, rules, options);
91
+
92
+ // Create booking info
93
+ const bookingInfo: IBookingInfo = {
94
+ journalEntryId: journalEntry.id,
95
+ transactionIds: journalEntry.transactionIds || [],
96
+ bookedAt: new Date(),
97
+ bookedBy: 'system',
98
+ bookingRules: {
99
+ vendorAccount: rules.vendorControlAccount,
100
+ customerAccount: rules.customerControlAccount,
101
+ expenseAccounts: this.getUsedExpenseAccounts(invoice, rules),
102
+ revenueAccounts: this.getUsedRevenueAccounts(invoice, rules),
103
+ vatAccounts: this.getUsedVATAccounts(invoice, rules)
104
+ },
105
+ confidence,
106
+ autoBooked: options?.autoBook || false
107
+ };
108
+
109
+ // Post the journal entry
110
+ // TODO: When MongoDB transactions are available, wrap this in a transaction
111
+ // Example: await db.withTransaction(async (session) => { ... })
112
+ try {
113
+ await journalEntry.validate();
114
+ await journalEntry.post();
115
+
116
+ // Mark invoice as posted if we have a reference to it
117
+ if (invoice.status !== 'posted') {
118
+ invoice.status = 'posted';
119
+ }
120
+ } catch (postError) {
121
+ this.logger.log('error', `Failed to post journal entry: ${postError}`);
122
+ throw postError; // Re-throw to trigger rollback when transactions are available
123
+ }
124
+
125
+ return {
126
+ success: true,
127
+ journalEntry,
128
+ bookingInfo,
129
+ confidence,
130
+ warnings: this.generateWarnings(invoice, rules)
131
+ };
132
+ } catch (error) {
133
+ this.logger.log('error', `Failed to book invoice: ${error}`);
134
+ return {
135
+ success: false,
136
+ confidence: 0,
137
+ errors: [`Booking failed: ${error.message}`]
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Build journal entry from invoice
144
+ */
145
+ private async buildJournalEntry(
146
+ invoice: IInvoice,
147
+ rules: IBookingRules,
148
+ options?: IBookingOptions
149
+ ): Promise<JournalEntry> {
150
+ const lines: IJournalEntryLine[] = [];
151
+ const isInbound = invoice.direction === 'inbound';
152
+ const isCredit = invoice.invoiceTypeCode === '381'; // Credit note
153
+
154
+ // Determine if we need to reverse the normal booking direction
155
+ const reverseDirection = isCredit;
156
+
157
+ if (isInbound) {
158
+ // Inbound invoice (AP)
159
+ lines.push(...this.buildAPEntry(invoice, rules, reverseDirection));
160
+ } else {
161
+ // Outbound invoice (AR)
162
+ lines.push(...this.buildAREntry(invoice, rules, reverseDirection));
163
+ }
164
+
165
+ // Create journal entry
166
+ const journalData: IJournalEntry = {
167
+ date: options?.bookingDate || invoice.issueDate,
168
+ description: this.buildDescription(invoice),
169
+ reference: options?.bookingReference || invoice.invoiceNumber,
170
+ lines,
171
+ skrType: this.skrType
172
+ };
173
+
174
+ const journalEntry = new JournalEntry(journalData);
175
+ return journalEntry;
176
+ }
177
+
178
+ /**
179
+ * Build AP (Accounts Payable) journal entry lines
180
+ */
181
+ private buildAPEntry(
182
+ invoice: IInvoice,
183
+ rules: IBookingRules,
184
+ reverseDirection: boolean
185
+ ): IJournalEntryLine[] {
186
+ const lines: IJournalEntryLine[] = [];
187
+
188
+ // Group lines by account
189
+ const accountGroups = this.groupLinesByAccount(invoice, rules);
190
+
191
+ // Create expense/asset entries
192
+ for (const [accountNumber, group] of Object.entries(accountGroups)) {
193
+ const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
194
+
195
+ if (reverseDirection) {
196
+ // Credit note: credit expense account
197
+ lines.push({
198
+ accountNumber,
199
+ credit: Math.abs(amount),
200
+ description: this.getAccountDescription(accountNumber, group),
201
+ postingKey: 9 // 19% input VAT for expenses
202
+ });
203
+ } else {
204
+ // Regular invoice: debit expense account
205
+ lines.push({
206
+ accountNumber,
207
+ debit: Math.abs(amount),
208
+ description: this.getAccountDescription(accountNumber, group),
209
+ postingKey: 9 // 19% input VAT for expenses
210
+ });
211
+ }
212
+ }
213
+
214
+ // Create VAT entries
215
+ const vatLines = this.buildVATLines(invoice, rules, 'input', reverseDirection);
216
+ lines.push(...vatLines);
217
+
218
+ // Create vendor control account entry
219
+ const controlAccount = this.mapper.getControlAccount(invoice, rules);
220
+ const totalAmount = Math.abs(invoice.payableAmount);
221
+
222
+ if (reverseDirection) {
223
+ // Credit note: debit vendor account
224
+ lines.push({
225
+ accountNumber: controlAccount,
226
+ debit: totalAmount,
227
+ description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`,
228
+ postingKey: 40 // Tax-free for control account
229
+ });
230
+ } else {
231
+ // Regular invoice: credit vendor account
232
+ lines.push({
233
+ accountNumber: controlAccount,
234
+ credit: totalAmount,
235
+ description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`,
236
+ postingKey: 40 // Tax-free for control account
237
+ });
238
+ }
239
+
240
+ return lines;
241
+ }
242
+
243
+ /**
244
+ * Build AR (Accounts Receivable) journal entry lines
245
+ */
246
+ private buildAREntry(
247
+ invoice: IInvoice,
248
+ rules: IBookingRules,
249
+ reverseDirection: boolean
250
+ ): IJournalEntryLine[] {
251
+ const lines: IJournalEntryLine[] = [];
252
+
253
+ // Group lines by account
254
+ const accountGroups = this.groupLinesByAccount(invoice, rules);
255
+
256
+ // Create revenue entries
257
+ for (const [accountNumber, group] of Object.entries(accountGroups)) {
258
+ const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
259
+
260
+ if (reverseDirection) {
261
+ // Credit note: debit revenue account
262
+ lines.push({
263
+ accountNumber,
264
+ debit: Math.abs(amount),
265
+ description: this.getAccountDescription(accountNumber, group),
266
+ postingKey: 9 // 19% output VAT for revenue
267
+ });
268
+ } else {
269
+ // Regular invoice: credit revenue account
270
+ lines.push({
271
+ accountNumber,
272
+ credit: Math.abs(amount),
273
+ description: this.getAccountDescription(accountNumber, group),
274
+ postingKey: 9 // 19% output VAT for revenue
275
+ });
276
+ }
277
+ }
278
+
279
+ // Create VAT entries
280
+ const vatLines = this.buildVATLines(invoice, rules, 'output', reverseDirection);
281
+ lines.push(...vatLines);
282
+
283
+ // Create customer control account entry
284
+ const controlAccount = this.mapper.getControlAccount(invoice, rules);
285
+ const totalAmount = Math.abs(invoice.payableAmount);
286
+
287
+ if (reverseDirection) {
288
+ // Credit note: credit customer account
289
+ lines.push({
290
+ accountNumber: controlAccount,
291
+ credit: totalAmount,
292
+ description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`,
293
+ postingKey: 40 // Tax-free for control account
294
+ });
295
+ } else {
296
+ // Regular invoice: debit customer account
297
+ lines.push({
298
+ accountNumber: controlAccount,
299
+ debit: totalAmount,
300
+ description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`,
301
+ postingKey: 40 // Tax-free for control account
302
+ });
303
+ }
304
+
305
+ return lines;
306
+ }
307
+
308
+ /**
309
+ * Build VAT lines
310
+ */
311
+ private buildVATLines(
312
+ invoice: IInvoice,
313
+ rules: IBookingRules,
314
+ direction: 'input' | 'output',
315
+ reverseDirection: boolean
316
+ ): IJournalEntryLine[] {
317
+ const lines: IJournalEntryLine[] = [];
318
+ const taxScenario = invoice.taxScenario || 'domestic_taxed';
319
+
320
+ // Handle reverse charge specially
321
+ if (taxScenario === 'reverse_charge') {
322
+ return this.buildReverseChargeVATLines(invoice, rules);
323
+ }
324
+
325
+ // Standard VAT booking
326
+ for (const vatBreak of invoice.vatBreakdown) {
327
+ if (vatBreak.taxAmount === 0) continue;
328
+
329
+ const vatAccount = this.mapper.getVATAccount(
330
+ vatBreak.vatCategory,
331
+ direction,
332
+ taxScenario
333
+ );
334
+
335
+ const amount = Math.abs(vatBreak.taxAmount);
336
+ const description = `VAT ${vatBreak.vatCategory.rate}%`;
337
+ const vatRate = vatBreak.vatCategory.rate;
338
+ // Select posting key based on VAT rate: 8 for 7%, 9 for 19%
339
+ const postingKey = vatRate === 7 ? 8 : 9;
340
+
341
+ if (direction === 'input') {
342
+ // Input VAT (Vorsteuer)
343
+ if (reverseDirection) {
344
+ lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
345
+ } else {
346
+ lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
347
+ }
348
+ } else {
349
+ // Output VAT (Umsatzsteuer)
350
+ if (reverseDirection) {
351
+ lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
352
+ } else {
353
+ lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
354
+ }
355
+ }
356
+ }
357
+
358
+ return lines;
359
+ }
360
+
361
+ /**
362
+ * Calculate VAT amount from taxable amount and rate
363
+ */
364
+ private calculateVAT(taxableAmount: number, rate: number): number {
365
+ return Math.round(taxableAmount * rate / 100 * 100) / 100; // Round to 2 decimals
366
+ }
367
+
368
+ /**
369
+ * Calculate effective VAT rate for the invoice (weighted average)
370
+ */
371
+ private calculateEffectiveVATRate(invoice: IInvoice): number {
372
+ const totalTaxable = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxableAmount, 0);
373
+ if (totalTaxable === 0) {
374
+ return 19; // Default to standard German VAT rate
375
+ }
376
+
377
+ // Calculate weighted average VAT rate
378
+ const weightedRate = invoice.vatBreakdown.reduce((sum, vb) => {
379
+ return sum + (vb.vatCategory.rate * vb.taxableAmount);
380
+ }, 0);
381
+
382
+ return Math.round(weightedRate / totalTaxable * 100) / 100;
383
+ }
384
+
385
+ /**
386
+ * Build reverse charge VAT lines (§13b UStG)
387
+ */
388
+ private buildReverseChargeVATLines(
389
+ invoice: IInvoice,
390
+ rules: IBookingRules
391
+ ): IJournalEntryLine[] {
392
+ const lines: IJournalEntryLine[] = [];
393
+
394
+ // For reverse charge, we book both input and output VAT
395
+ for (const vatBreak of invoice.vatBreakdown) {
396
+ // For reverse charge, calculate VAT if not provided
397
+ const amount = vatBreak.taxAmount > 0
398
+ ? Math.abs(vatBreak.taxAmount)
399
+ : this.calculateVAT(Math.abs(vatBreak.taxableAmount), vatBreak.vatCategory.rate);
400
+
401
+ // Input VAT (deductible)
402
+ const inputVATAccount = this.mapper.getVATAccount(
403
+ vatBreak.vatCategory,
404
+ 'input',
405
+ 'reverse_charge'
406
+ );
407
+
408
+ // Output VAT (payable)
409
+ const outputVATAccount = this.mapper.getVATAccount(
410
+ vatBreak.vatCategory,
411
+ 'output',
412
+ 'reverse_charge'
413
+ );
414
+
415
+ lines.push(
416
+ {
417
+ accountNumber: inputVATAccount,
418
+ debit: amount,
419
+ description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`,
420
+ postingKey: 94 // Reverse charge posting key
421
+ },
422
+ {
423
+ accountNumber: outputVATAccount,
424
+ credit: amount,
425
+ description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`,
426
+ postingKey: 94 // Reverse charge posting key
427
+ }
428
+ );
429
+ }
430
+
431
+ return lines;
432
+ }
433
+
434
+ /**
435
+ * Group invoice lines by account
436
+ */
437
+ private groupLinesByAccount(
438
+ invoice: IInvoice,
439
+ rules: IBookingRules
440
+ ): Record<string, IInvoiceLine[]> {
441
+ const groups: Record<string, IInvoiceLine[]> = {};
442
+
443
+ for (const line of invoice.lines) {
444
+ const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
445
+
446
+ if (!groups[account]) {
447
+ groups[account] = [];
448
+ }
449
+ groups[account].push(line);
450
+ }
451
+
452
+ return groups;
453
+ }
454
+
455
+ /**
456
+ * Book payment for an invoice
457
+ */
458
+ public async bookPayment(
459
+ invoice: IInvoice,
460
+ payment: IPaymentInfo,
461
+ rules: IBookingRules
462
+ ): Promise<IBookingResult> {
463
+ try {
464
+ const lines: IJournalEntryLine[] = [];
465
+ const isInbound = invoice.direction === 'inbound';
466
+ const controlAccount = this.mapper.getControlAccount(invoice, rules);
467
+
468
+ // Check for skonto
469
+ const skontoAmount = payment.skontoTaken || 0;
470
+ const paymentAmount = payment.amount;
471
+ const fullAmount = paymentAmount + skontoAmount;
472
+
473
+ if (isInbound) {
474
+ // Payment for vendor invoice
475
+ lines.push(
476
+ {
477
+ accountNumber: controlAccount,
478
+ debit: fullAmount,
479
+ description: `Payment to ${invoice.supplier.name}`,
480
+ postingKey: 3 // Payment with VAT
481
+ },
482
+ {
483
+ accountNumber: '1000', // Bank account (would be configurable)
484
+ credit: paymentAmount,
485
+ description: `Bank payment ${payment.endToEndId || payment.paymentId}`,
486
+ postingKey: 40 // Tax-free for bank account
487
+ }
488
+ );
489
+
490
+ // Book skonto if taken
491
+ if (skontoAmount > 0) {
492
+ const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
493
+ lines.push({
494
+ accountNumber: skontoAccounts.skontoAccount,
495
+ credit: skontoAmount,
496
+ description: `Skonto received`,
497
+ postingKey: 40 // Tax-free for skonto
498
+ });
499
+
500
+ // VAT correction for skonto
501
+ if (rules.skontoMethod === 'gross') {
502
+ const effectiveRate = this.calculateEffectiveVATRate(invoice);
503
+ const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
504
+ lines.push(
505
+ {
506
+ accountNumber: skontoAccounts.vatCorrectionAccount,
507
+ credit: vatCorrection,
508
+ description: `Skonto VAT correction`,
509
+ postingKey: 40 // Tax-free for correction
510
+ }
511
+ );
512
+ }
513
+ }
514
+ } else {
515
+ // Payment from customer
516
+ lines.push(
517
+ {
518
+ accountNumber: '1000', // Bank account
519
+ debit: paymentAmount,
520
+ description: `Payment from ${invoice.customer.name}`,
521
+ postingKey: 40 // Tax-free for bank account
522
+ },
523
+ {
524
+ accountNumber: controlAccount,
525
+ credit: fullAmount,
526
+ description: `Customer payment ${payment.endToEndId || payment.paymentId}`,
527
+ postingKey: 3 // Payment with VAT
528
+ }
529
+ );
530
+
531
+ // Book skonto if granted
532
+ if (skontoAmount > 0) {
533
+ const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
534
+ lines.push({
535
+ accountNumber: skontoAccounts.skontoAccount,
536
+ debit: skontoAmount,
537
+ description: `Skonto granted`,
538
+ postingKey: 40 // Tax-free for skonto
539
+ });
540
+
541
+ // VAT correction for skonto
542
+ if (rules.skontoMethod === 'gross') {
543
+ const effectiveRate = this.calculateEffectiveVATRate(invoice);
544
+ const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
545
+ lines.push(
546
+ {
547
+ accountNumber: skontoAccounts.vatCorrectionAccount,
548
+ debit: vatCorrection,
549
+ description: `Skonto VAT correction`,
550
+ postingKey: 40 // Tax-free for correction
551
+ }
552
+ );
553
+ }
554
+ }
555
+ }
556
+
557
+ // Create journal entry for payment
558
+ const journalData: IJournalEntry = {
559
+ date: payment.paymentDate,
560
+ description: `Payment for invoice ${invoice.invoiceNumber}`,
561
+ reference: payment.endToEndId || payment.remittanceInfo || payment.paymentId,
562
+ lines,
563
+ skrType: this.skrType
564
+ };
565
+
566
+ const journalEntry = new JournalEntry(journalData);
567
+ await journalEntry.validate();
568
+ await journalEntry.post();
569
+
570
+ return {
571
+ success: true,
572
+ journalEntry,
573
+ confidence: 100
574
+ };
575
+ } catch (error) {
576
+ this.logger.log('error', `Failed to book payment: ${error}`);
577
+ return {
578
+ success: false,
579
+ confidence: 0,
580
+ errors: [`Payment booking failed: ${error.message}`]
581
+ };
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Validate invoice before booking
587
+ */
588
+ private validateInvoice(invoice: IInvoice): string[] {
589
+ const errors: string[] = [];
590
+
591
+ // Check required fields
592
+ if (!invoice.invoiceNumber) {
593
+ errors.push('Invoice number is required');
594
+ }
595
+
596
+ if (!invoice.issueDate) {
597
+ errors.push('Issue date is required');
598
+ }
599
+
600
+ if (!invoice.supplier || !invoice.supplier.name) {
601
+ errors.push('Supplier information is required');
602
+ }
603
+
604
+ if (!invoice.customer || !invoice.customer.name) {
605
+ errors.push('Customer information is required');
606
+ }
607
+
608
+ if (invoice.lines.length === 0) {
609
+ errors.push('Invoice must have at least one line item');
610
+ }
611
+
612
+ // Validate amounts
613
+ const calculatedNet = invoice.lines.reduce((sum, line) => sum + line.netAmount, 0);
614
+ const tolerance = 0.01;
615
+
616
+ if (Math.abs(calculatedNet - invoice.lineNetAmount) > tolerance) {
617
+ errors.push(`Line net amount mismatch: calculated ${calculatedNet}, stated ${invoice.lineNetAmount}`);
618
+ }
619
+
620
+ // Validate VAT
621
+ const calculatedVAT = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxAmount, 0);
622
+ if (Math.abs(calculatedVAT - invoice.totalVATAmount) > tolerance) {
623
+ errors.push(`VAT amount mismatch: calculated ${calculatedVAT}, stated ${invoice.totalVATAmount}`);
624
+ }
625
+
626
+ // Validate total
627
+ const calculatedTotal = invoice.taxExclusiveAmount + invoice.totalVATAmount;
628
+ if (Math.abs(calculatedTotal - invoice.taxInclusiveAmount) > tolerance) {
629
+ errors.push(`Total amount mismatch: calculated ${calculatedTotal}, stated ${invoice.taxInclusiveAmount}`);
630
+ }
631
+
632
+ return errors;
633
+ }
634
+
635
+ /**
636
+ * Generate warnings for the booking
637
+ */
638
+ private generateWarnings(invoice: IInvoice, rules: IBookingRules): string[] {
639
+ const warnings: string[] = [];
640
+
641
+ // Warn about default account usage
642
+ const hasDefaultAccounts = invoice.lines.some(line =>
643
+ !line.accountNumber && !line.productCode
644
+ );
645
+ if (hasDefaultAccounts) {
646
+ warnings.push('Some lines are using default expense/revenue accounts');
647
+ }
648
+
649
+ // Warn about mixed VAT rates
650
+ if (invoice.vatBreakdown.length > 1) {
651
+ warnings.push('Invoice contains mixed VAT rates');
652
+ }
653
+
654
+ // Warn about reverse charge
655
+ if (invoice.taxScenario === 'reverse_charge') {
656
+ warnings.push('Reverse charge procedure applied - verify VAT treatment');
657
+ }
658
+
659
+ // Warn about credit notes
660
+ if (invoice.invoiceTypeCode === '381') {
661
+ warnings.push('This is a credit note - amounts will be reversed');
662
+ }
663
+
664
+ // Warn about foreign currency
665
+ if (invoice.currencyCode !== 'EUR') {
666
+ warnings.push(`Invoice is in foreign currency: ${invoice.currencyCode}`);
667
+ }
668
+
669
+ return warnings;
670
+ }
671
+
672
+ /**
673
+ * Build description for journal entry
674
+ */
675
+ private buildDescription(invoice: IInvoice): string {
676
+ const type = invoice.invoiceTypeCode === '381' ? 'Credit Note' : 'Invoice';
677
+ const party = invoice.direction === 'inbound'
678
+ ? invoice.supplier.name
679
+ : invoice.customer.name;
680
+
681
+ return `${type} ${invoice.invoiceNumber} - ${party}`;
682
+ }
683
+
684
+ /**
685
+ * Get account description for a group of lines
686
+ */
687
+ private getAccountDescription(accountNumber: string, lines: IInvoiceLine[]): string {
688
+ if (lines.length === 1) {
689
+ return lines[0].description;
690
+ }
691
+
692
+ return `${this.mapper.getAccountDescription(accountNumber)} (${lines.length} items)`;
693
+ }
694
+
695
+ /**
696
+ * Get used expense accounts
697
+ */
698
+ private getUsedExpenseAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
699
+ if (invoice.direction !== 'inbound') return [];
700
+
701
+ const accounts = new Set<string>();
702
+ for (const line of invoice.lines) {
703
+ const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
704
+ accounts.add(account);
705
+ }
706
+ return Array.from(accounts);
707
+ }
708
+
709
+ /**
710
+ * Get used revenue accounts
711
+ */
712
+ private getUsedRevenueAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
713
+ if (invoice.direction !== 'outbound') return [];
714
+
715
+ const accounts = new Set<string>();
716
+ for (const line of invoice.lines) {
717
+ const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
718
+ accounts.add(account);
719
+ }
720
+ return Array.from(accounts);
721
+ }
722
+
723
+ /**
724
+ * Get used VAT accounts
725
+ */
726
+ private getUsedVATAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
727
+ const accounts = new Set<string>();
728
+ const direction = invoice.direction === 'inbound' ? 'input' : 'output';
729
+ const taxScenario = invoice.taxScenario || 'domestic_taxed';
730
+
731
+ for (const vatBreak of invoice.vatBreakdown) {
732
+ const account = this.mapper.getVATAccount(
733
+ vatBreak.vatCategory,
734
+ direction,
735
+ taxScenario
736
+ );
737
+ accounts.add(account);
738
+ }
739
+
740
+ // Add reverse charge accounts if applicable
741
+ if (taxScenario === 'reverse_charge') {
742
+ for (const vatBreak of invoice.vatBreakdown) {
743
+ const inputAccount = this.mapper.getVATAccount(
744
+ vatBreak.vatCategory,
745
+ 'input',
746
+ 'reverse_charge'
747
+ );
748
+ const outputAccount = this.mapper.getVATAccount(
749
+ vatBreak.vatCategory,
750
+ 'output',
751
+ 'reverse_charge'
752
+ );
753
+ accounts.add(inputAccount);
754
+ accounts.add(outputAccount);
755
+ }
756
+ }
757
+
758
+ return Array.from(accounts);
759
+ }
760
+ }