@fin.cx/skr 1.1.0 → 1.2.0

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