@fin.cx/einvoice 5.1.3 → 5.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 (98) hide show
  1. package/{npmextra.json → .smartconfig.json} +12 -6
  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 +165 -60
  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/converters/xml-to-einvoice.converter.js +3 -3
  15. package/dist_ts/formats/factories/decoder.factory.d.ts +3 -1
  16. package/dist_ts/formats/factories/decoder.factory.js +4 -3
  17. package/dist_ts/formats/factories/validator.factory.d.ts +4 -1
  18. package/dist_ts/formats/factories/validator.factory.js +11 -8
  19. package/dist_ts/formats/pdf/extractors/base.extractor.js +49 -51
  20. package/dist_ts/formats/pdf/extractors/text.extractor.js +19 -22
  21. package/dist_ts/formats/pdf/pdf.extractor.js +2 -2
  22. package/dist_ts/formats/semantic/semantic.adapter.js +42 -12
  23. package/dist_ts/formats/semantic/semantic.validator.js +3 -1
  24. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -3
  25. package/dist_ts/formats/ubl/generic/ubl.encoder.js +11 -7
  26. package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -0
  27. package/dist_ts/formats/ubl/ubl.decoder.js +18 -8
  28. package/dist_ts/formats/ubl/ubl.validator.d.ts +1 -1
  29. package/dist_ts/formats/ubl/ubl.validator.js +17 -10
  30. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +37 -29
  31. package/dist_ts/formats/utils/currency.calculator.decimal.js +4 -1
  32. package/dist_ts/formats/utils/currency.utils.js +4 -1
  33. package/dist_ts/formats/utils/decimal.js +9 -2
  34. package/dist_ts/formats/utils/format.detector.js +47 -17
  35. package/dist_ts/formats/validation/codelist.validator.js +2 -4
  36. package/dist_ts/formats/validation/conformance.harness.js +13 -6
  37. package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -4
  38. package/dist_ts/formats/validation/en16931.validator.d.ts +6 -0
  39. package/dist_ts/formats/validation/en16931.validator.js +18 -12
  40. package/dist_ts/formats/validation/facturx.validator.js +147 -148
  41. package/dist_ts/formats/validation/integrated.validator.js +12 -4
  42. package/dist_ts/formats/validation/peppol.validator.js +2 -1
  43. package/dist_ts/formats/validation/schematron.downloader.js +13 -6
  44. package/dist_ts/formats/validation/schematron.integration.js +12 -5
  45. package/dist_ts/formats/validation/schematron.validator.js +17 -13
  46. package/dist_ts/formats/validation/schematron.worker.js +6 -5
  47. package/dist_ts/formats/validation/vat-categories.validator.js +3 -4
  48. package/dist_ts/formats/validation/xrechnung.validator.js +7 -7
  49. package/dist_ts/index.js +3 -2
  50. package/dist_ts/interfaces/common.js +1 -2
  51. package/dist_ts/plugins.d.ts +3 -4
  52. package/dist_ts/plugins.js +4 -5
  53. package/dist_ts/vendor/pako.d.ts +5 -0
  54. package/dist_ts/vendor/pako.js +3 -0
  55. package/dist_ts/vendor/saxonjs.d.ts +22 -0
  56. package/dist_ts/vendor/saxonjs.js +3 -0
  57. package/dist_ts/vendor/xmldom.d.ts +20 -0
  58. package/dist_ts/vendor/xmldom.js +5 -0
  59. package/dist_ts_install/download-schematron.d.ts +0 -3
  60. package/dist_ts_install/download-schematron.js +20 -11
  61. package/dist_ts_install/download-test-samples.d.ts +0 -3
  62. package/dist_ts_install/download-test-samples.js +4 -2
  63. package/dist_ts_install/download-xrechnung-rules.js +2 -1
  64. package/dist_ts_install/index.d.ts +0 -5
  65. package/dist_ts_install/index.js +26 -12
  66. package/license +21 -0
  67. package/package.json +25 -23
  68. package/readme.hints.md +24 -8
  69. package/readme.md +195 -328
  70. package/ts/00_commitinfo_data.ts +1 -1
  71. package/ts/einvoice.ts +148 -26
  72. package/ts/formats/base/base.decoder.ts +7 -0
  73. package/ts/formats/cii/cii.decoder.ts +17 -8
  74. package/ts/formats/cii/cii.validator.ts +18 -13
  75. package/ts/formats/converters/xml-to-einvoice.converter.ts +4 -5
  76. package/ts/formats/factories/decoder.factory.ts +7 -2
  77. package/ts/formats/factories/validator.factory.ts +14 -8
  78. package/ts/formats/semantic/semantic.adapter.ts +49 -15
  79. package/ts/formats/ubl/en16931.ubl.validator.ts +5 -3
  80. package/ts/formats/ubl/generic/ubl.encoder.ts +12 -7
  81. package/ts/formats/ubl/ubl.decoder.ts +18 -9
  82. package/ts/formats/ubl/ubl.validator.ts +17 -12
  83. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +41 -28
  84. package/ts/formats/utils/format.detector.ts +57 -19
  85. package/ts/formats/validation/conformance.harness.ts +6 -4
  86. package/ts/formats/validation/en16931.validator.ts +20 -10
  87. package/ts/formats/validation/integrated.validator.ts +5 -3
  88. package/ts/formats/validation/schematron.downloader.ts +11 -6
  89. package/ts/formats/validation/schematron.integration.ts +7 -4
  90. package/ts/formats/validation/schematron.validator.ts +12 -11
  91. package/ts/index.ts +2 -1
  92. package/ts/interfaces/common.ts +1 -3
  93. package/ts/plugins.ts +3 -4
  94. package/ts/readme.md +55 -0
  95. package/ts/vendor/modules.d.ts +32 -0
  96. package/ts/vendor/pako.ts +8 -0
  97. package/ts/vendor/saxonjs.ts +28 -0
  98. package/ts/vendor/xmldom.ts +27 -0
package/ts/einvoice.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
 
3
- import { business, finance } from './plugins.js';
3
+ import type { business, finance } from '@tsclass/tsclass';
4
4
  import type { TInvoice, TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
5
5
  import { InvoiceFormat, ValidationLevel } from './interfaces/common.js';
6
6
  import type { ValidationResult, ValidationError, EInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
@@ -28,6 +28,7 @@ import { PDFExtractor } from './formats/pdf/pdf.extractor.js';
28
28
  import { FormatDetector } from './formats/utils/format.detector.js';
29
29
 
30
30
  // Import enhanced validators
31
+ import { EN16931Validator } from './formats/validation/en16931.validator.js';
31
32
  import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
32
33
  import { CodeListValidator } from './formats/validation/codelist.validator.js';
33
34
  import type { ValidationOptions } from './formats/validation/validation.types.js';
@@ -41,6 +42,9 @@ import type { IEInvoiceMetadata } from './interfaces/en16931-metadata.js';
41
42
  * Extends the TInvoice interface for seamless integration with existing systems
42
43
  */
43
44
  export class EInvoice implements TInvoice {
45
+ private static sharedPdfEmbedder?: PDFEmbedder;
46
+ private static sharedPdfExtractor?: PDFExtractor;
47
+
44
48
  /**
45
49
  * Creates an EInvoice instance from XML string
46
50
  * @param xmlString XML string to parse
@@ -109,8 +113,8 @@ export class EInvoice implements TInvoice {
109
113
  public incidenceId: string = '';
110
114
  public language: string = 'en';
111
115
  public objectActions: any[] = [];
112
- public pdf: IPdf | null = null;
113
- public pdfAttachments: IPdf[] | null = null;
116
+ public pdf?: IPdf;
117
+ public pdfAttachments?: IPdf[];
114
118
  public accentColor: string | null = null;
115
119
  public logoUrl: string | null = null;
116
120
 
@@ -149,7 +153,13 @@ export class EInvoice implements TInvoice {
149
153
  this.accountingDocType === 'creditnote' ? 'creditnote' : 'debitnote';
150
154
  }
151
155
  public set invoiceType(value: 'invoice' | 'creditnote' | 'debitnote') {
152
- this.accountingDocType = 'invoice'; // Always set to invoice for TInvoice type
156
+ if (value !== 'invoice') {
157
+ throw new EInvoiceFormatError(
158
+ `Unsupported invoice type: ${value}`,
159
+ { unsupportedFeatures: [`invoiceType=${value}`] }
160
+ );
161
+ }
162
+ this.accountingDocType = 'invoice';
153
163
  }
154
164
 
155
165
  // Computed properties for convenience
@@ -181,15 +191,28 @@ export class EInvoice implements TInvoice {
181
191
 
182
192
  private xmlString: string = '';
183
193
  private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
194
+ private parsedXmlDocument?: Document;
184
195
  private validationErrors: ValidationError[] = [];
196
+ private validationCache = new Map<string, ValidationResult>();
185
197
  private options: EInvoiceOptions = {
186
198
  validateOnLoad: false,
187
199
  validationLevel: ValidationLevel.SYNTAX
188
200
  };
189
201
 
190
- // PDF utilities
191
- private pdfEmbedder = new PDFEmbedder();
192
- private pdfExtractor = new PDFExtractor();
202
+ // PDF utilities are created lazily because most invoice workflows never touch PDF I/O.
203
+ private get pdfEmbedder(): PDFEmbedder {
204
+ if (!EInvoice.sharedPdfEmbedder) {
205
+ EInvoice.sharedPdfEmbedder = new PDFEmbedder();
206
+ }
207
+ return EInvoice.sharedPdfEmbedder;
208
+ }
209
+
210
+ private get pdfExtractor(): PDFExtractor {
211
+ if (!EInvoice.sharedPdfExtractor) {
212
+ EInvoice.sharedPdfExtractor = new PDFExtractor();
213
+ }
214
+ return EInvoice.sharedPdfExtractor;
215
+ }
193
216
 
194
217
  /**
195
218
  * Creates a new EInvoice instance
@@ -260,6 +283,7 @@ export class EInvoice implements TInvoice {
260
283
  */
261
284
  public async fromXmlString(xmlString: string): Promise<EInvoice> {
262
285
  try {
286
+ this.validationCache.clear();
263
287
  this.xmlString = xmlString;
264
288
 
265
289
  // Detect format
@@ -269,8 +293,13 @@ export class EInvoice implements TInvoice {
269
293
  }
270
294
 
271
295
  // Get appropriate decoder
272
- const decoder = DecoderFactory.createDecoder(xmlString, !this.options.validateOnLoad);
296
+ const decoder = DecoderFactory.createDecoder(
297
+ xmlString,
298
+ !this.options.validateOnLoad,
299
+ this.detectedFormat,
300
+ );
273
301
  const invoice = await decoder.decode();
302
+ this.parsedXmlDocument = decoder.getParsedDocument();
274
303
 
275
304
  // Map the decoded invoice to our properties
276
305
  this.mapFromTInvoice(invoice);
@@ -282,10 +311,11 @@ export class EInvoice implements TInvoice {
282
311
 
283
312
  return this;
284
313
  } catch (error) {
314
+ const errorMessage = error instanceof Error ? error.message : String(error);
285
315
  if (error instanceof EInvoiceError) {
286
316
  throw error;
287
317
  }
288
- throw new EInvoiceParsingError(`Failed to parse XML: ${error.message}`, {}, error as Error);
318
+ throw new EInvoiceParsingError(`Failed to parse XML: ${errorMessage}`, {}, error as Error);
289
319
  }
290
320
  }
291
321
 
@@ -307,7 +337,8 @@ export class EInvoice implements TInvoice {
307
337
  const xmlString = fileBuffer.toString('utf-8');
308
338
  return this.fromXmlString(xmlString);
309
339
  } catch (error) {
310
- throw new EInvoiceError(`Failed to load file: ${error.message}`, 'FILE_LOAD_ERROR', { filePath });
340
+ const errorMessage = error instanceof Error ? error.message : String(error);
341
+ throw new EInvoiceError(`Failed to load file: ${errorMessage}`, 'FILE_LOAD_ERROR', { filePath });
311
342
  }
312
343
  }
313
344
 
@@ -343,10 +374,11 @@ export class EInvoice implements TInvoice {
343
374
 
344
375
  return this.fromXmlString(extractedXml);
345
376
  } catch (error) {
377
+ const errorMessage = error instanceof Error ? error.message : String(error);
346
378
  if (error instanceof EInvoiceError) {
347
379
  throw error;
348
380
  }
349
- throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${error.message}`, 'extract', {}, error as Error);
381
+ throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${errorMessage}`, 'extract', {}, error as Error);
350
382
  }
351
383
  }
352
384
 
@@ -423,7 +455,8 @@ export class EInvoice implements TInvoice {
423
455
 
424
456
  return await encoder.encode(invoice);
425
457
  } catch (error) {
426
- throw new EInvoiceFormatError(`Failed to encode to ${format}: ${error.message}`, { targetFormat: format });
458
+ const errorMessage = error instanceof Error ? error.message : String(error);
459
+ throw new EInvoiceFormatError(`Failed to encode to ${format}: ${errorMessage}`, { targetFormat: format });
427
460
  }
428
461
  }
429
462
 
@@ -434,21 +467,34 @@ export class EInvoice implements TInvoice {
434
467
  */
435
468
  public async validate(level: ValidationLevel = ValidationLevel.BUSINESS, options?: ValidationOptions): Promise<ValidationResult> {
436
469
  try {
437
- // For programmatically created invoices without XML, skip XML-based validation
470
+ // For programmatically created invoices without XML, validate the in-memory invoice object.
438
471
  let result: ValidationResult;
472
+ const cacheKey = this.getValidationCacheKey(level, options);
473
+
474
+ if (cacheKey) {
475
+ const cached = this.validationCache.get(cacheKey);
476
+ if (cached) {
477
+ return this.cloneValidationResult(cached);
478
+ }
479
+ }
439
480
 
440
481
  if (this.xmlString && this.detectedFormat !== InvoiceFormat.UNKNOWN) {
482
+ if (this.shouldUseFastBusinessValidation(level, options)) {
483
+ result = this.validateDecodedInvoice(level);
484
+ } else {
441
485
  // Use existing validator for XML-based validation
442
- const validator = ValidatorFactory.createValidator(this.xmlString);
443
- result = validator.validate(level);
486
+ const validator = ValidatorFactory.createValidator(
487
+ this.xmlString,
488
+ this.detectedFormat,
489
+ this.parsedXmlDocument,
490
+ );
491
+ result = validator.validate(level);
492
+
493
+ // Keep the raw XML, but drop the cached DOM once validation is done.
494
+ this.parsedXmlDocument = undefined;
495
+ }
444
496
  } else {
445
- // Create a basic result for programmatically created invoices
446
- result = {
447
- valid: true,
448
- errors: [],
449
- warnings: [],
450
- level: level
451
- };
497
+ result = this.validateDecodedInvoice(level);
452
498
  }
453
499
 
454
500
  // Enhanced validation with feature flags
@@ -489,14 +535,72 @@ export class EInvoice implements TInvoice {
489
535
  // Update validation status
490
536
  this.validationErrors = result.errors;
491
537
  result.valid = result.errors.length === 0 || options?.reportOnly === true;
538
+
539
+ if (cacheKey) {
540
+ this.validationCache.set(cacheKey, this.cloneValidationResult(result));
541
+ }
492
542
 
493
543
  return result;
494
544
  } catch (error) {
545
+ const errorMessage = error instanceof Error ? error.message : String(error);
495
546
  if (error instanceof EInvoiceError) {
496
547
  throw error;
497
548
  }
498
- throw new EInvoiceValidationError(`Validation failed: ${error.message}`, [], { validationLevel: level });
549
+ throw new EInvoiceValidationError(`Validation failed: ${errorMessage}`, [], { validationLevel: level });
550
+ }
551
+ }
552
+
553
+ private validateDecodedInvoice(level: ValidationLevel): ValidationResult {
554
+ const invoice = this.mapToTInvoice();
555
+ const errors = EN16931Validator.collectMandatoryFieldErrors(invoice).map(message =>
556
+ this.createValidationError(message)
557
+ );
558
+
559
+ return {
560
+ valid: errors.length === 0,
561
+ errors,
562
+ warnings: level === ValidationLevel.SYNTAX
563
+ ? [{
564
+ code: 'VAL-NO-XML',
565
+ message: 'Syntax validation was skipped because no XML document has been loaded.'
566
+ }]
567
+ : [],
568
+ level: level
569
+ };
570
+ }
571
+
572
+ private shouldUseFastBusinessValidation(level: ValidationLevel, options?: ValidationOptions): boolean {
573
+ if (level !== ValidationLevel.BUSINESS) {
574
+ return false;
575
+ }
576
+
577
+ if (options?.featureFlags?.length || options?.reportOnly || this.detectedFormat !== InvoiceFormat.UBL) {
578
+ return false;
499
579
  }
580
+
581
+ // For tiny plain-UBL documents without parties or lines, the decoded invoice model already
582
+ // contains everything needed for the mandatory-field failures the XML validator would report.
583
+ return this.items.length === 0 && !this.from?.name && !this.to?.name;
584
+ }
585
+
586
+ private getValidationCacheKey(level: ValidationLevel, options?: ValidationOptions): string | undefined {
587
+ if (!this.xmlString || options) {
588
+ return undefined;
589
+ }
590
+
591
+ if (level !== ValidationLevel.SYNTAX && level !== ValidationLevel.SEMANTIC) {
592
+ return undefined;
593
+ }
594
+
595
+ return `xml:${level}`;
596
+ }
597
+
598
+ private cloneValidationResult(result: ValidationResult): ValidationResult {
599
+ return {
600
+ ...result,
601
+ errors: result.errors.map(error => ({ ...error })),
602
+ warnings: result.warnings?.map(warning => ({ ...warning }))
603
+ };
500
604
  }
501
605
 
502
606
  /**
@@ -514,7 +618,8 @@ export class EInvoice implements TInvoice {
514
618
  }
515
619
  return embedResult.data! as Buffer;
516
620
  } catch (error) {
517
- throw new EInvoicePDFError(`Failed to embed XML in PDF: ${error.message}`, 'embed', { format }, error as Error);
621
+ const errorMessage = error instanceof Error ? error.message : String(error);
622
+ throw new EInvoicePDFError(`Failed to embed XML in PDF: ${errorMessage}`, 'embed', { format }, error as Error);
518
623
  }
519
624
  }
520
625
 
@@ -546,10 +651,11 @@ export class EInvoice implements TInvoice {
546
651
  await plugins.fs.writeFile(filePath, xmlString, 'utf-8');
547
652
  }
548
653
  } catch (error) {
654
+ const errorMessage = error instanceof Error ? error.message : String(error);
549
655
  if (error instanceof EInvoiceError) {
550
656
  throw error;
551
657
  }
552
- throw new EInvoiceError(`Failed to save file: ${error.message}`, 'FILE_SAVE_ERROR', { filePath });
658
+ throw new EInvoiceError(`Failed to save file: ${errorMessage}`, 'FILE_SAVE_ERROR', { filePath });
553
659
  }
554
660
  }
555
661
 
@@ -649,4 +755,20 @@ export class EInvoice implements TInvoice {
649
755
  public addItem(item: Partial<TAccountingDocItem>): void {
650
756
  this.items.push(this.createItem(item));
651
757
  }
652
- }
758
+
759
+ private createValidationError(message: string): ValidationError {
760
+ const match = message.match(/^([A-Z]{2,}(?:-[A-Z0-9]+)*-\d+):\s*(.+)$/);
761
+
762
+ if (match) {
763
+ return {
764
+ code: match[1],
765
+ message: match[2]
766
+ };
767
+ }
768
+
769
+ return {
770
+ code: 'VALIDATION_ERROR',
771
+ message
772
+ };
773
+ }
774
+ }
@@ -37,6 +37,13 @@ export abstract class BaseDecoder {
37
37
  return this.xml;
38
38
  }
39
39
 
40
+ /**
41
+ * Gets a parsed XML document when a decoder keeps one around.
42
+ */
43
+ public getParsedDocument(): Document | undefined {
44
+ return undefined;
45
+ }
46
+
40
47
  /**
41
48
  * Parses a CII date string based on format code
42
49
  * @param dateStr Date string
@@ -3,6 +3,14 @@ import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.
3
3
  import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
4
4
  import { DOMParser, xpath } from '../../plugins.js';
5
5
 
6
+ const ciiParser = new DOMParser();
7
+ const ciiNamespaces = {
8
+ rsm: CII_NAMESPACES.RSM,
9
+ ram: CII_NAMESPACES.RAM,
10
+ udt: CII_NAMESPACES.UDT,
11
+ };
12
+ const ciiSelect = xpath.useNamespaces(ciiNamespaces);
13
+
6
14
  /**
7
15
  * Base decoder for CII-based invoice formats
8
16
  */
@@ -16,22 +24,22 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
16
24
  super(xml, skipValidation);
17
25
 
18
26
  // Parse XML document
19
- this.doc = new DOMParser().parseFromString(xml, 'application/xml');
27
+ this.doc = ciiParser.parseFromString(xml, 'application/xml');
20
28
 
21
29
  // Set up namespaces for XPath queries
22
- this.namespaces = {
23
- rsm: CII_NAMESPACES.RSM,
24
- ram: CII_NAMESPACES.RAM,
25
- udt: CII_NAMESPACES.UDT
26
- };
30
+ this.namespaces = ciiNamespaces;
27
31
 
28
32
  // Create XPath selector with namespaces
29
- this.select = xpath.useNamespaces(this.namespaces);
33
+ this.select = ciiSelect;
30
34
 
31
35
  // Detect profile
32
36
  this.detectProfile();
33
37
  }
34
38
 
39
+ public override getParsedDocument(): Document {
40
+ return this.doc;
41
+ }
42
+
35
43
  /**
36
44
  * Decodes CII XML into a TInvoice object
37
45
  * @returns Promise resolving to a TInvoice object
@@ -93,7 +101,8 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
93
101
  * @returns Text value or empty string if not found
94
102
  */
95
103
  protected getText(xpathExpr: string, context?: Node): string {
96
- const node = this.select(xpathExpr, context || this.doc)[0];
104
+ const result = this.select(xpathExpr, context || this.doc);
105
+ const node = Array.isArray(result) ? result[0] : null;
97
106
  return node ? (node.textContent || '') : '';
98
107
  }
99
108
 
@@ -4,31 +4,35 @@ import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
5
5
  import { DOMParser, xpath } from '../../plugins.js';
6
6
 
7
+ const ciiValidatorParser = new DOMParser();
8
+ const ciiValidatorNamespaces = {
9
+ rsm: CII_NAMESPACES.RSM,
10
+ ram: CII_NAMESPACES.RAM,
11
+ udt: CII_NAMESPACES.UDT,
12
+ };
13
+ const ciiValidatorSelect = xpath.useNamespaces(ciiValidatorNamespaces);
14
+
7
15
  /**
8
16
  * Base validator for CII-based invoice formats
9
17
  */
10
18
  export abstract class CIIBaseValidator extends BaseValidator {
11
- protected doc: Document;
12
- protected namespaces: Record<string, string>;
13
- protected select: xpath.XPathSelect;
19
+ protected doc!: Document;
20
+ protected namespaces!: Record<string, string>;
21
+ protected select!: xpath.XPathSelect;
14
22
  protected profile: CIIProfile = CIIProfile.EN16931;
15
23
 
16
- constructor(xml: string) {
24
+ constructor(xml: string, doc?: Document) {
17
25
  super(xml);
18
26
 
19
27
  try {
20
- // Parse XML document
21
- this.doc = new DOMParser().parseFromString(xml, 'application/xml');
28
+ // Reuse an existing parsed document when available.
29
+ this.doc = doc ?? ciiValidatorParser.parseFromString(xml, 'application/xml');
22
30
 
23
31
  // Set up namespaces for XPath queries
24
- this.namespaces = {
25
- rsm: CII_NAMESPACES.RSM,
26
- ram: CII_NAMESPACES.RAM,
27
- udt: CII_NAMESPACES.UDT
28
- };
32
+ this.namespaces = ciiValidatorNamespaces;
29
33
 
30
34
  // Create XPath selector with namespaces
31
- this.select = xpath.useNamespaces(this.namespaces);
35
+ this.select = ciiValidatorSelect;
32
36
 
33
37
  // Detect profile
34
38
  this.detectProfile();
@@ -139,7 +143,8 @@ export abstract class CIIBaseValidator extends BaseValidator {
139
143
  * @returns Text value or empty string if not found
140
144
  */
141
145
  protected getText(xpathExpr: string, context?: Node): string {
142
- const node = this.select(xpathExpr, context || this.doc)[0];
146
+ const result = this.select(xpathExpr, context || this.doc);
147
+ const node = Array.isArray(result) ? result[0] : null;
143
148
  return node ? (node.textContent || '') : '';
144
149
  }
145
150
 
@@ -6,17 +6,16 @@
6
6
  import * as plugins from '../../plugins.js';
7
7
  import type { EInvoice } from '../../einvoice.js';
8
8
  import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
9
- import { DOMParser } from '@xmldom/xmldom';
10
9
 
11
10
  /**
12
11
  * Converter for XML formats to EInvoice - simplified version
13
12
  * This is a basic converter that extracts essential fields for testing
14
13
  */
15
14
  export class XMLToEInvoiceConverter {
16
- private parser: DOMParser;
15
+ private parser: InstanceType<typeof plugins.DOMParser>;
17
16
 
18
17
  constructor() {
19
- this.parser = new DOMParser();
18
+ this.parser = new plugins.DOMParser();
20
19
  }
21
20
 
22
21
  /**
@@ -116,7 +115,7 @@ export class XMLToEInvoiceConverter {
116
115
  console.warn('Error parsing XML:', error);
117
116
  }
118
117
 
119
- return mockInvoice as EInvoice;
118
+ return mockInvoice as unknown as EInvoice;
120
119
  }
121
120
 
122
121
  /**
@@ -139,4 +138,4 @@ export class XMLToEInvoiceConverter {
139
138
 
140
139
  return null;
141
140
  }
142
- }
141
+ }
@@ -16,10 +16,15 @@ export class DecoderFactory {
16
16
  * Creates a decoder for the specified XML content
17
17
  * @param xml XML content to decode
18
18
  * @param skipValidation Whether to skip EN16931 validation
19
+ * @param formatHint Optional pre-detected format to avoid re-detecting large XML inputs
19
20
  * @returns Appropriate decoder instance
20
21
  */
21
- public static createDecoder(xml: string, skipValidation: boolean = false): BaseDecoder {
22
- const format = FormatDetector.detectFormat(xml);
22
+ public static createDecoder(
23
+ xml: string,
24
+ skipValidation: boolean = false,
25
+ formatHint?: InvoiceFormat,
26
+ ): BaseDecoder {
27
+ const format = formatHint ?? FormatDetector.detectFormat(xml);
23
28
 
24
29
  switch (format) {
25
30
  case InvoiceFormat.UBL:
@@ -59,28 +59,34 @@ export class ValidatorFactory {
59
59
  /**
60
60
  * Creates a validator for the specified XML content
61
61
  * @param xml XML content to validate
62
+ * @param formatHint Optional pre-detected format to avoid re-detecting large XML inputs
63
+ * @param parsedDocument Optional parsed XML document to avoid parsing twice
62
64
  * @returns Appropriate validator instance
63
65
  */
64
- public static createValidator(xml: string): BaseValidator {
66
+ public static createValidator(
67
+ xml: string,
68
+ formatHint?: InvoiceFormat,
69
+ parsedDocument?: Document,
70
+ ): BaseValidator {
65
71
  try {
66
- const format = FormatDetector.detectFormat(xml);
72
+ const format = formatHint ?? FormatDetector.detectFormat(xml);
67
73
 
68
74
  switch (format) {
69
75
  case InvoiceFormat.UBL:
70
- return new EN16931UBLValidator(xml);
76
+ return new EN16931UBLValidator(xml, parsedDocument);
71
77
 
72
78
  case InvoiceFormat.XRECHNUNG:
73
- return new XRechnungValidator(xml);
79
+ return new XRechnungValidator(xml, parsedDocument);
74
80
 
75
81
  case InvoiceFormat.CII:
76
82
  // For now, use Factur-X validator for generic CII
77
- return new FacturXValidator(xml);
83
+ return new FacturXValidator(xml, parsedDocument);
78
84
 
79
85
  case InvoiceFormat.ZUGFERD:
80
- return new ZUGFeRDValidator(xml);
86
+ return new ZUGFeRDValidator(xml, parsedDocument);
81
87
 
82
88
  case InvoiceFormat.FACTURX:
83
- return new FacturXValidator(xml);
89
+ return new FacturXValidator(xml, parsedDocument);
84
90
 
85
91
  case InvoiceFormat.FATTURAPA:
86
92
  return new FatturaPAValidator(xml);
@@ -131,4 +137,4 @@ class GenericValidator extends BaseValidator {
131
137
  protected validateBusinessRules(): boolean {
132
138
  return false;
133
139
  }
134
- }
140
+ }
@@ -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
+ }