@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.
- package/.smartconfig.json +59 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +14 -5
- package/dist_ts/einvoice.js +173 -66
- 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/cii/facturx/facturx.encoder.js +25 -16
- 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/associated.extractor.js +3 -2
- package/dist_ts/formats/pdf/extractors/base.extractor.d.ts +1 -1
- package/dist_ts/formats/pdf/extractors/base.extractor.js +50 -52
- package/dist_ts/formats/pdf/extractors/standard.extractor.js +3 -2
- package/dist_ts/formats/pdf/extractors/text.extractor.js +19 -22
- package/dist_ts/formats/pdf/pdf.embedder.js +3 -2
- package/dist_ts/formats/pdf/pdf.extractor.d.ts +2 -1
- package/dist_ts/formats/pdf/pdf.extractor.js +20 -10
- 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.d.ts +0 -1
- package/dist_ts/formats/validation/schematron.downloader.js +12 -7
- package/dist_ts/formats/validation/schematron.integration.js +16 -6
- package/dist_ts/formats/validation/schematron.validator.d.ts +9 -11
- package/dist_ts/formats/validation/schematron.validator.js +70 -72
- package/dist_ts/formats/validation/schematron.worker.d.ts +7 -6
- package/dist_ts/formats/validation/schematron.worker.js +48 -42
- 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 -6
- package/dist_ts/plugins.js +3 -12
- package/dist_ts/vendor/pako.d.ts +5 -0
- package/dist_ts/vendor/pako.js +3 -0
- package/dist_ts/vendor/pdflib.d.ts +2 -0
- package/dist_ts/vendor/pdflib.js +9 -0
- package/dist_ts/vendor/saxonjs.d.ts +23 -0
- package/dist_ts/vendor/saxonjs.js +9 -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 +21 -15
- package/readme.hints.md +24 -8
- package/readme.md +195 -328
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +156 -33
- 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/cii/facturx/facturx.encoder.ts +25 -15
- 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/pdf/extractors/associated.extractor.ts +2 -1
- package/ts/formats/pdf/extractors/base.extractor.ts +3 -2
- package/ts/formats/pdf/extractors/standard.extractor.ts +2 -1
- package/ts/formats/pdf/pdf.embedder.ts +6 -3
- package/ts/formats/pdf/pdf.extractor.ts +24 -16
- 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 -8
- package/ts/formats/validation/schematron.integration.ts +11 -5
- package/ts/formats/validation/schematron.validator.ts +79 -79
- package/ts/formats/validation/schematron.worker.ts +59 -41
- package/ts/index.ts +2 -1
- package/ts/interfaces/common.ts +1 -3
- package/ts/plugins.ts +2 -29
- package/ts/readme.md +76 -0
- package/ts/vendor/modules.d.ts +32 -0
- package/ts/vendor/pako.ts +8 -0
- package/ts/vendor/pdflib.ts +12 -0
- package/ts/vendor/saxonjs.ts +36 -0
- package/ts/vendor/xmldom.ts +27 -0
- package/npmextra.json +0 -35
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } from '../../../plugins.js';
|
|
2
1
|
import { BaseXMLExtractor } from './base.extractor.js';
|
|
2
|
+
import { loadPdfLib } from '../../../vendor/pdflib.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Associated files extractor for PDF/A-3 documents
|
|
@@ -14,6 +14,7 @@ export class AssociatedFilesExtractor extends BaseXMLExtractor {
|
|
|
14
14
|
*/
|
|
15
15
|
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
|
|
16
16
|
try {
|
|
17
|
+
const { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } = await loadPdfLib();
|
|
17
18
|
const pdfDoc = await PDFDocument.load(pdfBuffer);
|
|
18
19
|
|
|
19
20
|
// Try to find associated files via the AF entry in the catalog
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PDFRawStream } from 'pdf-lib';
|
|
2
|
+
import { pako } from '../../../plugins.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Base class for PDF XML extractors with common functionality
|
|
@@ -352,4 +353,4 @@ export abstract class BaseXMLExtractor {
|
|
|
352
353
|
(content.includes('<?xml') ||
|
|
353
354
|
this.knownFormats.some(format => content.includes(format)));
|
|
354
355
|
}
|
|
355
|
-
}
|
|
356
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } from '../../../plugins.js';
|
|
2
1
|
import { BaseXMLExtractor } from './base.extractor.js';
|
|
2
|
+
import { loadPdfLib } from '../../../vendor/pdflib.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Standard PDF XML extractor that extracts XML from embedded files
|
|
@@ -13,6 +13,7 @@ export class StandardXMLExtractor extends BaseXMLExtractor {
|
|
|
13
13
|
*/
|
|
14
14
|
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
|
|
15
15
|
try {
|
|
16
|
+
const { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } = await loadPdfLib();
|
|
16
17
|
const pdfDoc = await PDFDocument.load(pdfBuffer);
|
|
17
18
|
|
|
18
19
|
// Get the document's metadata dictionary
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { PDFDocument
|
|
1
|
+
import type { PDFDocument as TPdfDocument } from 'pdf-lib';
|
|
2
2
|
import type { IPdf } from '../../interfaces/common.js';
|
|
3
|
+
import { loadPdfLib } from '../../vendor/pdflib.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Error types for PDF embedding operations
|
|
@@ -57,8 +58,10 @@ export class PDFEmbedder {
|
|
|
57
58
|
// Ensure buffer is Uint8Array
|
|
58
59
|
const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
|
|
59
60
|
|
|
61
|
+
const { PDFDocument, AFRelationship } = await loadPdfLib();
|
|
62
|
+
|
|
60
63
|
// Load the PDF
|
|
61
|
-
let pdfDoc:
|
|
64
|
+
let pdfDoc: TPdfDocument;
|
|
62
65
|
try {
|
|
63
66
|
pdfDoc = await PDFDocument.load(pdfBufferArray, {
|
|
64
67
|
ignoreEncryption: true, // Try to load encrypted PDFs
|
|
@@ -239,4 +242,4 @@ export class PDFEmbedder {
|
|
|
239
242
|
}
|
|
240
243
|
};
|
|
241
244
|
}
|
|
242
|
-
}
|
|
245
|
+
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BaseXMLExtractor,
|
|
3
|
-
StandardXMLExtractor,
|
|
4
|
-
AssociatedFilesExtractor,
|
|
5
|
-
TextXMLExtractor
|
|
6
|
-
} from './extractors/index.js';
|
|
1
|
+
import type { BaseXMLExtractor } from './extractors/base.extractor.js';
|
|
7
2
|
import { FormatDetector } from '../utils/format.detector.js';
|
|
8
3
|
import { InvoiceFormat } from '../../interfaces/common.js';
|
|
9
4
|
|
|
@@ -36,18 +31,30 @@ export interface PDFExtractResult {
|
|
|
36
31
|
* Uses multiple specialized extractors in sequence to maximize success rate
|
|
37
32
|
*/
|
|
38
33
|
export class PDFExtractor {
|
|
39
|
-
private extractors
|
|
34
|
+
private extractors?: BaseXMLExtractor[];
|
|
40
35
|
|
|
41
36
|
/**
|
|
42
37
|
* Constructor initializes the chain of extractors
|
|
43
38
|
*/
|
|
44
|
-
constructor() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
constructor() {}
|
|
40
|
+
|
|
41
|
+
private async getExtractors(): Promise<BaseXMLExtractor[]> {
|
|
42
|
+
if (!this.extractors) {
|
|
43
|
+
const [standardModule, associatedModule, textModule] = await Promise.all([
|
|
44
|
+
import('./extractors/standard.extractor.js'),
|
|
45
|
+
import('./extractors/associated.extractor.js'),
|
|
46
|
+
import('./extractors/text.extractor.js')
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Add extractors in order of preference/likelihood of success
|
|
50
|
+
this.extractors = [
|
|
51
|
+
new standardModule.StandardXMLExtractor(), // Standard PDF/A-3 embedded files
|
|
52
|
+
new associatedModule.AssociatedFilesExtractor(), // Associated files (ZUGFeRD v1, some Factur-X)
|
|
53
|
+
new textModule.TextXMLExtractor() // Text-based extraction (fallback)
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return this.extractors;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
@@ -69,7 +76,8 @@ export class PDFExtractor {
|
|
|
69
76
|
const pdfBufferArray = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
|
|
70
77
|
|
|
71
78
|
// Try each extractor in sequence
|
|
72
|
-
|
|
79
|
+
const extractors = await this.getExtractors();
|
|
80
|
+
for (const extractor of extractors) {
|
|
73
81
|
const extractorName = extractor.constructor.name;
|
|
74
82
|
console.log(`Trying extraction with ${extractorName}...`);
|
|
75
83
|
|
|
@@ -138,4 +146,4 @@ export class PDFExtractor {
|
|
|
138
146
|
}
|
|
139
147
|
};
|
|
140
148
|
}
|
|
141
|
-
}
|
|
149
|
+
}
|
|
@@ -169,17 +169,45 @@ export class SemanticModelAdapter {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Set payment options
|
|
172
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
model.paymentInstructions.paymentAccountIdentifier ||
|
|
174
|
+
model.paymentInstructions.paymentMeansText ||
|
|
175
|
+
model.paymentInstructions.paymentServiceProviderIdentifier
|
|
176
|
+
) {
|
|
173
177
|
invoice.paymentOptions = {
|
|
174
|
-
|
|
178
|
+
description: model.paymentInstructions.paymentMeansText,
|
|
179
|
+
sepaConnection: {
|
|
180
|
+
iban: model.paymentInstructions.paymentAccountIdentifier || '',
|
|
181
|
+
bic: model.paymentInstructions.paymentServiceProviderIdentifier || '',
|
|
182
|
+
institution: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
183
|
+
},
|
|
184
|
+
payPal: { email: '' }
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
invoice.metadata = {
|
|
188
|
+
...invoice.metadata,
|
|
189
|
+
paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
|
|
190
|
+
paymentAccount: {
|
|
175
191
|
iban: model.paymentInstructions.paymentAccountIdentifier,
|
|
176
|
-
|
|
192
|
+
accountName: model.paymentInstructions.paymentAccountName,
|
|
193
|
+
bankId: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
177
194
|
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
extensions: {
|
|
196
|
+
...invoice.metadata?.extensions,
|
|
197
|
+
paymentMeans: {
|
|
198
|
+
paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
|
|
199
|
+
paymentMeansText: model.paymentInstructions.paymentMeansText,
|
|
200
|
+
remittanceInformation: model.paymentInstructions.remittanceInformation
|
|
201
|
+
},
|
|
202
|
+
paymentAccount: {
|
|
203
|
+
iban: model.paymentInstructions.paymentAccountIdentifier,
|
|
204
|
+
accountName: model.paymentInstructions.paymentAccountName,
|
|
205
|
+
bankId: model.paymentInstructions.paymentServiceProviderIdentifier,
|
|
206
|
+
bic: model.paymentInstructions.paymentServiceProviderIdentifier,
|
|
207
|
+
institutionName: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
208
|
+
}
|
|
181
209
|
}
|
|
182
|
-
}
|
|
210
|
+
};
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
// Set extensions
|
|
@@ -376,15 +404,21 @@ export class SemanticModelAdapter {
|
|
|
376
404
|
*/
|
|
377
405
|
private mapPaymentInstructions(invoice: EInvoice): PaymentInstructions {
|
|
378
406
|
const paymentMeans = invoice.metadata?.extensions?.paymentMeans;
|
|
379
|
-
const paymentAccount = invoice.metadata?.extensions?.paymentAccount;
|
|
407
|
+
const paymentAccount = invoice.metadata?.extensions?.paymentAccount || invoice.metadata?.paymentAccount;
|
|
408
|
+
const sepaConnection = invoice.paymentOptions?.sepaConnection;
|
|
380
409
|
|
|
381
410
|
return {
|
|
382
|
-
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30',
|
|
383
|
-
paymentMeansText: paymentMeans?.paymentMeansText,
|
|
411
|
+
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || invoice.metadata?.paymentMeansCode || '30',
|
|
412
|
+
paymentMeansText: paymentMeans?.paymentMeansText || invoice.paymentOptions?.description,
|
|
384
413
|
remittanceInformation: paymentMeans?.remittanceInformation,
|
|
385
|
-
paymentAccountIdentifier: paymentAccount?.iban,
|
|
414
|
+
paymentAccountIdentifier: paymentAccount?.iban || sepaConnection?.iban,
|
|
386
415
|
paymentAccountName: paymentAccount?.accountName,
|
|
387
|
-
paymentServiceProviderIdentifier:
|
|
416
|
+
paymentServiceProviderIdentifier:
|
|
417
|
+
paymentAccount?.bic ||
|
|
418
|
+
paymentAccount?.institutionName ||
|
|
419
|
+
paymentAccount?.bankId ||
|
|
420
|
+
sepaConnection?.bic ||
|
|
421
|
+
sepaConnection?.institution
|
|
388
422
|
};
|
|
389
423
|
}
|
|
390
424
|
|
|
@@ -397,10 +431,10 @@ export class SemanticModelAdapter {
|
|
|
397
431
|
taxExclusiveAmount: invoice.totalNet,
|
|
398
432
|
taxInclusiveAmount: invoice.totalGross,
|
|
399
433
|
allowanceTotalAmount: invoice.metadata?.extensions?.documentAllowances?.reduce(
|
|
400
|
-
(sum, a) => sum + a.amount, 0
|
|
434
|
+
(sum: number, a: { amount: number }) => sum + a.amount, 0
|
|
401
435
|
),
|
|
402
436
|
chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
|
|
403
|
-
(sum, c) => sum + c.amount, 0
|
|
437
|
+
(sum: number, c: { amount: number }) => sum + c.amount, 0
|
|
404
438
|
),
|
|
405
439
|
prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
|
|
406
440
|
roundingAmount: invoice.metadata?.extensions?.roundingAmount,
|
|
@@ -597,4 +631,4 @@ export class SemanticModelAdapter {
|
|
|
597
631
|
|
|
598
632
|
return errors;
|
|
599
633
|
}
|
|
600
|
-
}
|
|
634
|
+
}
|
|
@@ -90,7 +90,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// BR-08: An Invoice shall contain the Seller postal address (BG-5).
|
|
93
|
-
const
|
|
93
|
+
const sellerAddressResult = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc);
|
|
94
|
+
const sellerAddress = Array.isArray(sellerAddressResult) ? sellerAddressResult[0] : null;
|
|
94
95
|
if (!sellerAddress || !this.exists('.//cbc:IdentificationCode', sellerAddress)) {
|
|
95
96
|
this.addError('BR-08', 'An Invoice shall contain the Seller postal address', '//cac:AccountingSupplierParty//cac:PostalAddress');
|
|
96
97
|
valid = false;
|
|
@@ -103,7 +104,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// BR-10: An Invoice shall contain the Buyer postal address (BG-8).
|
|
106
|
-
const
|
|
107
|
+
const buyerAddressResult = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc);
|
|
108
|
+
const buyerAddress = Array.isArray(buyerAddressResult) ? buyerAddressResult[0] : null;
|
|
107
109
|
if (!buyerAddress || !this.exists('.//cbc:IdentificationCode', buyerAddress)) {
|
|
108
110
|
this.addError('BR-10', 'An Invoice shall contain the Buyer postal address', '//cac:AccountingCustomerParty//cac:PostalAddress');
|
|
109
111
|
valid = false;
|
|
@@ -213,4 +215,4 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
213
215
|
|
|
214
216
|
return valid;
|
|
215
217
|
}
|
|
216
|
-
}
|
|
218
|
+
}
|
|
@@ -263,6 +263,11 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
263
263
|
* @param invoice Invoice data
|
|
264
264
|
*/
|
|
265
265
|
private addPaymentMeans(doc: Document, parentElement: Element, invoice: TInvoice): void {
|
|
266
|
+
const paymentOptions = invoice.paymentOptions;
|
|
267
|
+
if (!paymentOptions) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
266
271
|
const paymentMeansNode = doc.createElement('cac:PaymentMeans');
|
|
267
272
|
parentElement.appendChild(paymentMeansNode);
|
|
268
273
|
|
|
@@ -276,26 +281,26 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
276
281
|
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
|
|
277
282
|
|
|
278
283
|
// Add payment channel code if available
|
|
279
|
-
if (
|
|
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
|
+
}
|