@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
@@ -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
+ }
@@ -27,11 +27,11 @@ export class EN16931Validator {
27
27
  ];
28
28
 
29
29
  /**
30
- * Validates that an invoice object contains all mandatory EN16931 fields
30
+ * Collects mandatory EN16931 field errors without throwing.
31
31
  * @param invoice The invoice object to validate
32
- * @throws Error if mandatory fields are missing
32
+ * @returns List of validation error messages
33
33
  */
34
- public static validateMandatoryFields(invoice: any): void {
34
+ public static collectMandatoryFieldErrors(invoice: any): string[] {
35
35
  const errors: string[] = [];
36
36
 
37
37
  // BR-01: Invoice number is mandatory
@@ -49,7 +49,7 @@ export class EN16931Validator {
49
49
  errors.push('BR-06: Seller name is mandatory');
50
50
  }
51
51
 
52
- // BR-07: Buyer name is mandatory
52
+ // BR-07: Buyer name is mandatory
53
53
  if (!invoice.to?.name) {
54
54
  errors.push('BR-07: Buyer name is mandatory');
55
55
  }
@@ -67,11 +67,10 @@ export class EN16931Validator {
67
67
  // BR-05: Invoice currency code is mandatory
68
68
  if (!invoice.currency) {
69
69
  errors.push('BR-05: Invoice currency code is mandatory');
70
- } else {
71
- // Validate currency format
72
- if (!this.VALID_CURRENCIES.includes(invoice.currency.toUpperCase())) {
73
- errors.push(`Invalid currency code: ${invoice.currency}. Must be a valid ISO 4217 currency code`);
74
- }
70
+ } else if (!this.VALID_CURRENCIES.includes(invoice.currency.toUpperCase())) {
71
+ errors.push(
72
+ `BR-05: Invalid currency code: ${invoice.currency}. Must be a valid ISO 4217 currency code`
73
+ );
75
74
  }
76
75
 
77
76
  // BR-08: Seller postal address is mandatory
@@ -84,6 +83,17 @@ export class EN16931Validator {
84
83
  errors.push('BR-10: Buyer postal address (city, postal code, country) is mandatory');
85
84
  }
86
85
 
86
+ return errors;
87
+ }
88
+
89
+ /**
90
+ * Validates that an invoice object contains all mandatory EN16931 fields
91
+ * @param invoice The invoice object to validate
92
+ * @throws Error if mandatory fields are missing
93
+ */
94
+ public static validateMandatoryFields(invoice: any): void {
95
+ const errors = this.collectMandatoryFieldErrors(invoice);
96
+
87
97
  if (errors.length > 0) {
88
98
  throw new Error(`EN16931 validation failed:\n${errors.join('\n')}`);
89
99
  }
@@ -132,4 +142,4 @@ export class EN16931Validator {
132
142
  throw new Error(`EN16931 XML validation failed:\n${errors.join('\n')}`);
133
143
  }
134
144
  }
135
- }
145
+ }
@@ -60,7 +60,8 @@ export class MainValidator {
60
60
  this.schematronEnabled = true;
61
61
  console.log(`Schematron validation enabled for ${standard} ${format}`);
62
62
  } catch (error) {
63
- console.warn(`Failed to initialize Schematron: ${error.message}`);
63
+ const errorMessage = error instanceof Error ? error.message : String(error);
64
+ console.warn(`Failed to initialize Schematron: ${errorMessage}`);
64
65
  }
65
66
  }
66
67
 
@@ -121,7 +122,8 @@ export class MainValidator {
121
122
  );
122
123
  results.push(...schematronResults);
123
124
  } catch (error) {
124
- console.warn(`Schematron validation error: ${error.message}`);
125
+ const errorMessage = error instanceof Error ? error.message : String(error);
126
+ console.warn(`Schematron validation error: ${errorMessage}`);
125
127
  }
126
128
  }
127
129
 
@@ -402,4 +404,4 @@ export async function createValidator(
402
404
  }
403
405
 
404
406
  // Export for convenience
405
- export type { ValidationReport, ValidationResult, ValidationOptions } from './validation.types.js';
407
+ export type { ValidationReport, ValidationResult, ValidationOptions } from './validation.types.js';
@@ -139,7 +139,8 @@ export class SchematronDownloader {
139
139
  console.log(`Successfully downloaded: ${fileName}`);
140
140
  return filePath;
141
141
  } catch (error) {
142
- throw new Error(`Failed to download ${source.name}: ${error.message}`);
142
+ const errorMessage = error instanceof Error ? error.message : String(error);
143
+ throw new Error(`Failed to download ${source.name}: ${errorMessage}`);
143
144
  }
144
145
  }
145
146
 
@@ -160,7 +161,8 @@ export class SchematronDownloader {
160
161
  const path = await this.download(source);
161
162
  paths.push(path);
162
163
  } catch (error) {
163
- console.warn(`Failed to download ${source.name}: ${error.message}`);
164
+ const errorMessage = error instanceof Error ? error.message : String(error);
165
+ console.warn(`Failed to download ${source.name}: ${errorMessage}`);
164
166
  }
165
167
  }
166
168
 
@@ -209,7 +211,8 @@ export class SchematronDownloader {
209
211
  }
210
212
  }
211
213
  } catch (error) {
212
- console.warn(`Failed to list cached files: ${error.message}`);
214
+ const errorMessage = error instanceof Error ? error.message : String(error);
215
+ console.warn(`Failed to list cached files: ${errorMessage}`);
213
216
  }
214
217
 
215
218
  return files;
@@ -230,7 +233,8 @@ export class SchematronDownloader {
230
233
 
231
234
  console.log('Schematron cache cleared');
232
235
  } catch (error) {
233
- console.warn(`Failed to clear cache: ${error.message}`);
236
+ const errorMessage = error instanceof Error ? error.message : String(error);
237
+ console.warn(`Failed to clear cache: ${errorMessage}`);
234
238
  }
235
239
  }
236
240
 
@@ -296,9 +300,10 @@ export async function downloadISOSkeletons(targetDir: string = 'assets_downloade
296
300
 
297
301
  console.log(`Downloaded: ${name}`);
298
302
  } catch (error) {
299
- console.warn(`Failed to download ${name}: ${error.message}`);
303
+ const errorMessage = error instanceof Error ? error.message : String(error);
304
+ console.warn(`Failed to download ${name}: ${errorMessage}`);
300
305
  }
301
306
  }
302
307
 
303
308
  console.log('ISO Schematron skeleton download complete');
304
- }
309
+ }
@@ -146,7 +146,8 @@ export class IntegratedValidator {
146
146
  });
147
147
  results.push(...schematronResults);
148
148
  } catch (error) {
149
- console.warn(`Schematron validation failed: ${error.message}`);
149
+ const errorMessage = error instanceof Error ? error.message : String(error);
150
+ console.warn(`Schematron validation failed: ${errorMessage}`);
150
151
  }
151
152
  }
152
153
 
@@ -224,7 +225,8 @@ export class IntegratedValidator {
224
225
  try {
225
226
  await this.loadSchematron('EN16931', format);
226
227
  } catch (error) {
227
- console.warn(`Could not load Schematron: ${error.message}`);
228
+ const errorMessage = error instanceof Error ? error.message : String(error);
229
+ console.warn(`Could not load Schematron: ${errorMessage}`);
228
230
  }
229
231
  }
230
232
 
@@ -278,8 +280,9 @@ export async function createStandardValidator(
278
280
  try {
279
281
  await validator.loadSchematron(standard, format);
280
282
  } catch (error) {
281
- console.warn(`Schematron not available for ${standard} ${format}: ${error.message}`);
283
+ const errorMessage = error instanceof Error ? error.message : String(error);
284
+ console.warn(`Schematron not available for ${standard} ${format}: ${errorMessage}`);
282
285
  }
283
286
 
284
287
  return validator;
285
- }
288
+ }
@@ -1,5 +1,4 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import * as SaxonJS from 'saxon-js';
3
2
  import type { ValidationResult } from './validation.types.js';
4
3
 
5
4
  /**
@@ -30,9 +29,7 @@ export class SchematronValidator {
30
29
  */
31
30
  public async loadSchematron(source: string, isFilePath: boolean = true): Promise<void> {
32
31
  if (isFilePath) {
33
- // Load from file
34
- const smartfile = await import('@push.rocks/smartfile');
35
- this.schematronRules = await smartfile.SmartFile.fromFilePath(source).then(f => f.contentBuffer.toString());
32
+ this.schematronRules = await plugins.fs.readFile(source, 'utf-8');
36
33
  } else {
37
34
  // Use provided string
38
35
  this.schematronRules = source;
@@ -58,14 +55,15 @@ export class SchematronValidator {
58
55
  const xslt = this.generateXSLTFromSchematron(this.schematronRules);
59
56
 
60
57
  // Compile the XSLT with Saxon-JS
61
- this.compiledStylesheet = await SaxonJS.compile({
58
+ this.compiledStylesheet = await plugins.SaxonJS.compile({
62
59
  stylesheetText: xslt,
63
60
  warnings: 'silent'
64
61
  });
65
62
 
66
63
  this.isCompiled = true;
67
64
  } catch (error) {
68
- throw new Error(`Failed to compile Schematron: ${error.message}`);
65
+ const errorMessage = error instanceof Error ? error.message : String(error);
66
+ throw new Error(`Failed to compile Schematron: ${errorMessage}`);
69
67
  }
70
68
  }
71
69
 
@@ -87,7 +85,7 @@ export class SchematronValidator {
87
85
 
88
86
  try {
89
87
  // Transform the XML with the compiled Schematron XSLT
90
- const transformResult = await SaxonJS.transform({
88
+ const transformResult = await plugins.SaxonJS.transform({
91
89
  stylesheetInternal: this.compiledStylesheet,
92
90
  sourceText: xmlContent,
93
91
  destination: 'serialized',
@@ -108,11 +106,12 @@ export class SchematronValidator {
108
106
 
109
107
  return results;
110
108
  } catch (error) {
109
+ const errorMessage = error instanceof Error ? error.message : String(error);
111
110
  results.push({
112
111
  ruleId: 'SCHEMATRON-ERROR',
113
112
  source: 'SCHEMATRON',
114
113
  severity: 'error',
115
- message: `Schematron validation failed: ${error.message}`,
114
+ message: `Schematron validation failed: ${errorMessage}`,
116
115
  btReference: undefined,
117
116
  bgReference: undefined
118
117
  });
@@ -323,7 +322,8 @@ export class HybridValidator {
323
322
  try {
324
323
  results.push(...validator.validate(xmlContent));
325
324
  } catch (error) {
326
- console.warn(`TS validator failed: ${error.message}`);
325
+ const errorMessage = error instanceof Error ? error.message : String(error);
326
+ console.warn(`TS validator failed: ${errorMessage}`);
327
327
  }
328
328
  }
329
329
 
@@ -333,7 +333,8 @@ export class HybridValidator {
333
333
  const schematronResults = await this.schematronValidator.validate(xmlContent, options);
334
334
  results.push(...schematronResults);
335
335
  } catch (error) {
336
- console.warn(`Schematron validation failed: ${error.message}`);
336
+ const errorMessage = error instanceof Error ? error.message : String(error);
337
+ console.warn(`Schematron validation failed: ${errorMessage}`);
337
338
  }
338
339
  }
339
340
 
@@ -345,4 +346,4 @@ export class HybridValidator {
345
346
  return true;
346
347
  });
347
348
  }
348
- }
349
+ }
package/ts/index.ts CHANGED
@@ -144,11 +144,12 @@ export function validateXml(
144
144
  const validator = ValidatorFactory.createValidator(xml);
145
145
  return validator.validate(level);
146
146
  } catch (error) {
147
+ const errorMessage = error instanceof Error ? error.message : String(error);
147
148
  return {
148
149
  valid: false,
149
150
  errors: [{
150
151
  code: 'VAL-ERROR',
151
- message: `Validation error: ${error.message}`
152
+ message: `Validation error: ${errorMessage}`
152
153
  }],
153
154
  level
154
155
  };