@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,581 @@
1
+ import * as plugins from './plugins.js';
2
+ import type {
3
+ IInvoice,
4
+ IInvoiceLine,
5
+ IInvoiceParty,
6
+ IVATCategory,
7
+ IValidationResult,
8
+ TInvoiceFormat,
9
+ TInvoiceDirection,
10
+ TTaxScenario,
11
+ IAllowanceCharge,
12
+ IPaymentTerms
13
+ } from './skr.invoice.entity.js';
14
+
15
+ /**
16
+ * Adapter for @fin.cx/einvoice library
17
+ * Handles parsing, validation, and format conversion of e-invoices
18
+ */
19
+ export class InvoiceAdapter {
20
+ private logger: plugins.smartlog.ConsoleLog;
21
+
22
+ constructor() {
23
+ this.logger = new plugins.smartlog.ConsoleLog();
24
+ }
25
+
26
+ private readonly MAX_XML_SIZE = 10 * 1024 * 1024; // 10MB max
27
+ private readonly MAX_PDF_SIZE = 50 * 1024 * 1024; // 50MB max
28
+
29
+ /**
30
+ * Parse an invoice from file or buffer
31
+ */
32
+ public async parseInvoice(
33
+ file: Buffer | string,
34
+ direction: TInvoiceDirection
35
+ ): Promise<IInvoice> {
36
+ try {
37
+ // Validate input size
38
+ if (Buffer.isBuffer(file)) {
39
+ if (file.length > this.MAX_XML_SIZE) {
40
+ throw new Error(`Invoice file too large: ${file.length} bytes (max ${this.MAX_XML_SIZE} bytes)`);
41
+ }
42
+ } else if (typeof file === 'string' && file.length > this.MAX_XML_SIZE) {
43
+ throw new Error(`Invoice XML too large: ${file.length} characters (max ${this.MAX_XML_SIZE} characters)`);
44
+ }
45
+
46
+ // Parse the invoice using @fin.cx/einvoice
47
+ let einvoice;
48
+ if (typeof file === 'string') {
49
+ einvoice = await plugins.einvoice.EInvoice.fromXml(file);
50
+ } else {
51
+ // Convert buffer to string first
52
+ const xmlString = file.toString('utf-8');
53
+ einvoice = await plugins.einvoice.EInvoice.fromXml(xmlString);
54
+ }
55
+
56
+ // Get detected format
57
+ const format = this.mapEInvoiceFormat(einvoice.format || 'xrechnung');
58
+
59
+ // Validate the invoice (takes ~2.2ms)
60
+ const validationResult = await this.validateInvoice(einvoice);
61
+
62
+ // Extract invoice data
63
+ const invoiceData = einvoice.toObject();
64
+
65
+ // Map to internal invoice model
66
+ const invoice = await this.mapToInternalModel(
67
+ invoiceData,
68
+ format,
69
+ direction,
70
+ validationResult
71
+ );
72
+
73
+ // Store original XML content
74
+ invoice.xmlContent = einvoice.getXml();
75
+
76
+ // Calculate content hash
77
+ invoice.contentHash = await this.calculateContentHash(invoice.xmlContent);
78
+
79
+ // Classify tax scenario
80
+ invoice.taxScenario = this.classifyTaxScenario(invoice);
81
+
82
+ return invoice;
83
+ } catch (error) {
84
+ this.logger.log('error', `Failed to parse invoice: ${error}`);
85
+ throw new Error(`Invoice parsing failed: ${error.message}`);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Validate an invoice using multi-level validation
91
+ */
92
+ private async validateInvoice(einvoice: any): Promise<IValidationResult> {
93
+ // Perform multi-level validation
94
+ const validationResult = await einvoice.validate();
95
+
96
+ // Parse validation results into our structure
97
+ const syntaxResult = {
98
+ isValid: validationResult.syntax?.valid !== false,
99
+ errors: validationResult.syntax?.errors || [],
100
+ warnings: validationResult.syntax?.warnings || []
101
+ };
102
+
103
+ const semanticResult = {
104
+ isValid: validationResult.semantic?.valid !== false,
105
+ errors: validationResult.semantic?.errors || [],
106
+ warnings: validationResult.semantic?.warnings || []
107
+ };
108
+
109
+ const businessResult = {
110
+ isValid: validationResult.business?.valid !== false,
111
+ errors: validationResult.business?.errors || [],
112
+ warnings: validationResult.business?.warnings || []
113
+ };
114
+
115
+ const countryResult = {
116
+ isValid: validationResult.country?.valid !== false,
117
+ errors: validationResult.country?.errors || [],
118
+ warnings: validationResult.country?.warnings || []
119
+ };
120
+
121
+ return {
122
+ isValid: syntaxResult.isValid && semanticResult.isValid && businessResult.isValid,
123
+ syntax: {
124
+ valid: syntaxResult.isValid,
125
+ errors: syntaxResult.errors || [],
126
+ warnings: syntaxResult.warnings || []
127
+ },
128
+ semantic: {
129
+ valid: semanticResult.isValid,
130
+ errors: semanticResult.errors || [],
131
+ warnings: semanticResult.warnings || []
132
+ },
133
+ businessRules: {
134
+ valid: businessResult.isValid,
135
+ errors: businessResult.errors || [],
136
+ warnings: businessResult.warnings || []
137
+ },
138
+ countrySpecific: {
139
+ valid: countryResult.isValid,
140
+ errors: countryResult.errors || [],
141
+ warnings: countryResult.warnings || []
142
+ },
143
+ validatedAt: new Date(),
144
+ validatorVersion: '5.1.4'
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Map EN16931 Business Terms to internal invoice model
150
+ */
151
+ private async mapToInternalModel(
152
+ businessTerms: any,
153
+ format: TInvoiceFormat,
154
+ direction: TInvoiceDirection,
155
+ validationResult: IValidationResult
156
+ ): Promise<IInvoice> {
157
+ const invoice: IInvoice = {
158
+ // Identity
159
+ id: plugins.smartunique.shortId(),
160
+ direction,
161
+ format,
162
+
163
+ // EN16931 Business Terms
164
+ invoiceNumber: businessTerms.BT1_InvoiceNumber,
165
+ issueDate: new Date(businessTerms.BT2_IssueDate),
166
+ invoiceTypeCode: businessTerms.BT3_InvoiceTypeCode || '380',
167
+ currencyCode: businessTerms.BT5_CurrencyCode || 'EUR',
168
+ taxCurrencyCode: businessTerms.BT6_TaxCurrencyCode,
169
+ taxPointDate: businessTerms.BT7_TaxPointDate ? new Date(businessTerms.BT7_TaxPointDate) : undefined,
170
+ paymentDueDate: businessTerms.BT9_PaymentDueDate ? new Date(businessTerms.BT9_PaymentDueDate) : undefined,
171
+ buyerReference: businessTerms.BT10_BuyerReference,
172
+ projectReference: businessTerms.BT11_ProjectReference,
173
+ contractReference: businessTerms.BT12_ContractReference,
174
+ orderReference: businessTerms.BT13_OrderReference,
175
+ sellerOrderReference: businessTerms.BT14_SellerOrderReference,
176
+
177
+ // Parties
178
+ supplier: this.mapParty(businessTerms.BG4_Seller),
179
+ customer: this.mapParty(businessTerms.BG7_Buyer),
180
+ payee: businessTerms.BG10_Payee ? this.mapParty(businessTerms.BG10_Payee) : undefined,
181
+
182
+ // Line items
183
+ lines: this.mapInvoiceLines(businessTerms.BG25_InvoiceLines || []),
184
+
185
+ // Allowances and charges
186
+ allowances: this.mapAllowancesCharges(businessTerms.BG20_DocumentAllowances || [], true),
187
+ charges: this.mapAllowancesCharges(businessTerms.BG21_DocumentCharges || [], false),
188
+
189
+ // Amounts
190
+ lineNetAmount: parseFloat(businessTerms.BT106_SumOfLineNetAmounts || 0),
191
+ allowanceTotalAmount: parseFloat(businessTerms.BT107_AllowanceTotalAmount || 0),
192
+ chargeTotalAmount: parseFloat(businessTerms.BT108_ChargeTotalAmount || 0),
193
+ taxExclusiveAmount: parseFloat(businessTerms.BT109_TaxExclusiveAmount || 0),
194
+ taxInclusiveAmount: parseFloat(businessTerms.BT112_TaxInclusiveAmount || 0),
195
+ prepaidAmount: parseFloat(businessTerms.BT113_PrepaidAmount || 0),
196
+ payableAmount: parseFloat(businessTerms.BT115_PayableAmount || 0),
197
+
198
+ // VAT breakdown
199
+ vatBreakdown: this.mapVATBreakdown(businessTerms.BG23_VATBreakdown || []),
200
+ totalVATAmount: parseFloat(businessTerms.BT110_TotalVATAmount || 0),
201
+
202
+ // Payment
203
+ paymentTerms: this.mapPaymentTerms(businessTerms),
204
+ paymentMeans: this.mapPaymentMeans(businessTerms.BG16_PaymentInstructions),
205
+
206
+ // Notes
207
+ invoiceNote: businessTerms.BT22_InvoiceNote,
208
+
209
+ // Processing metadata
210
+ status: 'validated',
211
+
212
+ // Storage (to be filled later)
213
+ contentHash: '',
214
+
215
+ // Validation
216
+ validationResult,
217
+
218
+ // Audit trail
219
+ createdAt: new Date(),
220
+ createdBy: 'system',
221
+
222
+ // Metadata
223
+ metadata: {
224
+ importedAt: new Date(),
225
+ parserVersion: '5.1.4',
226
+ originalFormat: format
227
+ }
228
+ };
229
+
230
+ return invoice;
231
+ }
232
+
233
+ /**
234
+ * Map party information
235
+ */
236
+ private mapParty(partyData: any): IInvoiceParty {
237
+ if (!partyData) {
238
+ return {
239
+ id: '',
240
+ name: '',
241
+ address: { countryCode: 'DE' }
242
+ };
243
+ }
244
+
245
+ return {
246
+ id: partyData.BT29_SellerID || partyData.BT46_BuyerID || plugins.smartunique.shortId(),
247
+ name: partyData.BT27_SellerName || partyData.BT44_BuyerName || '',
248
+ address: {
249
+ street: partyData.BT35_SellerStreet || partyData.BT50_BuyerStreet,
250
+ city: partyData.BT37_SellerCity || partyData.BT52_BuyerCity,
251
+ postalCode: partyData.BT38_SellerPostalCode || partyData.BT53_BuyerPostalCode,
252
+ countryCode: partyData.BT40_SellerCountryCode || partyData.BT55_BuyerCountryCode || 'DE'
253
+ },
254
+ vatId: partyData.BT31_SellerVATID || partyData.BT48_BuyerVATID,
255
+ taxId: partyData.BT32_SellerTaxID || partyData.BT47_BuyerTaxID,
256
+ email: partyData.BT34_SellerEmail || partyData.BT49_BuyerEmail,
257
+ phone: partyData.BT33_SellerPhone,
258
+ bankAccount: this.mapBankAccount(partyData)
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Map bank account information
264
+ */
265
+ private mapBankAccount(partyData: any): IInvoiceParty['bankAccount'] | undefined {
266
+ if (!partyData?.BT84_PaymentAccountID) {
267
+ return undefined;
268
+ }
269
+
270
+ return {
271
+ iban: partyData.BT84_PaymentAccountID,
272
+ bic: partyData.BT86_PaymentServiceProviderID,
273
+ accountHolder: partyData.BT85_PaymentAccountName
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Map invoice lines
279
+ */
280
+ private mapInvoiceLines(linesData: any[]): IInvoiceLine[] {
281
+ return linesData.map((line, index) => ({
282
+ lineNumber: index + 1,
283
+ description: line.BT154_ItemDescription || '',
284
+ quantity: parseFloat(line.BT129_Quantity || 1),
285
+ unitPrice: parseFloat(line.BT146_NetPrice || 0),
286
+ netAmount: parseFloat(line.BT131_LineNetAmount || 0),
287
+ vatCategory: this.mapVATCategory(line.BT151_ItemVATCategory, line.BT152_ItemVATRate),
288
+ vatAmount: parseFloat(line.lineVATAmount || 0),
289
+ grossAmount: parseFloat(line.BT131_LineNetAmount || 0) + parseFloat(line.lineVATAmount || 0),
290
+ productCode: line.BT155_ItemSellerID,
291
+ allowances: this.mapLineAllowancesCharges(line.BG27_LineAllowances || [], true),
292
+ charges: this.mapLineAllowancesCharges(line.BG28_LineCharges || [], false)
293
+ }));
294
+ }
295
+
296
+ /**
297
+ * Map VAT category
298
+ */
299
+ private mapVATCategory(categoryCode: string, rate: string | number): IVATCategory {
300
+ const vatRate = typeof rate === 'string' ? parseFloat(rate) : rate;
301
+
302
+ return {
303
+ code: categoryCode || 'S',
304
+ rate: vatRate || 0,
305
+ exemptionReason: this.getExemptionReason(categoryCode)
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Get exemption reason for VAT category
311
+ */
312
+ private getExemptionReason(categoryCode: string): string | undefined {
313
+ const exemptionReasons: Record<string, string> = {
314
+ 'E': 'Tax exempt',
315
+ 'Z': 'Zero rated',
316
+ 'AE': 'Reverse charge (§13b UStG)',
317
+ 'K': 'Intra-EU supply',
318
+ 'G': 'Export outside EU',
319
+ 'O': 'Outside scope of tax',
320
+ 'S': undefined // Standard rate, no exemption
321
+ };
322
+
323
+ return exemptionReasons[categoryCode];
324
+ }
325
+
326
+ /**
327
+ * Map VAT breakdown
328
+ */
329
+ private mapVATBreakdown(vatBreakdown: any[]): IInvoice['vatBreakdown'] {
330
+ return vatBreakdown.map(vat => ({
331
+ vatCategory: this.mapVATCategory(vat.BT118_VATCategory, vat.BT119_VATRate),
332
+ taxableAmount: parseFloat(vat.BT116_TaxableAmount || 0),
333
+ taxAmount: parseFloat(vat.BT117_TaxAmount || 0)
334
+ }));
335
+ }
336
+
337
+ /**
338
+ * Map allowances and charges
339
+ */
340
+ private mapAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
341
+ return data.map(item => ({
342
+ reason: item.BT97_AllowanceReason || item.BT104_ChargeReason || '',
343
+ amount: parseFloat(item.BT92_AllowanceAmount || item.BT99_ChargeAmount || 0),
344
+ percentage: item.BT94_AllowancePercentage || item.BT101_ChargePercentage,
345
+ vatCategory: item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory
346
+ ? this.mapVATCategory(
347
+ item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory,
348
+ item.BT96_AllowanceVATRate || item.BT103_ChargeVATRate
349
+ )
350
+ : undefined,
351
+ vatAmount: parseFloat(item.allowanceVATAmount || item.chargeVATAmount || 0)
352
+ }));
353
+ }
354
+
355
+ /**
356
+ * Map line-level allowances and charges
357
+ */
358
+ private mapLineAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
359
+ return data.map(item => ({
360
+ reason: item.BT140_LineAllowanceReason || item.BT145_LineChargeReason || '',
361
+ amount: parseFloat(item.BT136_LineAllowanceAmount || item.BT141_LineChargeAmount || 0),
362
+ percentage: item.BT138_LineAllowancePercentage || item.BT143_LineChargePercentage
363
+ }));
364
+ }
365
+
366
+ /**
367
+ * Map payment terms
368
+ */
369
+ private mapPaymentTerms(businessTerms: any): IPaymentTerms | undefined {
370
+ if (!businessTerms.BT9_PaymentDueDate && !businessTerms.BT20_PaymentTerms) {
371
+ return undefined;
372
+ }
373
+
374
+ const paymentTerms: IPaymentTerms = {
375
+ dueDate: businessTerms.BT9_PaymentDueDate
376
+ ? new Date(businessTerms.BT9_PaymentDueDate)
377
+ : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Default 30 days
378
+ paymentTermsNote: businessTerms.BT20_PaymentTerms
379
+ };
380
+
381
+ // Parse skonto from payment terms note if present
382
+ if (businessTerms.BT20_PaymentTerms) {
383
+ paymentTerms.skonto = this.parseSkontoTerms(businessTerms.BT20_PaymentTerms);
384
+ }
385
+
386
+ return paymentTerms;
387
+ }
388
+
389
+ /**
390
+ * Parse skonto terms from payment terms text
391
+ */
392
+ private parseSkontoTerms(paymentTermsText: string): IPaymentTerms['skonto'] {
393
+ const skontoTerms: IPaymentTerms['skonto'] = [];
394
+
395
+ // Common German skonto patterns:
396
+ // "2% Skonto bei Zahlung innerhalb von 10 Tagen"
397
+ // "3% bei Zahlung bis 8 Tage, 2% bis 14 Tage"
398
+ const skontoPattern = /(\d+(?:\.\d+)?)\s*%.*?(\d+)\s*(?:Tag|Day)/gi;
399
+ let match;
400
+
401
+ while ((match = skontoPattern.exec(paymentTermsText)) !== null) {
402
+ skontoTerms.push({
403
+ percentage: parseFloat(match[1]),
404
+ days: parseInt(match[2]),
405
+ baseAmount: 0 // To be calculated based on invoice amount
406
+ });
407
+ }
408
+
409
+ return skontoTerms.length > 0 ? skontoTerms : undefined;
410
+ }
411
+
412
+ /**
413
+ * Map payment means
414
+ */
415
+ private mapPaymentMeans(paymentInstructions: any): IInvoice['paymentMeans'] | undefined {
416
+ if (!paymentInstructions) {
417
+ return undefined;
418
+ }
419
+
420
+ return {
421
+ code: paymentInstructions.BT81_PaymentMeansCode || '30', // 30 = Bank transfer
422
+ account: paymentInstructions.BT84_PaymentAccountID
423
+ ? {
424
+ iban: paymentInstructions.BT84_PaymentAccountID,
425
+ bic: paymentInstructions.BT86_PaymentServiceProviderID,
426
+ accountHolder: paymentInstructions.BT85_PaymentAccountName
427
+ }
428
+ : undefined
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Classify tax scenario based on invoice data
434
+ */
435
+ private classifyTaxScenario(invoice: IInvoice): TTaxScenario {
436
+ const supplierCountry = invoice.supplier.address.countryCode;
437
+ const customerCountry = invoice.customer.address.countryCode;
438
+ const hasVAT = invoice.totalVATAmount > 0;
439
+ const vatCategories = invoice.vatBreakdown.map(vb => vb.vatCategory.code);
440
+
441
+ // Reverse charge
442
+ if (vatCategories.includes('AE')) {
443
+ return 'reverse_charge';
444
+ }
445
+
446
+ // Small business exemption
447
+ if (vatCategories.includes('E') && invoice.invoiceNote?.includes('§19')) {
448
+ return 'small_business';
449
+ }
450
+
451
+ // Export outside EU
452
+ if (vatCategories.includes('G') || (!this.isEUCountry(customerCountry) && supplierCountry === 'DE')) {
453
+ return 'export';
454
+ }
455
+
456
+ // Intra-EU transactions
457
+ if (supplierCountry !== customerCountry && this.isEUCountry(supplierCountry) && this.isEUCountry(customerCountry)) {
458
+ if (invoice.direction === 'outbound') {
459
+ return 'intra_eu_supply';
460
+ } else {
461
+ return 'intra_eu_acquisition';
462
+ }
463
+ }
464
+
465
+ // Domestic exempt
466
+ if (!hasVAT && supplierCountry === 'DE' && customerCountry === 'DE') {
467
+ return 'domestic_exempt';
468
+ }
469
+
470
+ // Default: Domestic taxed
471
+ return 'domestic_taxed';
472
+ }
473
+
474
+ /**
475
+ * Check if country is in EU
476
+ */
477
+ private isEUCountry(countryCode: string): boolean {
478
+ const euCountries = [
479
+ 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
480
+ 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
481
+ 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
482
+ ];
483
+ return euCountries.includes(countryCode);
484
+ }
485
+
486
+ /**
487
+ * Map e-invoice format from library format
488
+ */
489
+ private mapEInvoiceFormat(format: string): TInvoiceFormat {
490
+ const formatMap: Record<string, TInvoiceFormat> = {
491
+ 'xrechnung': 'xrechnung',
492
+ 'zugferd': 'zugferd',
493
+ 'factur-x': 'facturx',
494
+ 'facturx': 'facturx',
495
+ 'peppol': 'peppol',
496
+ 'ubl': 'ubl'
497
+ };
498
+
499
+ return formatMap[format.toLowerCase()] || 'xrechnung';
500
+ }
501
+
502
+ /**
503
+ * Calculate content hash for the invoice
504
+ */
505
+ private async calculateContentHash(xmlContent: string): Promise<string> {
506
+ const hash = await plugins.smarthash.sha256FromString(xmlContent);
507
+ return hash;
508
+ }
509
+
510
+ /**
511
+ * Convert invoice to different format
512
+ */
513
+ public async convertFormat(
514
+ invoice: IInvoice,
515
+ targetFormat: TInvoiceFormat
516
+ ): Promise<string> {
517
+ try {
518
+ // Load from existing XML
519
+ const einvoice = await plugins.einvoice.EInvoice.fromXml(invoice.xmlContent!);
520
+
521
+ // Convert to target format (takes ~0.6ms)
522
+ const convertedXml = await einvoice.exportXml(targetFormat as any);
523
+
524
+ return convertedXml;
525
+ } catch (error) {
526
+ this.logger.log('error', `Failed to convert invoice format: ${error}`);
527
+ throw new Error(`Format conversion failed: ${error.message}`);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Generate invoice from internal data
533
+ */
534
+ public async generateInvoice(
535
+ invoiceData: Partial<IInvoice>,
536
+ format: TInvoiceFormat
537
+ ): Promise<{ xml: string; pdf?: Buffer }> {
538
+ try {
539
+ // Create a new invoice instance
540
+ const einvoice = new plugins.einvoice.EInvoice();
541
+
542
+ // Set invoice data
543
+ const businessTerms = this.mapToBusinessTerms(invoiceData);
544
+ Object.assign(einvoice, businessTerms);
545
+
546
+ // Generate XML in requested format
547
+ const xml = await einvoice.exportXml(format as any);
548
+
549
+ // Generate PDF if ZUGFeRD or Factur-X
550
+ let pdf: Buffer | undefined;
551
+ if (format === 'zugferd' || format === 'facturx') {
552
+ // Access the pdf property if it exists
553
+ if (einvoice.pdf && einvoice.pdf.buffer) {
554
+ pdf = Buffer.from(einvoice.pdf.buffer);
555
+ }
556
+ }
557
+
558
+ return { xml, pdf };
559
+ } catch (error) {
560
+ this.logger.log('error', `Failed to generate invoice: ${error}`);
561
+ throw new Error(`Invoice generation failed: ${error.message}`);
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Map internal invoice to EN16931 Business Terms
567
+ */
568
+ private mapToBusinessTerms(invoice: Partial<IInvoice>): any {
569
+ return {
570
+ BT1_InvoiceNumber: invoice.invoiceNumber,
571
+ BT2_IssueDate: invoice.issueDate?.toISOString(),
572
+ BT3_InvoiceTypeCode: invoice.invoiceTypeCode || '380',
573
+ BT5_CurrencyCode: invoice.currencyCode || 'EUR',
574
+ BT7_TaxPointDate: invoice.taxPointDate?.toISOString(),
575
+ BT9_PaymentDueDate: invoice.paymentDueDate?.toISOString(),
576
+
577
+ // Map other Business Terms...
578
+ // This would be a comprehensive mapping in production
579
+ };
580
+ }
581
+ }