@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
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';
@@ -20,14 +20,11 @@ import { DecoderFactory } from './formats/factories/decoder.factory.js';
20
20
  import { EncoderFactory } from './formats/factories/encoder.factory.js';
21
21
  import { ValidatorFactory } from './formats/factories/validator.factory.js';
22
22
 
23
- // Import PDF utilities
24
- import { PDFEmbedder } from './formats/pdf/pdf.embedder.js';
25
- import { PDFExtractor } from './formats/pdf/pdf.extractor.js';
26
-
27
23
  // Import format detector
28
24
  import { FormatDetector } from './formats/utils/format.detector.js';
29
25
 
30
26
  // Import enhanced validators
27
+ import { EN16931Validator } from './formats/validation/en16931.validator.js';
31
28
  import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
32
29
  import { CodeListValidator } from './formats/validation/codelist.validator.js';
33
30
  import type { ValidationOptions } from './formats/validation/validation.types.js';
@@ -41,6 +38,9 @@ import type { IEInvoiceMetadata } from './interfaces/en16931-metadata.js';
41
38
  * Extends the TInvoice interface for seamless integration with existing systems
42
39
  */
43
40
  export class EInvoice implements TInvoice {
41
+ private static sharedPdfEmbedder?: import('./formats/pdf/pdf.embedder.js').PDFEmbedder;
42
+ private static sharedPdfExtractor?: import('./formats/pdf/pdf.extractor.js').PDFExtractor;
43
+
44
44
  /**
45
45
  * Creates an EInvoice instance from XML string
46
46
  * @param xmlString XML string to parse
@@ -75,7 +75,8 @@ export class EInvoice implements TInvoice {
75
75
  await invoice.fromPdfFile(pdfBuffer);
76
76
  } else {
77
77
  // If given a buffer, extract XML and parse it
78
- const extractResult = await invoice.pdfExtractor.extractXml(pdfBuffer);
78
+ const pdfExtractor = await invoice.getPdfExtractor();
79
+ const extractResult = await pdfExtractor.extractXml(pdfBuffer);
79
80
  if (!extractResult.success || !extractResult.xml) {
80
81
  throw new EInvoicePDFError('No invoice XML found in PDF', 'extract');
81
82
  }
@@ -109,8 +110,8 @@ export class EInvoice implements TInvoice {
109
110
  public incidenceId: string = '';
110
111
  public language: string = 'en';
111
112
  public objectActions: any[] = [];
112
- public pdf: IPdf | null = null;
113
- public pdfAttachments: IPdf[] | null = null;
113
+ public pdf?: IPdf;
114
+ public pdfAttachments?: IPdf[];
114
115
  public accentColor: string | null = null;
115
116
  public logoUrl: string | null = null;
116
117
 
@@ -149,7 +150,13 @@ export class EInvoice implements TInvoice {
149
150
  this.accountingDocType === 'creditnote' ? 'creditnote' : 'debitnote';
150
151
  }
151
152
  public set invoiceType(value: 'invoice' | 'creditnote' | 'debitnote') {
152
- this.accountingDocType = 'invoice'; // Always set to invoice for TInvoice type
153
+ if (value !== 'invoice') {
154
+ throw new EInvoiceFormatError(
155
+ `Unsupported invoice type: ${value}`,
156
+ { unsupportedFeatures: [`invoiceType=${value}`] }
157
+ );
158
+ }
159
+ this.accountingDocType = 'invoice';
153
160
  }
154
161
 
155
162
  // Computed properties for convenience
@@ -181,15 +188,30 @@ export class EInvoice implements TInvoice {
181
188
 
182
189
  private xmlString: string = '';
183
190
  private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
191
+ private parsedXmlDocument?: Document;
184
192
  private validationErrors: ValidationError[] = [];
193
+ private validationCache = new Map<string, ValidationResult>();
185
194
  private options: EInvoiceOptions = {
186
195
  validateOnLoad: false,
187
196
  validationLevel: ValidationLevel.SYNTAX
188
197
  };
189
198
 
190
- // PDF utilities
191
- private pdfEmbedder = new PDFEmbedder();
192
- private pdfExtractor = new PDFExtractor();
199
+ // PDF utilities are created lazily because most invoice workflows never touch PDF I/O.
200
+ private async getPdfEmbedder(): Promise<import('./formats/pdf/pdf.embedder.js').PDFEmbedder> {
201
+ if (!EInvoice.sharedPdfEmbedder) {
202
+ const { PDFEmbedder } = await import('./formats/pdf/pdf.embedder.js');
203
+ EInvoice.sharedPdfEmbedder = new PDFEmbedder();
204
+ }
205
+ return EInvoice.sharedPdfEmbedder;
206
+ }
207
+
208
+ private async getPdfExtractor(): Promise<import('./formats/pdf/pdf.extractor.js').PDFExtractor> {
209
+ if (!EInvoice.sharedPdfExtractor) {
210
+ const { PDFExtractor } = await import('./formats/pdf/pdf.extractor.js');
211
+ EInvoice.sharedPdfExtractor = new PDFExtractor();
212
+ }
213
+ return EInvoice.sharedPdfExtractor;
214
+ }
193
215
 
194
216
  /**
195
217
  * Creates a new EInvoice instance
@@ -260,6 +282,7 @@ export class EInvoice implements TInvoice {
260
282
  */
261
283
  public async fromXmlString(xmlString: string): Promise<EInvoice> {
262
284
  try {
285
+ this.validationCache.clear();
263
286
  this.xmlString = xmlString;
264
287
 
265
288
  // Detect format
@@ -269,8 +292,13 @@ export class EInvoice implements TInvoice {
269
292
  }
270
293
 
271
294
  // Get appropriate decoder
272
- const decoder = DecoderFactory.createDecoder(xmlString, !this.options.validateOnLoad);
295
+ const decoder = DecoderFactory.createDecoder(
296
+ xmlString,
297
+ !this.options.validateOnLoad,
298
+ this.detectedFormat,
299
+ );
273
300
  const invoice = await decoder.decode();
301
+ this.parsedXmlDocument = decoder.getParsedDocument();
274
302
 
275
303
  // Map the decoded invoice to our properties
276
304
  this.mapFromTInvoice(invoice);
@@ -282,10 +310,11 @@ export class EInvoice implements TInvoice {
282
310
 
283
311
  return this;
284
312
  } catch (error) {
313
+ const errorMessage = error instanceof Error ? error.message : String(error);
285
314
  if (error instanceof EInvoiceError) {
286
315
  throw error;
287
316
  }
288
- throw new EInvoiceParsingError(`Failed to parse XML: ${error.message}`, {}, error as Error);
317
+ throw new EInvoiceParsingError(`Failed to parse XML: ${errorMessage}`, {}, error as Error);
289
318
  }
290
319
  }
291
320
 
@@ -307,7 +336,8 @@ export class EInvoice implements TInvoice {
307
336
  const xmlString = fileBuffer.toString('utf-8');
308
337
  return this.fromXmlString(xmlString);
309
338
  } catch (error) {
310
- throw new EInvoiceError(`Failed to load file: ${error.message}`, 'FILE_LOAD_ERROR', { filePath });
339
+ const errorMessage = error instanceof Error ? error.message : String(error);
340
+ throw new EInvoiceError(`Failed to load file: ${errorMessage}`, 'FILE_LOAD_ERROR', { filePath });
311
341
  }
312
342
  }
313
343
 
@@ -319,7 +349,8 @@ export class EInvoice implements TInvoice {
319
349
  public async fromPdfFile(filePath: string): Promise<EInvoice> {
320
350
  try {
321
351
  const pdfBuffer = await plugins.fs.readFile(filePath);
322
- const extractResult = await this.pdfExtractor.extractXml(pdfBuffer);
352
+ const pdfExtractor = await this.getPdfExtractor();
353
+ const extractResult = await pdfExtractor.extractXml(pdfBuffer);
323
354
  const extractedXml = extractResult.success ? extractResult.xml : null;
324
355
 
325
356
  if (!extractedXml) {
@@ -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,16 +535,74 @@ 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 });
499
550
  }
500
551
  }
501
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;
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
+ };
604
+ }
605
+
502
606
  /**
503
607
  * Embeds the invoice XML into a PDF
504
608
  * @param pdfBuffer The PDF buffer to embed into
@@ -508,13 +612,15 @@ export class EInvoice implements TInvoice {
508
612
  public async embedInPdf(pdfBuffer: Buffer, format: ExportFormat = 'facturx'): Promise<Buffer> {
509
613
  try {
510
614
  const xmlString = await this.toXmlString(format);
511
- const embedResult = await this.pdfEmbedder.embedXml(pdfBuffer, xmlString, 'invoice.xml', `${format} Invoice`);
615
+ const pdfEmbedder = await this.getPdfEmbedder();
616
+ const embedResult = await pdfEmbedder.embedXml(pdfBuffer, xmlString, 'invoice.xml', `${format} Invoice`);
512
617
  if (!embedResult.success) {
513
618
  throw new EInvoicePDFError('Failed to embed XML in PDF', 'embed', { format });
514
619
  }
515
620
  return embedResult.data! as Buffer;
516
621
  } catch (error) {
517
- throw new EInvoicePDFError(`Failed to embed XML in PDF: ${error.message}`, 'embed', { format }, error as Error);
622
+ const errorMessage = error instanceof Error ? error.message : String(error);
623
+ throw new EInvoicePDFError(`Failed to embed XML in PDF: ${errorMessage}`, 'embed', { format }, error as Error);
518
624
  }
519
625
  }
520
626
 
@@ -546,10 +652,11 @@ export class EInvoice implements TInvoice {
546
652
  await plugins.fs.writeFile(filePath, xmlString, 'utf-8');
547
653
  }
548
654
  } catch (error) {
655
+ const errorMessage = error instanceof Error ? error.message : String(error);
549
656
  if (error instanceof EInvoiceError) {
550
657
  throw error;
551
658
  }
552
- throw new EInvoiceError(`Failed to save file: ${error.message}`, 'FILE_SAVE_ERROR', { filePath });
659
+ throw new EInvoiceError(`Failed to save file: ${errorMessage}`, 'FILE_SAVE_ERROR', { filePath });
553
660
  }
554
661
  }
555
662
 
@@ -649,4 +756,20 @@ export class EInvoice implements TInvoice {
649
756
  public addItem(item: Partial<TAccountingDocItem>): void {
650
757
  this.items.push(this.createItem(item));
651
758
  }
652
- }
759
+
760
+ private createValidationError(message: string): ValidationError {
761
+ const match = message.match(/^([A-Z]{2,}(?:-[A-Z0-9]+)*-\d+):\s*(.+)$/);
762
+
763
+ if (match) {
764
+ return {
765
+ code: match[1],
766
+ message: match[2]
767
+ };
768
+ }
769
+
770
+ return {
771
+ code: 'VALIDATION_ERROR',
772
+ message
773
+ };
774
+ }
775
+ }
@@ -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
 
@@ -2,6 +2,9 @@ import { CIIBaseEncoder } from '../cii.encoder.js';
2
2
  import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
3
3
  import { FACTURX_PROFILE_IDS } from './facturx.types.js';
4
4
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
5
+ import { Decimal } from '../../utils/decimal.js';
6
+ import { DecimalCurrencyCalculator } from '../../utils/currency.calculator.decimal.js';
7
+ import { getCurrencyMinorUnits } from '../../utils/currency.utils.js';
5
8
 
6
9
  /**
7
10
  * Encoder for Factur-X invoice format
@@ -329,42 +332,46 @@ export class FacturXEncoder extends CIIBaseEncoder {
329
332
  // Add totals
330
333
  const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
331
334
 
332
- // Calculate totals
333
- let totalNetAmount = 0;
334
- let totalTaxAmount = 0;
335
+ // Calculate totals with decimal arithmetic to avoid floating point drift.
336
+ const calculator = new DecimalCurrencyCalculator(invoice.currency);
337
+ const minorUnits = getCurrencyMinorUnits(invoice.currency);
338
+ let totalNetAmount = Decimal.ZERO;
339
+ let totalTaxAmount = Decimal.ZERO;
335
340
 
336
341
  // Calculate from items
337
342
  if (invoice.items) {
338
343
  for (const item of invoice.items) {
339
- const itemNetAmount = item.unitNetPrice * item.unitQuantity;
340
- const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
344
+ const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
345
+ const itemTaxAmount = calculator.calculateVAT(itemNetAmount, item.vatPercentage);
341
346
 
342
- totalNetAmount += itemNetAmount;
343
- totalTaxAmount += itemTaxAmount;
347
+ totalNetAmount = totalNetAmount.add(itemNetAmount);
348
+ totalTaxAmount = totalTaxAmount.add(itemTaxAmount);
344
349
  }
345
350
  }
346
351
 
347
- const totalGrossAmount = totalNetAmount + totalTaxAmount;
352
+ totalNetAmount = calculator.round(totalNetAmount);
353
+ totalTaxAmount = calculator.round(totalTaxAmount);
354
+ const totalGrossAmount = calculator.calculateGrossAmount(totalNetAmount, totalTaxAmount);
348
355
 
349
356
  // Add line total amount
350
357
  const lineTotalElement = doc.createElement('ram:LineTotalAmount');
351
- lineTotalElement.textContent = totalNetAmount.toFixed(2);
358
+ lineTotalElement.textContent = totalNetAmount.toFixed(minorUnits);
352
359
  monetarySummationElement.appendChild(lineTotalElement);
353
360
 
354
361
  // Add tax total amount
355
362
  const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
356
- taxTotalElement.textContent = totalTaxAmount.toFixed(2);
363
+ taxTotalElement.textContent = totalTaxAmount.toFixed(minorUnits);
357
364
  taxTotalElement.setAttribute('currencyID', invoice.currency);
358
365
  monetarySummationElement.appendChild(taxTotalElement);
359
366
 
360
367
  // Add grand total amount
361
368
  const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
362
- grandTotalElement.textContent = totalGrossAmount.toFixed(2);
369
+ grandTotalElement.textContent = totalGrossAmount.toFixed(minorUnits);
363
370
  monetarySummationElement.appendChild(grandTotalElement);
364
371
 
365
372
  // Add due payable amount
366
373
  const duePayableElement = doc.createElement('ram:DuePayableAmount');
367
- duePayableElement.textContent = totalGrossAmount.toFixed(2);
374
+ duePayableElement.textContent = totalGrossAmount.toFixed(minorUnits);
368
375
  monetarySummationElement.appendChild(duePayableElement);
369
376
 
370
377
  settlementElement.appendChild(monetarySummationElement);
@@ -377,6 +384,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
377
384
  * @param invoice Invoice data
378
385
  */
379
386
  private addLineItems(doc: Document, transactionElement: Element, invoice: TInvoice): void {
387
+ const calculator = new DecimalCurrencyCalculator(invoice.currency);
388
+ const minorUnits = getCurrencyMinorUnits(invoice.currency);
389
+
380
390
  // Add each line item
381
391
  if (invoice.items) {
382
392
  for (const item of invoice.items) {
@@ -450,12 +460,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
450
460
  // Add monetary summation
451
461
  const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');
452
462
 
453
- // Calculate item total
454
- const itemNetAmount = item.unitNetPrice * item.unitQuantity;
463
+ // Calculate item total with decimal arithmetic and currency rounding.
464
+ const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
455
465
 
456
466
  // Add line total amount
457
467
  const lineTotalElement = doc.createElement('ram:LineTotalAmount');
458
- lineTotalElement.textContent = itemNetAmount.toFixed(2);
468
+ lineTotalElement.textContent = itemNetAmount.toFixed(minorUnits);
459
469
  monetarySummationElement.appendChild(lineTotalElement);
460
470
 
461
471
  settlementElement.appendChild(monetarySummationElement);
@@ -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
+ }