@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.
- package/{npmextra.json → .smartconfig.json} +12 -6
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +14 -5
- package/dist_ts/einvoice.js +165 -60
- package/dist_ts/errors.js +14 -4
- package/dist_ts/formats/base/base.decoder.d.ts +4 -0
- package/dist_ts/formats/base/base.decoder.js +9 -1
- package/dist_ts/formats/base/base.validator.js +3 -2
- package/dist_ts/formats/cii/cii.decoder.d.ts +1 -0
- package/dist_ts/formats/cii/cii.decoder.js +20 -10
- package/dist_ts/formats/cii/cii.encoder.js +2 -5
- package/dist_ts/formats/cii/cii.validator.d.ts +1 -1
- package/dist_ts/formats/cii/cii.validator.js +19 -12
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +3 -3
- package/dist_ts/formats/factories/decoder.factory.d.ts +3 -1
- package/dist_ts/formats/factories/decoder.factory.js +4 -3
- package/dist_ts/formats/factories/validator.factory.d.ts +4 -1
- package/dist_ts/formats/factories/validator.factory.js +11 -8
- package/dist_ts/formats/pdf/extractors/base.extractor.js +49 -51
- package/dist_ts/formats/pdf/extractors/text.extractor.js +19 -22
- package/dist_ts/formats/pdf/pdf.extractor.js +2 -2
- package/dist_ts/formats/semantic/semantic.adapter.js +42 -12
- package/dist_ts/formats/semantic/semantic.validator.js +3 -1
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -3
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +11 -7
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +18 -8
- package/dist_ts/formats/ubl/ubl.validator.d.ts +1 -1
- package/dist_ts/formats/ubl/ubl.validator.js +17 -10
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +37 -29
- package/dist_ts/formats/utils/currency.calculator.decimal.js +4 -1
- package/dist_ts/formats/utils/currency.utils.js +4 -1
- package/dist_ts/formats/utils/decimal.js +9 -2
- package/dist_ts/formats/utils/format.detector.js +47 -17
- package/dist_ts/formats/validation/codelist.validator.js +2 -4
- package/dist_ts/formats/validation/conformance.harness.js +13 -6
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -4
- package/dist_ts/formats/validation/en16931.validator.d.ts +6 -0
- package/dist_ts/formats/validation/en16931.validator.js +18 -12
- package/dist_ts/formats/validation/facturx.validator.js +147 -148
- package/dist_ts/formats/validation/integrated.validator.js +12 -4
- package/dist_ts/formats/validation/peppol.validator.js +2 -1
- package/dist_ts/formats/validation/schematron.downloader.js +13 -6
- package/dist_ts/formats/validation/schematron.integration.js +12 -5
- package/dist_ts/formats/validation/schematron.validator.js +17 -13
- package/dist_ts/formats/validation/schematron.worker.js +6 -5
- package/dist_ts/formats/validation/vat-categories.validator.js +3 -4
- package/dist_ts/formats/validation/xrechnung.validator.js +7 -7
- package/dist_ts/index.js +3 -2
- package/dist_ts/interfaces/common.js +1 -2
- package/dist_ts/plugins.d.ts +3 -4
- package/dist_ts/plugins.js +4 -5
- package/dist_ts/vendor/pako.d.ts +5 -0
- package/dist_ts/vendor/pako.js +3 -0
- package/dist_ts/vendor/saxonjs.d.ts +22 -0
- package/dist_ts/vendor/saxonjs.js +3 -0
- package/dist_ts/vendor/xmldom.d.ts +20 -0
- package/dist_ts/vendor/xmldom.js +5 -0
- package/dist_ts_install/download-schematron.d.ts +0 -3
- package/dist_ts_install/download-schematron.js +20 -11
- package/dist_ts_install/download-test-samples.d.ts +0 -3
- package/dist_ts_install/download-test-samples.js +4 -2
- package/dist_ts_install/download-xrechnung-rules.js +2 -1
- package/dist_ts_install/index.d.ts +0 -5
- package/dist_ts_install/index.js +26 -12
- package/license +21 -0
- package/package.json +25 -23
- package/readme.hints.md +24 -8
- package/readme.md +195 -328
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +148 -26
- package/ts/formats/base/base.decoder.ts +7 -0
- package/ts/formats/cii/cii.decoder.ts +17 -8
- package/ts/formats/cii/cii.validator.ts +18 -13
- package/ts/formats/converters/xml-to-einvoice.converter.ts +4 -5
- package/ts/formats/factories/decoder.factory.ts +7 -2
- package/ts/formats/factories/validator.factory.ts +14 -8
- package/ts/formats/semantic/semantic.adapter.ts +49 -15
- package/ts/formats/ubl/en16931.ubl.validator.ts +5 -3
- package/ts/formats/ubl/generic/ubl.encoder.ts +12 -7
- package/ts/formats/ubl/ubl.decoder.ts +18 -9
- package/ts/formats/ubl/ubl.validator.ts +17 -12
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +41 -28
- package/ts/formats/utils/format.detector.ts +57 -19
- package/ts/formats/validation/conformance.harness.ts +6 -4
- package/ts/formats/validation/en16931.validator.ts +20 -10
- package/ts/formats/validation/integrated.validator.ts +5 -3
- package/ts/formats/validation/schematron.downloader.ts +11 -6
- package/ts/formats/validation/schematron.integration.ts +7 -4
- package/ts/formats/validation/schematron.validator.ts +12 -11
- package/ts/index.ts +2 -1
- package/ts/interfaces/common.ts +1 -3
- package/ts/plugins.ts +3 -4
- package/ts/readme.md +55 -0
- package/ts/vendor/modules.d.ts +32 -0
- package/ts/vendor/pako.ts +8 -0
- package/ts/vendor/saxonjs.ts +28 -0
- 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 (
|
|
280
|
-
this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote',
|
|
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 (
|
|
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',
|
|
296
|
+
this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', paymentOptions.sepaConnection.iban);
|
|
292
297
|
|
|
293
298
|
// Add financial institution information if BIC is available
|
|
294
|
-
if (
|
|
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',
|
|
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
|
-
|
|
25
|
+
this.doc = ublParser.parseFromString(xml, 'application/xml');
|
|
26
|
+
|
|
27
|
+
// Set up namespaces for XPath queries
|
|
28
|
+
this.namespaces = ublNamespaces;
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cac: UBL_NAMESPACES.CAC
|
|
24
|
-
};
|
|
30
|
+
// Create XPath selector with namespaces
|
|
31
|
+
this.select = ublSelect;
|
|
32
|
+
}
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
12
|
-
protected namespaces
|
|
13
|
-
protected select
|
|
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
|
-
//
|
|
20
|
-
this.doc =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
*
|
|
30
|
+
* Collects mandatory EN16931 field errors without throwing.
|
|
31
31
|
* @param invoice The invoice object to validate
|
|
32
|
-
* @
|
|
32
|
+
* @returns List of validation error messages
|
|
33
33
|
*/
|
|
34
|
-
public static
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
152
|
+
message: `Validation error: ${errorMessage}`
|
|
152
153
|
}],
|
|
153
154
|
level
|
|
154
155
|
};
|