@fin.cx/einvoice 5.1.4 → 5.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 (118) hide show
  1. package/.smartconfig.json +59 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/einvoice.d.ts +14 -5
  4. package/dist_ts/einvoice.js +173 -66
  5. package/dist_ts/errors.js +14 -4
  6. package/dist_ts/formats/base/base.decoder.d.ts +4 -0
  7. package/dist_ts/formats/base/base.decoder.js +9 -1
  8. package/dist_ts/formats/base/base.validator.js +3 -2
  9. package/dist_ts/formats/cii/cii.decoder.d.ts +1 -0
  10. package/dist_ts/formats/cii/cii.decoder.js +20 -10
  11. package/dist_ts/formats/cii/cii.encoder.js +2 -5
  12. package/dist_ts/formats/cii/cii.validator.d.ts +1 -1
  13. package/dist_ts/formats/cii/cii.validator.js +19 -12
  14. package/dist_ts/formats/cii/facturx/facturx.encoder.js +25 -16
  15. package/dist_ts/formats/converters/xml-to-einvoice.converter.js +3 -3
  16. package/dist_ts/formats/factories/decoder.factory.d.ts +3 -1
  17. package/dist_ts/formats/factories/decoder.factory.js +4 -3
  18. package/dist_ts/formats/factories/validator.factory.d.ts +4 -1
  19. package/dist_ts/formats/factories/validator.factory.js +11 -8
  20. package/dist_ts/formats/pdf/extractors/associated.extractor.js +3 -2
  21. package/dist_ts/formats/pdf/extractors/base.extractor.d.ts +1 -1
  22. package/dist_ts/formats/pdf/extractors/base.extractor.js +50 -52
  23. package/dist_ts/formats/pdf/extractors/standard.extractor.js +3 -2
  24. package/dist_ts/formats/pdf/extractors/text.extractor.js +19 -22
  25. package/dist_ts/formats/pdf/pdf.embedder.js +3 -2
  26. package/dist_ts/formats/pdf/pdf.extractor.d.ts +2 -1
  27. package/dist_ts/formats/pdf/pdf.extractor.js +20 -10
  28. package/dist_ts/formats/semantic/semantic.adapter.js +42 -12
  29. package/dist_ts/formats/semantic/semantic.validator.js +3 -1
  30. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -3
  31. package/dist_ts/formats/ubl/generic/ubl.encoder.js +11 -7
  32. package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -0
  33. package/dist_ts/formats/ubl/ubl.decoder.js +18 -8
  34. package/dist_ts/formats/ubl/ubl.validator.d.ts +1 -1
  35. package/dist_ts/formats/ubl/ubl.validator.js +17 -10
  36. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +37 -29
  37. package/dist_ts/formats/utils/currency.calculator.decimal.js +4 -1
  38. package/dist_ts/formats/utils/currency.utils.js +4 -1
  39. package/dist_ts/formats/utils/decimal.js +9 -2
  40. package/dist_ts/formats/utils/format.detector.js +47 -17
  41. package/dist_ts/formats/validation/codelist.validator.js +2 -4
  42. package/dist_ts/formats/validation/conformance.harness.js +13 -6
  43. package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -4
  44. package/dist_ts/formats/validation/en16931.validator.d.ts +6 -0
  45. package/dist_ts/formats/validation/en16931.validator.js +18 -12
  46. package/dist_ts/formats/validation/facturx.validator.js +147 -148
  47. package/dist_ts/formats/validation/integrated.validator.js +12 -4
  48. package/dist_ts/formats/validation/peppol.validator.js +2 -1
  49. package/dist_ts/formats/validation/schematron.downloader.d.ts +0 -1
  50. package/dist_ts/formats/validation/schematron.downloader.js +12 -7
  51. package/dist_ts/formats/validation/schematron.integration.js +16 -6
  52. package/dist_ts/formats/validation/schematron.validator.d.ts +9 -11
  53. package/dist_ts/formats/validation/schematron.validator.js +70 -72
  54. package/dist_ts/formats/validation/schematron.worker.d.ts +7 -6
  55. package/dist_ts/formats/validation/schematron.worker.js +48 -42
  56. package/dist_ts/formats/validation/vat-categories.validator.js +3 -4
  57. package/dist_ts/formats/validation/xrechnung.validator.js +7 -7
  58. package/dist_ts/index.js +3 -2
  59. package/dist_ts/interfaces/common.js +1 -2
  60. package/dist_ts/plugins.d.ts +3 -6
  61. package/dist_ts/plugins.js +3 -12
  62. package/dist_ts/vendor/pako.d.ts +5 -0
  63. package/dist_ts/vendor/pako.js +3 -0
  64. package/dist_ts/vendor/pdflib.d.ts +2 -0
  65. package/dist_ts/vendor/pdflib.js +9 -0
  66. package/dist_ts/vendor/saxonjs.d.ts +23 -0
  67. package/dist_ts/vendor/saxonjs.js +9 -0
  68. package/dist_ts/vendor/xmldom.d.ts +20 -0
  69. package/dist_ts/vendor/xmldom.js +5 -0
  70. package/dist_ts_install/download-schematron.d.ts +0 -3
  71. package/dist_ts_install/download-schematron.js +20 -11
  72. package/dist_ts_install/download-test-samples.d.ts +0 -3
  73. package/dist_ts_install/download-test-samples.js +4 -2
  74. package/dist_ts_install/download-xrechnung-rules.js +2 -1
  75. package/dist_ts_install/index.d.ts +0 -5
  76. package/dist_ts_install/index.js +26 -12
  77. package/license +21 -0
  78. package/package.json +21 -15
  79. package/readme.hints.md +24 -8
  80. package/readme.md +195 -328
  81. package/ts/00_commitinfo_data.ts +1 -1
  82. package/ts/einvoice.ts +156 -33
  83. package/ts/formats/base/base.decoder.ts +7 -0
  84. package/ts/formats/cii/cii.decoder.ts +17 -8
  85. package/ts/formats/cii/cii.validator.ts +18 -13
  86. package/ts/formats/cii/facturx/facturx.encoder.ts +25 -15
  87. package/ts/formats/converters/xml-to-einvoice.converter.ts +4 -5
  88. package/ts/formats/factories/decoder.factory.ts +7 -2
  89. package/ts/formats/factories/validator.factory.ts +14 -8
  90. package/ts/formats/pdf/extractors/associated.extractor.ts +2 -1
  91. package/ts/formats/pdf/extractors/base.extractor.ts +3 -2
  92. package/ts/formats/pdf/extractors/standard.extractor.ts +2 -1
  93. package/ts/formats/pdf/pdf.embedder.ts +6 -3
  94. package/ts/formats/pdf/pdf.extractor.ts +24 -16
  95. package/ts/formats/semantic/semantic.adapter.ts +49 -15
  96. package/ts/formats/ubl/en16931.ubl.validator.ts +5 -3
  97. package/ts/formats/ubl/generic/ubl.encoder.ts +12 -7
  98. package/ts/formats/ubl/ubl.decoder.ts +18 -9
  99. package/ts/formats/ubl/ubl.validator.ts +17 -12
  100. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +41 -28
  101. package/ts/formats/utils/format.detector.ts +57 -19
  102. package/ts/formats/validation/conformance.harness.ts +6 -4
  103. package/ts/formats/validation/en16931.validator.ts +20 -10
  104. package/ts/formats/validation/integrated.validator.ts +5 -3
  105. package/ts/formats/validation/schematron.downloader.ts +11 -8
  106. package/ts/formats/validation/schematron.integration.ts +11 -5
  107. package/ts/formats/validation/schematron.validator.ts +79 -79
  108. package/ts/formats/validation/schematron.worker.ts +59 -41
  109. package/ts/index.ts +2 -1
  110. package/ts/interfaces/common.ts +1 -3
  111. package/ts/plugins.ts +2 -29
  112. package/ts/readme.md +76 -0
  113. package/ts/vendor/modules.d.ts +32 -0
  114. package/ts/vendor/pako.ts +8 -0
  115. package/ts/vendor/pdflib.ts +12 -0
  116. package/ts/vendor/saxonjs.ts +36 -0
  117. package/ts/vendor/xmldom.ts +27 -0
  118. package/npmextra.json +0 -35
@@ -1,5 +1,5 @@
1
- import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } from '../../../plugins.js';
2
1
  import { BaseXMLExtractor } from './base.extractor.js';
2
+ import { loadPdfLib } from '../../../vendor/pdflib.js';
3
3
 
4
4
  /**
5
5
  * Associated files extractor for PDF/A-3 documents
@@ -14,6 +14,7 @@ export class AssociatedFilesExtractor extends BaseXMLExtractor {
14
14
  */
15
15
  public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
16
16
  try {
17
+ const { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } = await loadPdfLib();
17
18
  const pdfDoc = await PDFDocument.load(pdfBuffer);
18
19
 
19
20
  // Try to find associated files via the AF entry in the catalog
@@ -1,4 +1,5 @@
1
- import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString, pako } from '../../../plugins.js';
1
+ import type { PDFRawStream } from 'pdf-lib';
2
+ import { pako } from '../../../plugins.js';
2
3
 
3
4
  /**
4
5
  * Base class for PDF XML extractors with common functionality
@@ -352,4 +353,4 @@ export abstract class BaseXMLExtractor {
352
353
  (content.includes('<?xml') ||
353
354
  this.knownFormats.some(format => content.includes(format)));
354
355
  }
355
- }
356
+ }
@@ -1,5 +1,5 @@
1
- import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } from '../../../plugins.js';
2
1
  import { BaseXMLExtractor } from './base.extractor.js';
2
+ import { loadPdfLib } from '../../../vendor/pdflib.js';
3
3
 
4
4
  /**
5
5
  * Standard PDF XML extractor that extracts XML from embedded files
@@ -13,6 +13,7 @@ export class StandardXMLExtractor extends BaseXMLExtractor {
13
13
  */
14
14
  public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
15
15
  try {
16
+ const { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } = await loadPdfLib();
16
17
  const pdfDoc = await PDFDocument.load(pdfBuffer);
17
18
 
18
19
  // Get the document's metadata dictionary
@@ -1,5 +1,6 @@
1
- import { PDFDocument, AFRelationship } from '../../plugins.js';
1
+ import type { PDFDocument as TPdfDocument } from 'pdf-lib';
2
2
  import type { IPdf } from '../../interfaces/common.js';
3
+ import { loadPdfLib } from '../../vendor/pdflib.js';
3
4
 
4
5
  /**
5
6
  * Error types for PDF embedding operations
@@ -57,8 +58,10 @@ export class PDFEmbedder {
57
58
  // Ensure buffer is Uint8Array
58
59
  const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
59
60
 
61
+ const { PDFDocument, AFRelationship } = await loadPdfLib();
62
+
60
63
  // Load the PDF
61
- let pdfDoc: PDFDocument;
64
+ let pdfDoc: TPdfDocument;
62
65
  try {
63
66
  pdfDoc = await PDFDocument.load(pdfBufferArray, {
64
67
  ignoreEncryption: true, // Try to load encrypted PDFs
@@ -239,4 +242,4 @@ export class PDFEmbedder {
239
242
  }
240
243
  };
241
244
  }
242
- }
245
+ }
@@ -1,9 +1,4 @@
1
- import {
2
- BaseXMLExtractor,
3
- StandardXMLExtractor,
4
- AssociatedFilesExtractor,
5
- TextXMLExtractor
6
- } from './extractors/index.js';
1
+ import type { BaseXMLExtractor } from './extractors/base.extractor.js';
7
2
  import { FormatDetector } from '../utils/format.detector.js';
8
3
  import { InvoiceFormat } from '../../interfaces/common.js';
9
4
 
@@ -36,18 +31,30 @@ export interface PDFExtractResult {
36
31
  * Uses multiple specialized extractors in sequence to maximize success rate
37
32
  */
38
33
  export class PDFExtractor {
39
- private extractors: BaseXMLExtractor[] = [];
34
+ private extractors?: BaseXMLExtractor[];
40
35
 
41
36
  /**
42
37
  * Constructor initializes the chain of extractors
43
38
  */
44
- constructor() {
45
- // Add extractors in order of preference/likelihood of success
46
- this.extractors.push(
47
- new StandardXMLExtractor(), // Standard PDF/A-3 embedded files
48
- new AssociatedFilesExtractor(), // Associated files (ZUGFeRD v1, some Factur-X)
49
- new TextXMLExtractor() // Text-based extraction (fallback)
50
- );
39
+ constructor() {}
40
+
41
+ private async getExtractors(): Promise<BaseXMLExtractor[]> {
42
+ if (!this.extractors) {
43
+ const [standardModule, associatedModule, textModule] = await Promise.all([
44
+ import('./extractors/standard.extractor.js'),
45
+ import('./extractors/associated.extractor.js'),
46
+ import('./extractors/text.extractor.js')
47
+ ]);
48
+
49
+ // Add extractors in order of preference/likelihood of success
50
+ this.extractors = [
51
+ new standardModule.StandardXMLExtractor(), // Standard PDF/A-3 embedded files
52
+ new associatedModule.AssociatedFilesExtractor(), // Associated files (ZUGFeRD v1, some Factur-X)
53
+ new textModule.TextXMLExtractor() // Text-based extraction (fallback)
54
+ ];
55
+ }
56
+
57
+ return this.extractors;
51
58
  }
52
59
 
53
60
  /**
@@ -69,7 +76,8 @@ export class PDFExtractor {
69
76
  const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
70
77
 
71
78
  // Try each extractor in sequence
72
- for (const extractor of this.extractors) {
79
+ const extractors = await this.getExtractors();
80
+ for (const extractor of extractors) {
73
81
  const extractorName = extractor.constructor.name;
74
82
  console.log(`Trying extraction with ${extractorName}...`);
75
83
 
@@ -138,4 +146,4 @@ export class PDFExtractor {
138
146
  }
139
147
  };
140
148
  }
141
- }
149
+ }
@@ -169,17 +169,45 @@ export class SemanticModelAdapter {
169
169
  }
170
170
 
171
171
  // Set payment options
172
- if (model.paymentInstructions.paymentAccountIdentifier) {
172
+ if (
173
+ model.paymentInstructions.paymentAccountIdentifier ||
174
+ model.paymentInstructions.paymentMeansText ||
175
+ model.paymentInstructions.paymentServiceProviderIdentifier
176
+ ) {
173
177
  invoice.paymentOptions = {
174
- sepa: {
178
+ description: model.paymentInstructions.paymentMeansText,
179
+ sepaConnection: {
180
+ iban: model.paymentInstructions.paymentAccountIdentifier || '',
181
+ bic: model.paymentInstructions.paymentServiceProviderIdentifier || '',
182
+ institution: model.paymentInstructions.paymentServiceProviderIdentifier
183
+ },
184
+ payPal: { email: '' }
185
+ };
186
+
187
+ invoice.metadata = {
188
+ ...invoice.metadata,
189
+ paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
190
+ paymentAccount: {
175
191
  iban: model.paymentInstructions.paymentAccountIdentifier,
176
- bic: model.paymentInstructions.paymentServiceProviderIdentifier
192
+ accountName: model.paymentInstructions.paymentAccountName,
193
+ bankId: model.paymentInstructions.paymentServiceProviderIdentifier
177
194
  },
178
- bankInfo: {
179
- accountHolder: model.paymentInstructions.paymentAccountName || '',
180
- institutionName: model.paymentInstructions.paymentServiceProviderIdentifier || ''
195
+ extensions: {
196
+ ...invoice.metadata?.extensions,
197
+ paymentMeans: {
198
+ paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
199
+ paymentMeansText: model.paymentInstructions.paymentMeansText,
200
+ remittanceInformation: model.paymentInstructions.remittanceInformation
201
+ },
202
+ paymentAccount: {
203
+ iban: model.paymentInstructions.paymentAccountIdentifier,
204
+ accountName: model.paymentInstructions.paymentAccountName,
205
+ bankId: model.paymentInstructions.paymentServiceProviderIdentifier,
206
+ bic: model.paymentInstructions.paymentServiceProviderIdentifier,
207
+ institutionName: model.paymentInstructions.paymentServiceProviderIdentifier
208
+ }
181
209
  }
182
- } as any;
210
+ };
183
211
  }
184
212
 
185
213
  // Set extensions
@@ -376,15 +404,21 @@ export class SemanticModelAdapter {
376
404
  */
377
405
  private mapPaymentInstructions(invoice: EInvoice): PaymentInstructions {
378
406
  const paymentMeans = invoice.metadata?.extensions?.paymentMeans;
379
- const paymentAccount = invoice.metadata?.extensions?.paymentAccount;
407
+ const paymentAccount = invoice.metadata?.extensions?.paymentAccount || invoice.metadata?.paymentAccount;
408
+ const sepaConnection = invoice.paymentOptions?.sepaConnection;
380
409
 
381
410
  return {
382
- paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30', // Default to credit transfer
383
- paymentMeansText: paymentMeans?.paymentMeansText,
411
+ paymentMeansTypeCode: paymentMeans?.paymentMeansCode || invoice.metadata?.paymentMeansCode || '30',
412
+ paymentMeansText: paymentMeans?.paymentMeansText || invoice.paymentOptions?.description,
384
413
  remittanceInformation: paymentMeans?.remittanceInformation,
385
- paymentAccountIdentifier: paymentAccount?.iban,
414
+ paymentAccountIdentifier: paymentAccount?.iban || sepaConnection?.iban,
386
415
  paymentAccountName: paymentAccount?.accountName,
387
- paymentServiceProviderIdentifier: paymentAccount?.bic || paymentAccount?.institutionName
416
+ paymentServiceProviderIdentifier:
417
+ paymentAccount?.bic ||
418
+ paymentAccount?.institutionName ||
419
+ paymentAccount?.bankId ||
420
+ sepaConnection?.bic ||
421
+ sepaConnection?.institution
388
422
  };
389
423
  }
390
424
 
@@ -397,10 +431,10 @@ export class SemanticModelAdapter {
397
431
  taxExclusiveAmount: invoice.totalNet,
398
432
  taxInclusiveAmount: invoice.totalGross,
399
433
  allowanceTotalAmount: invoice.metadata?.extensions?.documentAllowances?.reduce(
400
- (sum, a) => sum + a.amount, 0
434
+ (sum: number, a: { amount: number }) => sum + a.amount, 0
401
435
  ),
402
436
  chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
403
- (sum, c) => sum + c.amount, 0
437
+ (sum: number, c: { amount: number }) => sum + c.amount, 0
404
438
  ),
405
439
  prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
406
440
  roundingAmount: invoice.metadata?.extensions?.roundingAmount,
@@ -597,4 +631,4 @@ export class SemanticModelAdapter {
597
631
 
598
632
  return errors;
599
633
  }
600
- }
634
+ }
@@ -90,7 +90,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
90
90
  }
91
91
 
92
92
  // BR-08: An Invoice shall contain the Seller postal address (BG-5).
93
- const sellerAddress = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc)[0];
93
+ const sellerAddressResult = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc);
94
+ const sellerAddress = Array.isArray(sellerAddressResult) ? sellerAddressResult[0] : null;
94
95
  if (!sellerAddress || !this.exists('.//cbc:IdentificationCode', sellerAddress)) {
95
96
  this.addError('BR-08', 'An Invoice shall contain the Seller postal address', '//cac:AccountingSupplierParty//cac:PostalAddress');
96
97
  valid = false;
@@ -103,7 +104,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
103
104
  }
104
105
 
105
106
  // BR-10: An Invoice shall contain the Buyer postal address (BG-8).
106
- const buyerAddress = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc)[0];
107
+ const buyerAddressResult = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc);
108
+ const buyerAddress = Array.isArray(buyerAddressResult) ? buyerAddressResult[0] : null;
107
109
  if (!buyerAddress || !this.exists('.//cbc:IdentificationCode', buyerAddress)) {
108
110
  this.addError('BR-10', 'An Invoice shall contain the Buyer postal address', '//cac:AccountingCustomerParty//cac:PostalAddress');
109
111
  valid = false;
@@ -213,4 +215,4 @@ export class EN16931UBLValidator extends UBLBaseValidator {
213
215
 
214
216
  return valid;
215
217
  }
216
- }
218
+ }
@@ -263,6 +263,11 @@ export class UBLEncoder extends UBLBaseEncoder {
263
263
  * @param invoice Invoice data
264
264
  */
265
265
  private addPaymentMeans(doc: Document, parentElement: Element, invoice: TInvoice): void {
266
+ const paymentOptions = invoice.paymentOptions;
267
+ if (!paymentOptions) {
268
+ return;
269
+ }
270
+
266
271
  const paymentMeansNode = doc.createElement('cac:PaymentMeans');
267
272
  parentElement.appendChild(paymentMeansNode);
268
273
 
@@ -276,26 +281,26 @@ export class UBLEncoder extends UBLBaseEncoder {
276
281
  this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
277
282
 
278
283
  // Add payment channel code if available
279
- if (invoice.paymentOptions.description) {
280
- this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote', invoice.paymentOptions.description);
284
+ if (paymentOptions.description) {
285
+ this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote', paymentOptions.description);
281
286
  }
282
287
 
283
288
  // Add payment ID information if available - use invoice ID as payment reference
284
289
  this.appendElement(doc, paymentMeansNode, 'cbc:PaymentID', invoice.id);
285
290
 
286
291
  // Add bank account information if available
287
- if (invoice.paymentOptions.sepaConnection && invoice.paymentOptions.sepaConnection.iban) {
292
+ if (paymentOptions.sepaConnection && paymentOptions.sepaConnection.iban) {
288
293
  const payeeFinancialAccountNode = doc.createElement('cac:PayeeFinancialAccount');
289
294
  paymentMeansNode.appendChild(payeeFinancialAccountNode);
290
295
 
291
- this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.iban);
296
+ this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', paymentOptions.sepaConnection.iban);
292
297
 
293
298
  // Add financial institution information if BIC is available
294
- if (invoice.paymentOptions.sepaConnection.bic) {
299
+ if (paymentOptions.sepaConnection.bic) {
295
300
  const financialInstitutionNode = doc.createElement('cac:FinancialInstitutionBranch');
296
301
  payeeFinancialAccountNode.appendChild(financialInstitutionNode);
297
302
 
298
- this.appendElement(doc, financialInstitutionNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.bic);
303
+ this.appendElement(doc, financialInstitutionNode, 'cbc:ID', paymentOptions.sepaConnection.bic);
299
304
  }
300
305
  }
301
306
  }
@@ -1038,4 +1043,4 @@ export class UBLEncoder extends UBLBaseEncoder {
1038
1043
  }
1039
1044
  }
1040
1045
  }
1041
- }
1046
+ }
@@ -3,6 +3,13 @@ import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
  import { DOMParser, xpath } from '../../plugins.js';
5
5
 
6
+ const ublParser = new DOMParser();
7
+ const ublNamespaces = {
8
+ cbc: UBL_NAMESPACES.CBC,
9
+ cac: UBL_NAMESPACES.CAC,
10
+ };
11
+ const ublSelect = xpath.useNamespaces(ublNamespaces);
12
+
6
13
  /**
7
14
  * Base decoder for UBL-based invoice formats
8
15
  */
@@ -15,16 +22,17 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
15
22
  super(xml, skipValidation);
16
23
 
17
24
  // Parse XML document
18
- this.doc = new DOMParser().parseFromString(xml, 'application/xml');
25
+ this.doc = ublParser.parseFromString(xml, 'application/xml');
26
+
27
+ // Set up namespaces for XPath queries
28
+ this.namespaces = ublNamespaces;
19
29
 
20
- // Set up namespaces for XPath queries
21
- this.namespaces = {
22
- cbc: UBL_NAMESPACES.CBC,
23
- cac: UBL_NAMESPACES.CAC
24
- };
30
+ // Create XPath selector with namespaces
31
+ this.select = ublSelect;
32
+ }
25
33
 
26
- // Create XPath selector with namespaces
27
- this.select = xpath.useNamespaces(this.namespaces);
34
+ public override getParsedDocument(): Document {
35
+ return this.doc;
28
36
  }
29
37
 
30
38
  /**
@@ -75,7 +83,8 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
75
83
  * @returns Text value or empty string if not found
76
84
  */
77
85
  protected getText(xpathExpr: string, context?: Node): string {
78
- const node = this.select(xpathExpr, context || this.doc)[0];
86
+ const result = this.select(xpathExpr, context || this.doc);
87
+ const node = Array.isArray(result) ? result[0] : null;
79
88
  return node ? (node.textContent || '') : '';
80
89
  }
81
90
 
@@ -4,29 +4,33 @@ import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { UBLDocumentType } from './ubl.types.js';
5
5
  import { DOMParser, xpath } from '../../plugins.js';
6
6
 
7
+ const ublValidatorParser = new DOMParser();
8
+ const ublValidatorNamespaces = {
9
+ cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
10
+ cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
11
+ };
12
+ const ublValidatorSelect = xpath.useNamespaces(ublValidatorNamespaces);
13
+
7
14
  /**
8
15
  * Base validator for UBL-based invoice formats
9
16
  */
10
17
  export abstract class UBLBaseValidator extends BaseValidator {
11
- protected doc: Document;
12
- protected namespaces: Record<string, string>;
13
- protected select: xpath.XPathSelect;
18
+ protected doc!: Document;
19
+ protected namespaces!: Record<string, string>;
20
+ protected select!: xpath.XPathSelect;
14
21
 
15
- constructor(xml: string) {
22
+ constructor(xml: string, doc?: Document) {
16
23
  super(xml);
17
24
 
18
25
  try {
19
- // Parse XML document
20
- this.doc = new DOMParser().parseFromString(xml, 'application/xml');
26
+ // Reuse an existing parsed document when available.
27
+ this.doc = doc ?? ublValidatorParser.parseFromString(xml, 'application/xml');
21
28
 
22
29
  // Set up namespaces for XPath queries
23
- this.namespaces = {
24
- cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
25
- cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'
26
- };
30
+ this.namespaces = ublValidatorNamespaces;
27
31
 
28
32
  // Create XPath selector with namespaces
29
- this.select = xpath.useNamespaces(this.namespaces);
33
+ this.select = ublValidatorSelect;
30
34
  } catch (error) {
31
35
  this.addError('UBL-PARSE', `Failed to parse XML: ${error}`, '/');
32
36
  }
@@ -101,7 +105,8 @@ export abstract class UBLBaseValidator extends BaseValidator {
101
105
  * @returns Text value or empty string if not found
102
106
  */
103
107
  protected getText(xpathExpr: string, context?: Node): string {
104
- const node = this.select(xpathExpr, context || this.doc)[0];
108
+ const result = this.select(xpathExpr, context || this.doc);
109
+ const node = Array.isArray(result) ? result[0] : null;
105
110
  return node ? (node.textContent || '') : '';
106
111
  }
107
112
 
@@ -194,7 +194,36 @@ export class XRechnungDecoder extends UBLBaseDecoder {
194
194
  const seller = this.extractParty('//cac:AccountingSupplierParty/cac:Party');
195
195
  const buyer = this.extractParty('//cac:AccountingCustomerParty/cac:Party');
196
196
 
197
- // Create the common invoice data with metadata for business references
197
+ const businessReferences = {
198
+ buyerReference,
199
+ orderReference,
200
+ contractReference,
201
+ projectReference
202
+ };
203
+
204
+ const paymentInformation = {
205
+ paymentMeansCode,
206
+ paymentID,
207
+ paymentDueDate,
208
+ iban,
209
+ bic,
210
+ bankName,
211
+ accountName,
212
+ paymentTermsNote,
213
+ discountPercent
214
+ };
215
+
216
+ const dateInformation = {
217
+ periodStart,
218
+ periodEnd,
219
+ deliveryDate
220
+ };
221
+
222
+ const hasBusinessReferences = Object.values(businessReferences).some(value => Boolean(value));
223
+ const hasPaymentInformation = Object.values(paymentInformation).some(value => Boolean(value));
224
+ const hasDateInformation = Object.values(dateInformation).some(value => Boolean(value));
225
+
226
+ // Create the common invoice data with metadata only when the source XML actually contains it.
198
227
  const invoiceData: any = {
199
228
  type: 'accounting-doc' as const,
200
229
  accountingDocType: 'invoice' as const,
@@ -216,36 +245,20 @@ export class XRechnungDecoder extends UBLBaseDecoder {
216
245
  reverseCharge: false,
217
246
  currency: currencyCode as finance.TCurrency,
218
247
  notes: notes,
219
- objectActions: [],
220
- metadata: {
248
+ objectActions: []
249
+ };
250
+
251
+ if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
252
+ invoiceData.metadata = {
221
253
  format: 'xrechnung' as any,
222
254
  version: '1.0.0',
223
255
  extensions: {
224
- businessReferences: {
225
- buyerReference,
226
- orderReference,
227
- contractReference,
228
- projectReference
229
- },
230
- paymentInformation: {
231
- paymentMeansCode,
232
- paymentID,
233
- paymentDueDate,
234
- iban,
235
- bic,
236
- bankName,
237
- accountName,
238
- paymentTermsNote,
239
- discountPercent
240
- },
241
- dateInformation: {
242
- periodStart,
243
- periodEnd,
244
- deliveryDate
245
- }
256
+ ...(hasBusinessReferences ? { businessReferences } : {}),
257
+ ...(hasPaymentInformation ? { paymentInformation } : {}),
258
+ ...(hasDateInformation ? { dateInformation } : {}),
246
259
  }
247
- }
248
- };
260
+ };
261
+ }
249
262
 
250
263
  // Validate mandatory EN16931 fields unless validation is skipped
251
264
  if (!this.skipValidation) {
@@ -255,7 +268,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
255
268
  return invoiceData;
256
269
  } catch (error) {
257
270
  // Re-throw validation errors
258
- if (error.message && error.message.includes('EN16931 validation failed')) {
271
+ if (error instanceof Error && error.message.includes('EN16931 validation failed')) {
259
272
  throw error;
260
273
  }
261
274
 
@@ -70,41 +70,81 @@ export class FormatDetector {
70
70
  * @returns Detected format or UNKNOWN if more analysis is needed
71
71
  */
72
72
  private static quickFormatCheck(xml: string): InvoiceFormat {
73
- const lowerXml = xml.toLowerCase();
73
+ // Only scan a small prefix so large payloads do not create another full-size string copy.
74
+ const sample = xml.slice(0, 65536);
75
+
76
+ // Root-element checks avoid a DOM parse for the common invoice formats.
77
+ if (/<(?:[A-Za-z_][\w.-]*:)?(?:Invoice|CreditNote)\b/.test(sample)) {
78
+ const customizationIdMatch = sample.match(
79
+ /<[^>]*CustomizationID[^>]*>\s*([^<]+?)\s*<\/[^>]*CustomizationID>/i,
80
+ );
81
+ const customizationId = customizationIdMatch?.[1] ?? '';
82
+
83
+ if (/xrechnung/i.test(customizationId) || /urn:xoev-de:kosit:standard:xrechnung/i.test(customizationId)) {
84
+ return InvoiceFormat.XRECHNUNG;
85
+ }
86
+ return InvoiceFormat.UBL;
87
+ }
88
+
89
+ if (/<(?:[A-Za-z_][\w.-]*:)?CrossIndustryInvoice\b/.test(sample)) {
90
+ const guidelineIdMatch = sample.match(
91
+ /<[^>]*GuidelineSpecifiedDocumentContextParameter[^>]*>[\s\S]*?<[^>]*ID[^>]*>\s*([^<]+?)\s*<\/[^>]*ID>/i,
92
+ );
93
+ const guidelineId = guidelineIdMatch?.[1] ?? '';
94
+
95
+ if (/xrechnung/i.test(guidelineId)) {
96
+ return InvoiceFormat.XRECHNUNG;
97
+ }
98
+ if (/factur-x/i.test(guidelineId) || /urn:cen\.eu:en16931:2017/i.test(guidelineId)) {
99
+ return InvoiceFormat.FACTURX;
100
+ }
101
+ if (/zugferd/i.test(guidelineId) || /urn:ferd:/i.test(guidelineId) || /urn:zugferd/i.test(guidelineId)) {
102
+ return InvoiceFormat.ZUGFERD;
103
+ }
104
+ return InvoiceFormat.CII;
105
+ }
106
+
107
+ if (/<(?:[A-Za-z_][\w.-]*:)?CrossIndustryDocument\b/.test(sample)) {
108
+ return InvoiceFormat.ZUGFERD;
109
+ }
110
+
111
+ if (/<FatturaElettronica\b/.test(sample)) {
112
+ return InvoiceFormat.FATTURAPA;
113
+ }
74
114
 
75
115
  // Check for obvious Factur-X indicators
76
116
  if (
77
- lowerXml.includes('factur-x.eu') ||
78
- lowerXml.includes('factur-x.xml') ||
79
- lowerXml.includes('factur-x:') ||
80
- lowerXml.includes('urn:cen.eu:en16931:2017') && lowerXml.includes('factur-x')
117
+ /factur-x\.eu/i.test(sample) ||
118
+ /factur-x\.xml/i.test(sample) ||
119
+ /factur-x:/i.test(sample) ||
120
+ (/urn:cen\.eu:en16931:2017/i.test(sample) && /factur-x/i.test(sample))
81
121
  ) {
82
122
  return InvoiceFormat.FACTURX;
83
123
  }
84
124
 
85
125
  // Check for obvious ZUGFeRD indicators
86
126
  if (
87
- lowerXml.includes('zugferd:') ||
88
- lowerXml.includes('zugferd-invoice.xml') ||
89
- lowerXml.includes('urn:ferd:') ||
90
- lowerXml.includes('urn:zugferd')
127
+ /zugferd:/i.test(sample) ||
128
+ /zugferd-invoice\.xml/i.test(sample) ||
129
+ /urn:ferd:/i.test(sample) ||
130
+ /urn:zugferd/i.test(sample)
91
131
  ) {
92
132
  return InvoiceFormat.ZUGFERD;
93
133
  }
94
134
 
95
135
  // Check for obvious XRechnung indicators
96
136
  if (
97
- lowerXml.includes('xrechnung') ||
98
- lowerXml.includes('urn:xoev-de:kosit:standard:xrechnung')
137
+ /xrechnung/i.test(sample) ||
138
+ /urn:xoev-de:kosit:standard:xrechnung/i.test(sample)
99
139
  ) {
100
140
  return InvoiceFormat.XRECHNUNG;
101
141
  }
102
142
 
103
143
  // Check for obvious FatturaPA indicators
104
144
  if (
105
- lowerXml.includes('fatturapa') ||
106
- lowerXml.includes('fattura elettronica') ||
107
- lowerXml.includes('fatturaelettronica')
145
+ /fatturapa/i.test(sample) ||
146
+ /fattura elettronica/i.test(sample) ||
147
+ /fatturaelettronica/i.test(sample)
108
148
  ) {
109
149
  return InvoiceFormat.FATTURAPA;
110
150
  }
@@ -198,10 +238,8 @@ export class FormatDetector {
198
238
  * @returns True if it's a FatturaPA format
199
239
  */
200
240
  private static isFatturaPAFormat(root: Element): boolean {
201
- return (
202
- root.nodeName === 'FatturaElettronica' ||
203
- (root.getAttribute('xmlns') && root.getAttribute('xmlns')!.includes('fatturapa.gov.it'))
204
- );
241
+ const xmlns = root.getAttribute('xmlns') || '';
242
+ return root.nodeName === 'FatturaElettronica' || xmlns.includes('fatturapa.gov.it');
205
243
  }
206
244
 
207
245
  /**
@@ -303,4 +341,4 @@ export class FormatDetector {
303
341
  // Generic CII if we can't determine more specifically
304
342
  return InvoiceFormat.CII;
305
343
  }
306
- }
344
+ }
@@ -185,7 +185,8 @@ export class ConformanceTestHarness {
185
185
  r.source === 'Schematron'
186
186
  );
187
187
  } catch (error) {
188
- console.warn(`Schematron not available for ${sample.format}: ${error.message}`);
188
+ const errorMessage = error instanceof Error ? error.message : String(error);
189
+ console.warn(`Schematron not available for ${sample.format}: ${errorMessage}`);
189
190
  }
190
191
 
191
192
  // Aggregate results
@@ -202,12 +203,13 @@ export class ConformanceTestHarness {
202
203
  result.passed = result.errors.length === 0 === sample.expectedValid;
203
204
 
204
205
  } catch (error) {
205
- console.error(`Error testing ${sample.name}: ${error.message}`);
206
+ const errorMessage = error instanceof Error ? error.message : String(error);
207
+ console.error(`Error testing ${sample.name}: ${errorMessage}`);
206
208
  result.errors.push({
207
209
  ruleId: 'TEST-ERROR',
208
210
  source: 'TestHarness',
209
211
  severity: 'error',
210
- message: `Test execution failed: ${error.message}`
212
+ message: `Test execution failed: ${errorMessage}`
211
213
  });
212
214
  }
213
215
 
@@ -588,4 +590,4 @@ export async function runConformanceTests(
588
590
  // Generate HTML report
589
591
  await harness.generateHTMLReport();
590
592
  }
591
- }
593
+ }