@fin.cx/skr 1.3.0 → 2.0.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 (94) hide show
  1. package/.smartconfig.json +33 -6
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/core.accountpolicy.d.ts +67 -0
  4. package/dist_ts/core/core.accountpolicy.js +91 -0
  5. package/dist_ts/core/core.bukeys.d.ts +38 -0
  6. package/dist_ts/core/core.bukeys.js +117 -0
  7. package/dist_ts/core/core.datev.captions.d.ts +7 -0
  8. package/dist_ts/core/core.datev.captions.js +134 -0
  9. package/dist_ts/core/core.datev.d.ts +47 -0
  10. package/dist_ts/core/core.datev.js +129 -0
  11. package/dist_ts/core/core.hash.d.ts +27 -0
  12. package/dist_ts/core/core.hash.js +72 -0
  13. package/dist_ts/core/core.plugins.d.ts +3 -0
  14. package/dist_ts/core/core.plugins.js +9 -0
  15. package/dist_ts/core/core.recipes.d.ts +118 -0
  16. package/dist_ts/core/core.recipes.js +504 -0
  17. package/dist_ts/core/core.taxscenario.d.ts +27 -0
  18. package/dist_ts/core/core.taxscenario.js +85 -0
  19. package/dist_ts/core/core.types.d.ts +48 -0
  20. package/dist_ts/core/core.types.js +2 -0
  21. package/dist_ts/core/core.validate.d.ts +17 -0
  22. package/dist_ts/core/core.validate.js +89 -0
  23. package/dist_ts/core/index.d.ts +15 -0
  24. package/dist_ts/core/index.js +16 -0
  25. package/dist_ts/index.d.ts +15 -0
  26. package/dist_ts/index.js +10 -1
  27. package/dist_ts/plugins.d.ts +2 -1
  28. package/dist_ts/plugins.js +4 -2
  29. package/dist_ts/skr.api.d.ts +91 -22
  30. package/dist_ts/skr.api.js +357 -42
  31. package/dist_ts/skr.balances.d.ts +28 -0
  32. package/dist_ts/skr.balances.js +96 -0
  33. package/dist_ts/skr.classes.journalentry.d.ts +19 -3
  34. package/dist_ts/skr.classes.journalentry.js +67 -4
  35. package/dist_ts/skr.classes.reports.js +49 -102
  36. package/dist_ts/skr.database.d.ts +2 -0
  37. package/dist_ts/skr.database.js +1 -1
  38. package/dist_ts/skr.errors.d.ts +18 -0
  39. package/dist_ts/skr.errors.js +34 -0
  40. package/dist_ts/skr.export.js +4 -7
  41. package/dist_ts/skr.invoice.adapter.d.ts +15 -1
  42. package/dist_ts/skr.invoice.adapter.js +253 -41
  43. package/dist_ts/skr.invoice.booking.js +14 -28
  44. package/dist_ts/skr.migrate.d.ts +20 -0
  45. package/dist_ts/skr.migrate.js +223 -0
  46. package/dist_ts/skr.period.d.ts +47 -0
  47. package/dist_ts/skr.period.js +95 -0
  48. package/dist_ts/skr.posting.d.ts +74 -0
  49. package/dist_ts/skr.posting.js +209 -0
  50. package/dist_ts/skr.security.d.ts +6 -9
  51. package/dist_ts/skr.security.js +16 -89
  52. package/dist_ts/skr.sequence.d.ts +32 -0
  53. package/dist_ts/skr.sequence.js +51 -0
  54. package/dist_ts/skr.txview.d.ts +46 -0
  55. package/dist_ts/skr.txview.js +235 -0
  56. package/dist_ts/skr.types.d.ts +35 -1
  57. package/dist_ts/skr.verify.d.ts +13 -0
  58. package/dist_ts/skr.verify.js +84 -0
  59. package/package.json +21 -24
  60. package/readme.hints.md +37 -0
  61. package/readme.md +94 -1
  62. package/readme.plan.md +180 -243
  63. package/ts/00_commitinfo_data.ts +1 -1
  64. package/ts/core/core.accountpolicy.ts +154 -0
  65. package/ts/core/core.bukeys.ts +146 -0
  66. package/ts/core/core.datev.captions.ts +133 -0
  67. package/ts/core/core.datev.ts +174 -0
  68. package/ts/core/core.hash.ts +76 -0
  69. package/ts/core/core.plugins.ts +9 -0
  70. package/ts/core/core.recipes.ts +659 -0
  71. package/ts/core/core.taxscenario.ts +156 -0
  72. package/ts/core/core.types.ts +74 -0
  73. package/ts/core/core.validate.ts +119 -0
  74. package/ts/core/index.ts +15 -0
  75. package/ts/index.ts +31 -0
  76. package/ts/plugins.ts +4 -0
  77. package/ts/skr.api.ts +432 -46
  78. package/ts/skr.balances.ts +127 -0
  79. package/ts/skr.classes.journalentry.ts +54 -5
  80. package/ts/skr.classes.reports.ts +54 -126
  81. package/ts/skr.database.ts +3 -0
  82. package/ts/skr.errors.ts +44 -0
  83. package/ts/skr.export.ts +3 -6
  84. package/ts/skr.invoice.adapter.ts +291 -45
  85. package/ts/skr.invoice.booking.ts +22 -34
  86. package/ts/skr.migrate.ts +268 -0
  87. package/ts/skr.period.ts +139 -0
  88. package/ts/skr.posting.ts +273 -0
  89. package/ts/skr.security.ts +18 -93
  90. package/ts/skr.sequence.ts +89 -0
  91. package/ts/skr.txview.ts +268 -0
  92. package/ts/skr.types.ts +37 -1
  93. package/ts/skr.verify.ts +99 -0
  94. /package/{license.md → license} +0 -0
@@ -69,13 +69,13 @@ export class InvoiceAdapter {
69
69
  }
70
70
 
71
71
  // Get detected format
72
- const format = this.mapEInvoiceFormat(einvoice.format || 'xrechnung');
72
+ const format = this.mapEInvoiceFormat(this.getDetectedFormat(einvoice));
73
73
 
74
74
  // Validate the invoice (takes ~2.2ms)
75
75
  const validationResult = await this.validateInvoice(einvoice);
76
76
 
77
77
  // Extract invoice data
78
- const invoiceData = einvoice.toObject();
78
+ const invoiceData = this.mapEInvoiceToBusinessTerms(einvoice);
79
79
 
80
80
  // Map to internal invoice model
81
81
  const invoice = await this.mapToInternalModel(
@@ -106,33 +106,9 @@ export class InvoiceAdapter {
106
106
  * Validate an invoice using multi-level validation
107
107
  */
108
108
  private async validateInvoice(einvoice: any): Promise<IValidationResult> {
109
- // Perform multi-level validation
110
- const validationResult = await einvoice.validate();
111
-
112
- // Parse validation results into our structure
113
- const syntaxResult = {
114
- isValid: validationResult.syntax?.valid !== false,
115
- errors: validationResult.syntax?.errors || [],
116
- warnings: validationResult.syntax?.warnings || []
117
- };
118
-
119
- const semanticResult = {
120
- isValid: validationResult.semantic?.valid !== false,
121
- errors: validationResult.semantic?.errors || [],
122
- warnings: validationResult.semantic?.warnings || []
123
- };
124
-
125
- const businessResult = {
126
- isValid: validationResult.business?.valid !== false,
127
- errors: validationResult.business?.errors || [],
128
- warnings: validationResult.business?.warnings || []
129
- };
130
-
131
- const countryResult = {
132
- isValid: validationResult.country?.valid !== false,
133
- errors: validationResult.country?.errors || [],
134
- warnings: validationResult.country?.warnings || []
135
- };
109
+ const syntaxResult = await this.validateInvoiceLevel(einvoice, 'syntax');
110
+ const semanticResult = await this.validateInvoiceLevel(einvoice, 'semantic');
111
+ const businessResult = await this.validateInvoiceLevel(einvoice, 'business');
136
112
 
137
113
  return {
138
114
  isValid: syntaxResult.isValid && semanticResult.isValid && businessResult.isValid,
@@ -152,15 +128,219 @@ export class InvoiceAdapter {
152
128
  warnings: businessResult.warnings || []
153
129
  },
154
130
  countrySpecific: {
155
- valid: countryResult.isValid,
156
- errors: countryResult.errors || [],
157
- warnings: countryResult.warnings || []
131
+ valid: true,
132
+ errors: [],
133
+ warnings: []
158
134
  },
159
135
  validatedAt: new Date(),
160
- validatorVersion: '5.1.4'
136
+ validatorVersion: '6.0.0'
137
+ };
138
+ }
139
+
140
+ private async validateInvoiceLevel(
141
+ einvoice: any,
142
+ level: 'syntax' | 'semantic' | 'business'
143
+ ): Promise<{ isValid: boolean; errors: string[]; warnings: string[] }> {
144
+ const validationResult = await einvoice.validate(level);
145
+
146
+ return {
147
+ isValid: validationResult.valid !== false,
148
+ errors: this.formatValidationMessages(validationResult.errors),
149
+ warnings: this.formatValidationMessages(validationResult.warnings),
150
+ };
151
+ }
152
+
153
+ private formatValidationMessages(messages: any[] = []): string[] {
154
+ return messages.map((message) => {
155
+ if (typeof message === 'string') {
156
+ return message;
157
+ }
158
+
159
+ return [message.code, message.message, message.location]
160
+ .filter(Boolean)
161
+ .join(': ');
162
+ });
163
+ }
164
+
165
+ private getDetectedFormat(einvoice: any): string {
166
+ if (typeof einvoice.getFormat === 'function') {
167
+ return einvoice.getFormat();
168
+ }
169
+
170
+ return einvoice.format || 'xrechnung';
171
+ }
172
+
173
+ private mapEInvoiceToBusinessTerms(einvoice: any): any {
174
+ const issueDate = new Date(einvoice.date || Date.now());
175
+ const invoiceLines = this.mapEInvoiceLinesToBusinessTerms(einvoice.items || [], Boolean(einvoice.reverseCharge));
176
+ const vatBreakdown = this.buildVatBreakdown(invoiceLines);
177
+ const lineNetAmount = invoiceLines.reduce((sum, line) => sum + line.BT131_LineNetAmount, 0);
178
+ const totalVATAmount = vatBreakdown.reduce((sum, vat) => sum + vat.BT117_TaxAmount, 0);
179
+ const taxInclusiveAmount = lineNetAmount + totalVATAmount;
180
+
181
+ return {
182
+ BT1_InvoiceNumber: einvoice.accountingDocId || einvoice.id || einvoice.invoiceId || '',
183
+ BT2_IssueDate: issueDate.toISOString(),
184
+ BT3_InvoiceTypeCode: einvoice.accountingDocType === 'creditnote' ? '381' : '380',
185
+ BT5_CurrencyCode: einvoice.currency || 'EUR',
186
+ BT7_TaxPointDate: einvoice.deliveryDate ? new Date(einvoice.deliveryDate).toISOString() : undefined,
187
+ BT9_PaymentDueDate: this.getDueDate(issueDate, einvoice.dueInDays)?.toISOString(),
188
+ BT10_BuyerReference: einvoice.buyerReference,
189
+ BT22_InvoiceNote: Array.isArray(einvoice.notes) ? einvoice.notes.join('\n') : undefined,
190
+ BG4_Seller: this.mapEInvoiceContactToBusinessTerms(einvoice.from, 'seller', einvoice.paymentOptions),
191
+ BG7_Buyer: this.mapEInvoiceContactToBusinessTerms(einvoice.to, 'buyer'),
192
+ BG25_InvoiceLines: invoiceLines,
193
+ BG20_DocumentAllowances: [],
194
+ BG21_DocumentCharges: [],
195
+ BT106_SumOfLineNetAmounts: lineNetAmount,
196
+ BT107_AllowanceTotalAmount: 0,
197
+ BT108_ChargeTotalAmount: 0,
198
+ BT109_TaxExclusiveAmount: lineNetAmount,
199
+ BT110_TotalVATAmount: totalVATAmount,
200
+ BT112_TaxInclusiveAmount: taxInclusiveAmount,
201
+ BT113_PrepaidAmount: 0,
202
+ BT115_PayableAmount: taxInclusiveAmount,
203
+ BG23_VATBreakdown: vatBreakdown,
204
+ BG16_PaymentInstructions: this.mapPaymentOptionsToBusinessTerms(einvoice.paymentOptions),
161
205
  };
162
206
  }
163
207
 
208
+ private mapEInvoiceLinesToBusinessTerms(items: any[], reverseCharge: boolean): any[] {
209
+ return items.map((item, index) => {
210
+ const quantity = Number(item.unitQuantity || 0);
211
+ const unitPrice = Number(item.unitNetPrice || 0);
212
+ const vatRate = Number(item.vatPercentage || 0);
213
+ const netAmount = quantity * unitPrice;
214
+ const vatAmount = reverseCharge ? 0 : netAmount * vatRate / 100;
215
+
216
+ return {
217
+ BT154_ItemDescription: item.name || '',
218
+ BT129_Quantity: quantity,
219
+ BT146_NetPrice: unitPrice,
220
+ BT131_LineNetAmount: netAmount,
221
+ BT151_ItemVATCategory: this.getVatCategoryCode(vatRate, reverseCharge),
222
+ BT152_ItemVATRate: vatRate,
223
+ BT155_ItemSellerID: item.articleNumber,
224
+ lineVATAmount: vatAmount,
225
+ };
226
+ });
227
+ }
228
+
229
+ private buildVatBreakdown(invoiceLines: any[]): any[] {
230
+ const vatBreakdownMap = new Map<string, { categoryCode: string; rate: number; taxableAmount: number; taxAmount: number }>();
231
+
232
+ for (const line of invoiceLines) {
233
+ const categoryCode = line.BT151_ItemVATCategory || 'S';
234
+ const rate = Number(line.BT152_ItemVATRate || 0);
235
+ const key = `${categoryCode}:${rate}`;
236
+ const existingBreakdown = vatBreakdownMap.get(key) || {
237
+ categoryCode,
238
+ rate,
239
+ taxableAmount: 0,
240
+ taxAmount: 0,
241
+ };
242
+
243
+ existingBreakdown.taxableAmount += Number(line.BT131_LineNetAmount || 0);
244
+ existingBreakdown.taxAmount += Number(line.lineVATAmount || 0);
245
+ vatBreakdownMap.set(key, existingBreakdown);
246
+ }
247
+
248
+ return Array.from(vatBreakdownMap.values()).map((vatBreakdown) => ({
249
+ BT118_VATCategory: vatBreakdown.categoryCode,
250
+ BT119_VATRate: vatBreakdown.rate,
251
+ BT116_TaxableAmount: vatBreakdown.taxableAmount,
252
+ BT117_TaxAmount: vatBreakdown.taxAmount,
253
+ }));
254
+ }
255
+
256
+ private mapEInvoiceContactToBusinessTerms(
257
+ contact: any,
258
+ role: 'seller' | 'buyer',
259
+ paymentOptions?: any,
260
+ ): any {
261
+ const address = contact?.address || {};
262
+ const registrationDetails = contact?.registrationDetails || {};
263
+ const countryCode = this.normalizeCountryCode(address.countryCode || address.country);
264
+
265
+ if (role === 'seller') {
266
+ return {
267
+ BT27_SellerName: contact?.name || '',
268
+ BT29_SellerID: registrationDetails.registrationId,
269
+ BT31_SellerVATID: registrationDetails.vatId,
270
+ BT32_SellerTaxID: registrationDetails.registrationId,
271
+ BT33_SellerPhone: contact?.phone,
272
+ BT34_SellerEmail: contact?.email,
273
+ BT35_SellerStreet: this.formatStreet(address),
274
+ BT37_SellerCity: address.city,
275
+ BT38_SellerPostalCode: address.postalCode,
276
+ BT40_SellerCountryCode: countryCode,
277
+ BT84_PaymentAccountID: paymentOptions?.sepaConnection?.iban,
278
+ BT85_PaymentAccountName: contact?.name,
279
+ BT86_PaymentServiceProviderID: paymentOptions?.sepaConnection?.bic,
280
+ };
281
+ }
282
+
283
+ return {
284
+ BT44_BuyerName: contact?.name || '',
285
+ BT46_BuyerID: registrationDetails.registrationId,
286
+ BT47_BuyerTaxID: registrationDetails.registrationId,
287
+ BT48_BuyerVATID: registrationDetails.vatId,
288
+ BT49_BuyerEmail: contact?.email,
289
+ BT50_BuyerStreet: this.formatStreet(address),
290
+ BT52_BuyerCity: address.city,
291
+ BT53_BuyerPostalCode: address.postalCode,
292
+ BT55_BuyerCountryCode: countryCode,
293
+ };
294
+ }
295
+
296
+ private mapPaymentOptionsToBusinessTerms(paymentOptions: any): any | undefined {
297
+ if (!paymentOptions?.sepaConnection?.iban) {
298
+ return undefined;
299
+ }
300
+
301
+ return {
302
+ BT81_PaymentMeansCode: '30',
303
+ BT84_PaymentAccountID: paymentOptions.sepaConnection.iban,
304
+ BT85_PaymentAccountName: paymentOptions.description,
305
+ BT86_PaymentServiceProviderID: paymentOptions.sepaConnection.bic,
306
+ };
307
+ }
308
+
309
+ private getDueDate(issueDate: Date, dueInDays?: number): Date | undefined {
310
+ if (typeof dueInDays !== 'number') {
311
+ return undefined;
312
+ }
313
+
314
+ return new Date(issueDate.getTime() + dueInDays * 24 * 60 * 60 * 1000);
315
+ }
316
+
317
+ private getVatCategoryCode(vatRate: number, reverseCharge: boolean): string {
318
+ if (reverseCharge) {
319
+ return 'AE';
320
+ }
321
+
322
+ return vatRate === 0 ? 'Z' : 'S';
323
+ }
324
+
325
+ private normalizeCountryCode(country?: string): string {
326
+ if (!country) {
327
+ return 'DE';
328
+ }
329
+
330
+ const countryMap: Record<string, string> = {
331
+ Germany: 'DE',
332
+ Deutschland: 'DE',
333
+ };
334
+
335
+ return countryMap[country] || country;
336
+ }
337
+
338
+ private formatStreet(address: any): string {
339
+ return [address.streetName, address.houseNumber]
340
+ .filter(Boolean)
341
+ .join(' ');
342
+ }
343
+
164
344
  /**
165
345
  * Map EN16931 Business Terms to internal invoice model
166
346
  */
@@ -559,8 +739,8 @@ export class InvoiceAdapter {
559
739
  const einvoice: any = new EInvoice();
560
740
 
561
741
  // Set invoice data
562
- const businessTerms = this.mapToBusinessTerms(invoiceData);
563
- Object.assign(einvoice, businessTerms);
742
+ const einvoiceData = this.mapToEInvoiceData(invoiceData);
743
+ Object.assign(einvoice, einvoiceData);
564
744
 
565
745
  // Generate XML in requested format
566
746
  const xml = await einvoice.exportXml(format as any);
@@ -585,17 +765,83 @@ export class InvoiceAdapter {
585
765
  /**
586
766
  * Map internal invoice to EN16931 Business Terms
587
767
  */
588
- private mapToBusinessTerms(invoice: Partial<IInvoice>): any {
768
+ private mapToEInvoiceData(invoice: Partial<IInvoice>): any {
769
+ const issueDate = invoice.issueDate || new Date();
770
+
589
771
  return {
590
- BT1_InvoiceNumber: invoice.invoiceNumber,
591
- BT2_IssueDate: invoice.issueDate?.toISOString(),
592
- BT3_InvoiceTypeCode: invoice.invoiceTypeCode || '380',
593
- BT5_CurrencyCode: invoice.currencyCode || 'EUR',
594
- BT7_TaxPointDate: invoice.taxPointDate?.toISOString(),
595
- BT9_PaymentDueDate: invoice.paymentDueDate?.toISOString(),
596
-
597
- // Map other Business Terms...
598
- // This would be a comprehensive mapping in production
772
+ id: invoice.invoiceNumber || '',
773
+ accountingDocId: invoice.invoiceNumber || '',
774
+ accountingDocType: invoice.invoiceTypeCode === '381' ? 'creditnote' : 'invoice',
775
+ date: issueDate.getTime(),
776
+ currency: invoice.currencyCode || 'EUR',
777
+ buyerReference: invoice.buyerReference,
778
+ deliveryDate: invoice.taxPointDate?.getTime(),
779
+ dueInDays: this.calculateDueInDays(issueDate, invoice.paymentDueDate),
780
+ notes: invoice.invoiceNote ? [invoice.invoiceNote] : [],
781
+ from: this.mapInvoicePartyToEInvoiceContact(invoice.supplier),
782
+ to: this.mapInvoicePartyToEInvoiceContact(invoice.customer),
783
+ items: (invoice.lines || []).map((line) => ({
784
+ position: line.lineNumber,
785
+ name: line.description,
786
+ articleNumber: line.productCode,
787
+ unitType: 'EA',
788
+ unitQuantity: line.quantity,
789
+ unitNetPrice: line.unitPrice,
790
+ vatPercentage: line.vatCategory.rate,
791
+ })),
792
+ paymentOptions: invoice.paymentMeans?.account ? {
793
+ description: invoice.paymentMeans.account.accountHolder,
794
+ sepaConnection: {
795
+ iban: invoice.paymentMeans.account.iban,
796
+ bic: invoice.paymentMeans.account.bic || '',
797
+ },
798
+ payPal: {
799
+ email: '',
800
+ },
801
+ } : undefined,
599
802
  };
600
803
  }
804
+
805
+ private mapInvoicePartyToEInvoiceContact(party?: IInvoiceParty): any {
806
+ const countryCode = this.normalizeCountryCode(party?.address?.countryCode);
807
+
808
+ return {
809
+ type: 'company',
810
+ name: party?.name || '',
811
+ description: '',
812
+ address: {
813
+ streetName: party?.address?.street || '',
814
+ houseNumber: '',
815
+ city: party?.address?.city || '',
816
+ postalCode: party?.address?.postalCode || '',
817
+ country: countryCode,
818
+ countryCode,
819
+ },
820
+ registrationDetails: {
821
+ vatId: party?.vatId || '',
822
+ registrationId: party?.id || party?.taxId || '',
823
+ registrationName: party?.name || '',
824
+ },
825
+ status: 'active',
826
+ foundedDate: {
827
+ year: new Date().getFullYear(),
828
+ month: new Date().getMonth() + 1,
829
+ day: new Date().getDate(),
830
+ },
831
+ email: party?.email,
832
+ phone: party?.phone,
833
+ sepaConnection: party?.bankAccount ? {
834
+ iban: party.bankAccount.iban,
835
+ bic: party.bankAccount.bic || '',
836
+ } : undefined,
837
+ };
838
+ }
839
+
840
+ private calculateDueInDays(issueDate: Date, paymentDueDate?: Date): number {
841
+ if (!paymentDueDate) {
842
+ return 30;
843
+ }
844
+
845
+ return Math.max(0, Math.round((paymentDueDate.getTime() - issueDate.getTime()) / (24 * 60 * 60 * 1000)));
846
+ }
601
847
  }
@@ -1,5 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { JournalEntry } from './skr.classes.journalentry.js';
3
+ import { JournalPoster } from './skr.posting.js';
4
+ import { journalInputToDraft } from './skr.txview.js';
3
5
  import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
4
6
  import { suggestPostingKey } from './skr.postingkeys.js';
5
7
  import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
@@ -86,13 +88,18 @@ export class InvoiceBookingEngine {
86
88
  }
87
89
  }
88
90
 
89
- // Build journal entry
90
- const journalEntry = await this.buildJournalEntry(invoice, rules, options);
91
-
92
- // Create booking info
91
+ // Build journal input and post it atomically via the v2 path
92
+ const journalData = await this.buildJournalEntry(invoice, rules, options);
93
+ const journalEntry = await new JournalPoster().postDraft(
94
+ journalInputToDraft(journalData),
95
+ );
96
+ if (invoice.status !== 'posted') {
97
+ invoice.status = 'posted';
98
+ }
99
+
93
100
  const bookingInfo: IBookingInfo = {
94
101
  journalEntryId: journalEntry.id,
95
- transactionIds: journalEntry.transactionIds || [],
102
+ transactionIds: [],
96
103
  bookedAt: new Date(),
97
104
  bookedBy: 'system',
98
105
  bookingRules: {
@@ -105,23 +112,7 @@ export class InvoiceBookingEngine {
105
112
  confidence,
106
113
  autoBooked: options?.autoBook || false
107
114
  };
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
-
115
+
125
116
  return {
126
117
  success: true,
127
118
  journalEntry,
@@ -147,7 +138,7 @@ export class InvoiceBookingEngine {
147
138
  invoice: IInvoice,
148
139
  rules: IBookingRules,
149
140
  options?: IBookingOptions
150
- ): Promise<JournalEntry> {
141
+ ): Promise<IJournalEntry> {
151
142
  const lines: IJournalEntryLine[] = [];
152
143
  const isInbound = invoice.direction === 'inbound';
153
144
  const isCredit = invoice.invoiceTypeCode === '381'; // Credit note
@@ -163,17 +154,14 @@ export class InvoiceBookingEngine {
163
154
  lines.push(...this.buildAREntry(invoice, rules, reverseDirection));
164
155
  }
165
156
 
166
- // Create journal entry
167
- const journalData: IJournalEntry = {
157
+ // Journal input data — posted through the v2 atomic path by the caller
158
+ return {
168
159
  date: options?.bookingDate || invoice.issueDate,
169
160
  description: this.buildDescription(invoice),
170
161
  reference: options?.bookingReference || invoice.invoiceNumber,
171
162
  lines,
172
163
  skrType: this.skrType
173
164
  };
174
-
175
- const journalEntry = new JournalEntry(journalData);
176
- return journalEntry;
177
165
  }
178
166
 
179
167
  /**
@@ -555,7 +543,7 @@ export class InvoiceBookingEngine {
555
543
  }
556
544
  }
557
545
 
558
- // Create journal entry for payment
546
+ // Create journal entry for payment (v2 atomic path)
559
547
  const journalData: IJournalEntry = {
560
548
  date: payment.paymentDate,
561
549
  description: `Payment for invoice ${invoice.invoiceNumber}`,
@@ -563,11 +551,11 @@ export class InvoiceBookingEngine {
563
551
  lines,
564
552
  skrType: this.skrType
565
553
  };
566
-
567
- const journalEntry = new JournalEntry(journalData);
568
- await journalEntry.validate();
569
- await journalEntry.post();
570
-
554
+
555
+ const journalEntry = await new JournalPoster().postDraft(
556
+ journalInputToDraft(journalData),
557
+ );
558
+
571
559
  return {
572
560
  success: true,
573
561
  journalEntry,