@fin.cx/einvoice 5.0.2
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/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.decoder.d.ts +40 -0
- package/dist_ts/classes.decoder.js +320 -0
- package/dist_ts/classes.encoder.d.ts +51 -0
- package/dist_ts/classes.encoder.js +293 -0
- package/dist_ts/classes.xinvoice.d.ts +136 -0
- package/dist_ts/classes.xinvoice.js +352 -0
- package/dist_ts/einvoice.d.ts +217 -0
- package/dist_ts/einvoice.js +505 -0
- package/dist_ts/errors.d.ts +121 -0
- package/dist_ts/errors.js +241 -0
- package/dist_ts/formats/base/base.decoder.d.ts +32 -0
- package/dist_ts/formats/base/base.decoder.js +52 -0
- package/dist_ts/formats/base/base.encoder.d.ts +13 -0
- package/dist_ts/formats/base/base.encoder.js +7 -0
- package/dist_ts/formats/base/base.validator.d.ts +44 -0
- package/dist_ts/formats/base/base.validator.js +39 -0
- package/dist_ts/formats/base.decoder.d.ts +19 -0
- package/dist_ts/formats/base.decoder.js +130 -0
- package/dist_ts/formats/base.validator.d.ts +44 -0
- package/dist_ts/formats/base.validator.js +39 -0
- package/dist_ts/formats/cii/cii.decoder.d.ts +61 -0
- package/dist_ts/formats/cii/cii.decoder.js +111 -0
- package/dist_ts/formats/cii/cii.encoder.d.ts +43 -0
- package/dist_ts/formats/cii/cii.encoder.js +48 -0
- package/dist_ts/formats/cii/cii.types.d.ts +31 -0
- package/dist_ts/formats/cii/cii.types.js +40 -0
- package/dist_ts/formats/cii/cii.validator.d.ts +56 -0
- package/dist_ts/formats/cii/cii.validator.js +146 -0
- package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +43 -0
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +207 -0
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +82 -0
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +400 -0
- package/dist_ts/formats/cii/facturx/facturx.types.d.ts +10 -0
- package/dist_ts/formats/cii/facturx/facturx.types.js +15 -0
- package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +32 -0
- package/dist_ts/formats/cii/facturx/facturx.validator.js +138 -0
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +43 -0
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +218 -0
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +97 -0
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +550 -0
- package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +10 -0
- package/dist_ts/formats/cii/zugferd/zugferd.types.js +15 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +49 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +229 -0
- package/dist_ts/formats/cii/zugferd/zugferd.validator.d.ts +16 -0
- package/dist_ts/formats/cii/zugferd/zugferd.validator.js +29 -0
- package/dist_ts/formats/decoder.factory.d.ts +15 -0
- package/dist_ts/formats/decoder.factory.js +46 -0
- package/dist_ts/formats/factories/decoder.factory.d.ts +13 -0
- package/dist_ts/formats/factories/decoder.factory.js +56 -0
- package/dist_ts/formats/factories/encoder.factory.d.ts +14 -0
- package/dist_ts/formats/factories/encoder.factory.js +40 -0
- package/dist_ts/formats/factories/validator.factory.d.ts +12 -0
- package/dist_ts/formats/factories/validator.factory.js +110 -0
- package/dist_ts/formats/factorx.decoder.d.ts +18 -0
- package/dist_ts/formats/factorx.decoder.js +178 -0
- package/dist_ts/formats/factorx.encoder.d.ts +51 -0
- package/dist_ts/formats/factorx.encoder.js +293 -0
- package/dist_ts/formats/facturx.decoder.d.ts +18 -0
- package/dist_ts/formats/facturx.decoder.js +210 -0
- package/dist_ts/formats/facturx.encoder.d.ts +51 -0
- package/dist_ts/formats/facturx.encoder.js +293 -0
- package/dist_ts/formats/facturx.validator.d.ts +67 -0
- package/dist_ts/formats/facturx.validator.js +266 -0
- package/dist_ts/formats/pdf/extractors/associated.extractor.d.ts +14 -0
- package/dist_ts/formats/pdf/extractors/associated.extractor.js +68 -0
- package/dist_ts/formats/pdf/extractors/base.extractor.d.ts +92 -0
- package/dist_ts/formats/pdf/extractors/base.extractor.js +319 -0
- package/dist_ts/formats/pdf/extractors/index.d.ts +4 -0
- package/dist_ts/formats/pdf/extractors/index.js +5 -0
- package/dist_ts/formats/pdf/extractors/standard.extractor.d.ts +13 -0
- package/dist_ts/formats/pdf/extractors/standard.extractor.js +74 -0
- package/dist_ts/formats/pdf/extractors/text.extractor.d.ts +45 -0
- package/dist_ts/formats/pdf/extractors/text.extractor.js +151 -0
- package/dist_ts/formats/pdf/pdf.embedder.d.ts +69 -0
- package/dist_ts/formats/pdf/pdf.embedder.js +183 -0
- package/dist_ts/formats/pdf/pdf.extractor.d.ts +49 -0
- package/dist_ts/formats/pdf/pdf.extractor.js +98 -0
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +40 -0
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +324 -0
- package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +18 -0
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +169 -0
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +151 -0
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +892 -0
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +61 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +95 -0
- package/dist_ts/formats/ubl/ubl.encoder.d.ts +38 -0
- package/dist_ts/formats/ubl/ubl.encoder.js +46 -0
- package/dist_ts/formats/ubl/ubl.types.d.ts +16 -0
- package/dist_ts/formats/ubl/ubl.types.js +21 -0
- package/dist_ts/formats/ubl/ubl.validator.d.ts +50 -0
- package/dist_ts/formats/ubl/ubl.validator.js +112 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +34 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +424 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +75 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +555 -0
- package/dist_ts/formats/ubl/xrechnung.validator.d.ts +15 -0
- package/dist_ts/formats/ubl/xrechnung.validator.js +107 -0
- package/dist_ts/formats/ubl.validator.d.ts +82 -0
- package/dist_ts/formats/ubl.validator.js +306 -0
- package/dist_ts/formats/utils/format.detector.d.ts +62 -0
- package/dist_ts/formats/utils/format.detector.js +249 -0
- package/dist_ts/formats/validation/en16931.validator.d.ts +23 -0
- package/dist_ts/formats/validation/en16931.validator.js +119 -0
- package/dist_ts/formats/validator.factory.d.ts +18 -0
- package/dist_ts/formats/validator.factory.js +78 -0
- package/dist_ts/formats/xinvoice.decoder.d.ts +28 -0
- package/dist_ts/formats/xinvoice.decoder.js +332 -0
- package/dist_ts/formats/xinvoice.encoder.d.ts +19 -0
- package/dist_ts/formats/xinvoice.encoder.js +282 -0
- package/dist_ts/index.d.ts +51 -0
- package/dist_ts/index.js +90 -0
- package/dist_ts/interfaces/common.d.ts +79 -0
- package/dist_ts/interfaces/common.js +24 -0
- package/dist_ts/interfaces.d.ts +87 -0
- package/dist_ts/interfaces.js +23 -0
- package/dist_ts/plugins.d.ts +14 -0
- package/dist_ts/plugins.js +31 -0
- package/npmextra.json +35 -0
- package/package.json +71 -0
- package/readme.hints.md +1107 -0
- package/readme.howtofixtests.md +38 -0
- package/readme.literature.md +1 -0
- package/readme.md +998 -0
- package/readme.plan.md +481 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/einvoice.ts +603 -0
- package/ts/errors.ts +341 -0
- package/ts/formats/base/base.decoder.ts +68 -0
- package/ts/formats/base/base.encoder.ts +14 -0
- package/ts/formats/base/base.validator.ts +64 -0
- package/ts/formats/cii/cii.decoder.ts +139 -0
- package/ts/formats/cii/cii.encoder.ts +64 -0
- package/ts/formats/cii/cii.types.ts +44 -0
- package/ts/formats/cii/cii.validator.ts +171 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +243 -0
- package/ts/formats/cii/facturx/facturx.encoder.ts +483 -0
- package/ts/formats/cii/facturx/facturx.types.ts +18 -0
- package/ts/formats/cii/facturx/facturx.validator.ts +180 -0
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +256 -0
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +661 -0
- package/ts/formats/cii/zugferd/zugferd.types.ts +18 -0
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +267 -0
- package/ts/formats/cii/zugferd/zugferd.validator.ts +31 -0
- package/ts/formats/factories/decoder.factory.ts +62 -0
- package/ts/formats/factories/encoder.factory.ts +47 -0
- package/ts/formats/factories/validator.factory.ts +134 -0
- package/ts/formats/pdf/extractors/associated.extractor.ts +78 -0
- package/ts/formats/pdf/extractors/base.extractor.ts +355 -0
- package/ts/formats/pdf/extractors/index.ts +4 -0
- package/ts/formats/pdf/extractors/standard.extractor.ts +86 -0
- package/ts/formats/pdf/extractors/text.extractor.ts +162 -0
- package/ts/formats/pdf/pdf.embedder.ts +242 -0
- package/ts/formats/pdf/pdf.extractor.ts +141 -0
- package/ts/formats/ubl/en16931.ubl.validator.ts +216 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +1041 -0
- package/ts/formats/ubl/ubl.decoder.ts +121 -0
- package/ts/formats/ubl/ubl.encoder.ts +63 -0
- package/ts/formats/ubl/ubl.types.ts +22 -0
- package/ts/formats/ubl/ubl.validator.ts +133 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +471 -0
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +619 -0
- package/ts/formats/ubl/xrechnung.validator.ts +185 -0
- package/ts/formats/utils/format.detector.ts +306 -0
- package/ts/formats/validation/en16931.validator.ts +135 -0
- package/ts/index.ts +164 -0
- package/ts/interfaces/common.ts +90 -0
- package/ts/interfaces.ts +98 -0
- package/ts/plugins.ts +61 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { EN16931UBLValidator } from './en16931.ubl.validator.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* XRechnung-specific validator that extends EN16931 validation
|
|
5
|
+
* Implements additional German CIUS (Core Invoice Usage Specification) rules
|
|
6
|
+
*/
|
|
7
|
+
export class XRechnungValidator extends EN16931UBLValidator {
|
|
8
|
+
/**
|
|
9
|
+
* Validates XRechnung-specific structure requirements
|
|
10
|
+
*/
|
|
11
|
+
protected validateStructure(): boolean {
|
|
12
|
+
// First validate EN16931 structure
|
|
13
|
+
let valid = super.validateStructure();
|
|
14
|
+
|
|
15
|
+
// XRechnung-specific: Check for proper customization ID
|
|
16
|
+
const customizationID = this.getText('//cbc:CustomizationID');
|
|
17
|
+
if (!customizationID || !customizationID.includes('xrechnung')) {
|
|
18
|
+
this.addError(
|
|
19
|
+
'XRECH-STRUCT-1',
|
|
20
|
+
'XRechnung customization ID is missing or invalid',
|
|
21
|
+
'//cbc:CustomizationID'
|
|
22
|
+
);
|
|
23
|
+
valid = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return valid;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates XRechnung-specific business rules
|
|
31
|
+
*/
|
|
32
|
+
protected validateBusinessRules(): boolean {
|
|
33
|
+
// First validate EN16931 business rules
|
|
34
|
+
let valid = super.validateBusinessRules();
|
|
35
|
+
|
|
36
|
+
// BR-DE-1: Payment terms (BT-20) or Payment due date (BT-9) shall be provided.
|
|
37
|
+
if (!this.exists('//cbc:PaymentDueDate') && !this.exists('//cac:PaymentTerms/cbc:Note')) {
|
|
38
|
+
this.addError(
|
|
39
|
+
'BR-DE-1',
|
|
40
|
+
'Payment terms or Payment due date shall be provided',
|
|
41
|
+
'//cac:PaymentTerms'
|
|
42
|
+
);
|
|
43
|
+
valid = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// BR-DE-2: The element "Buyer reference" (BT-10) shall be provided.
|
|
47
|
+
if (!this.exists('//cbc:BuyerReference')) {
|
|
48
|
+
this.addError(
|
|
49
|
+
'BR-DE-2',
|
|
50
|
+
'Buyer reference is required in XRechnung',
|
|
51
|
+
'//cbc:BuyerReference'
|
|
52
|
+
);
|
|
53
|
+
valid = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// BR-DE-5: In Germany, the element "Seller VAT identifier" (BT-31) shall be provided.
|
|
57
|
+
const sellerCountry = this.getText('//cac:AccountingSupplierParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
|
|
58
|
+
if (sellerCountry === 'DE' && !this.exists('//cac:AccountingSupplierParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
|
|
59
|
+
this.addError(
|
|
60
|
+
'BR-DE-5',
|
|
61
|
+
'Seller VAT identifier is required for German sellers',
|
|
62
|
+
'//cac:AccountingSupplierParty//cac:PartyTaxScheme'
|
|
63
|
+
);
|
|
64
|
+
valid = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// BR-DE-6: In Germany, the element "Buyer VAT identifier" (BT-48) shall be provided.
|
|
68
|
+
const buyerCountry = this.getText('//cac:AccountingCustomerParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
|
|
69
|
+
if (buyerCountry === 'DE' && !this.exists('//cac:AccountingCustomerParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
|
|
70
|
+
this.addError(
|
|
71
|
+
'BR-DE-6',
|
|
72
|
+
'Buyer VAT identifier is required for German buyers',
|
|
73
|
+
'//cac:AccountingCustomerParty//cac:PartyTaxScheme'
|
|
74
|
+
);
|
|
75
|
+
valid = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// BR-DE-7: The element "Seller city" (BT-37) shall be provided.
|
|
79
|
+
if (!this.exists('//cac:AccountingSupplierParty//cac:PostalAddress/cbc:CityName')) {
|
|
80
|
+
this.addError(
|
|
81
|
+
'BR-DE-7',
|
|
82
|
+
'Seller city is required',
|
|
83
|
+
'//cac:AccountingSupplierParty//cac:PostalAddress'
|
|
84
|
+
);
|
|
85
|
+
valid = false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// BR-DE-8: The element "Seller post code" (BT-38) shall be provided.
|
|
89
|
+
if (!this.exists('//cac:AccountingSupplierParty//cac:PostalAddress/cbc:PostalZone')) {
|
|
90
|
+
this.addError(
|
|
91
|
+
'BR-DE-8',
|
|
92
|
+
'Seller post code is required',
|
|
93
|
+
'//cac:AccountingSupplierParty//cac:PostalAddress'
|
|
94
|
+
);
|
|
95
|
+
valid = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// BR-DE-9: The element "Buyer city" (BT-52) shall be provided.
|
|
99
|
+
if (!this.exists('//cac:AccountingCustomerParty//cac:PostalAddress/cbc:CityName')) {
|
|
100
|
+
this.addError(
|
|
101
|
+
'BR-DE-9',
|
|
102
|
+
'Buyer city is required',
|
|
103
|
+
'//cac:AccountingCustomerParty//cac:PostalAddress'
|
|
104
|
+
);
|
|
105
|
+
valid = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// BR-DE-10: The element "Buyer post code" (BT-53) shall be provided.
|
|
109
|
+
if (!this.exists('//cac:AccountingCustomerParty//cac:PostalAddress/cbc:PostalZone')) {
|
|
110
|
+
this.addError(
|
|
111
|
+
'BR-DE-10',
|
|
112
|
+
'Buyer post code is required',
|
|
113
|
+
'//cac:AccountingCustomerParty//cac:PostalAddress'
|
|
114
|
+
);
|
|
115
|
+
valid = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// BR-DE-11: The element "Seller contact telephone number" (BT-42) shall be provided.
|
|
119
|
+
if (!this.exists('//cac:AccountingSupplierParty//cac:Contact/cbc:Telephone')) {
|
|
120
|
+
this.addError(
|
|
121
|
+
'BR-DE-11',
|
|
122
|
+
'Seller contact telephone number is required',
|
|
123
|
+
'//cac:AccountingSupplierParty//cac:Contact'
|
|
124
|
+
);
|
|
125
|
+
valid = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// BR-DE-12: The element "Seller contact email address" (BT-43) shall be provided.
|
|
129
|
+
if (!this.exists('//cac:AccountingSupplierParty//cac:Contact/cbc:ElectronicMail')) {
|
|
130
|
+
this.addError(
|
|
131
|
+
'BR-DE-12',
|
|
132
|
+
'Seller contact email address is required',
|
|
133
|
+
'//cac:AccountingSupplierParty//cac:Contact'
|
|
134
|
+
);
|
|
135
|
+
valid = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// BR-DE-13: The element "Buyer electronic address" (BT-49) shall be provided.
|
|
139
|
+
if (!this.exists('//cac:AccountingCustomerParty//cac:Party/cbc:EndpointID')) {
|
|
140
|
+
this.addError(
|
|
141
|
+
'BR-DE-13',
|
|
142
|
+
'Buyer electronic address (EndpointID) is required',
|
|
143
|
+
'//cac:AccountingCustomerParty//cac:Party'
|
|
144
|
+
);
|
|
145
|
+
valid = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// BR-DE-14: The element "Payment means type code" (BT-81) shall be provided.
|
|
149
|
+
if (!this.exists('//cac:PaymentMeans/cbc:PaymentMeansCode')) {
|
|
150
|
+
this.addError(
|
|
151
|
+
'BR-DE-14',
|
|
152
|
+
'Payment means type code is required',
|
|
153
|
+
'//cac:PaymentMeans'
|
|
154
|
+
);
|
|
155
|
+
valid = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// BR-DE-15: The element "Invoice line identifier" (BT-126) shall be provided.
|
|
159
|
+
const invoiceLines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Node[];
|
|
160
|
+
for (let i = 0; i < invoiceLines.length; i++) {
|
|
161
|
+
const line = invoiceLines[i];
|
|
162
|
+
if (!this.exists('./cbc:ID', line)) {
|
|
163
|
+
this.addError(
|
|
164
|
+
'BR-DE-15',
|
|
165
|
+
`Invoice line ${i + 1} is missing identifier`,
|
|
166
|
+
`//cac:InvoiceLine[${i + 1}]`
|
|
167
|
+
);
|
|
168
|
+
valid = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// German VAT ID format validation
|
|
173
|
+
const supplierVatId = this.getText('//cac:AccountingSupplierParty//cbc:CompanyID[../cac:TaxScheme/cbc:ID="VAT"]');
|
|
174
|
+
if (supplierVatId && supplierVatId.startsWith('DE') && !/^DE[0-9]{9}$/.test(supplierVatId)) {
|
|
175
|
+
this.addError(
|
|
176
|
+
'BR-DE-VAT',
|
|
177
|
+
'German VAT ID format is invalid (must be DE followed by 9 digits)',
|
|
178
|
+
'//cac:AccountingSupplierParty//cbc:CompanyID'
|
|
179
|
+
);
|
|
180
|
+
valid = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return valid;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { InvoiceFormat } from '../../interfaces/common.js';
|
|
2
|
+
import { DOMParser, xpath } from '../../plugins.js';
|
|
3
|
+
import { CII_PROFILE_IDS, ZUGFERD_V1_NAMESPACES } from '../cii/cii.types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility class for detecting invoice formats
|
|
7
|
+
*/
|
|
8
|
+
export class FormatDetector {
|
|
9
|
+
/**
|
|
10
|
+
* Detects the format of an XML document
|
|
11
|
+
* @param xml XML content to analyze
|
|
12
|
+
* @returns Detected invoice format
|
|
13
|
+
*/
|
|
14
|
+
public static detectFormat(xml: string): InvoiceFormat {
|
|
15
|
+
try {
|
|
16
|
+
// Quick check for empty or invalid XML
|
|
17
|
+
if (!xml || typeof xml !== 'string' || xml.trim().length === 0) {
|
|
18
|
+
return InvoiceFormat.UNKNOWN;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Quick string-based pre-checks for performance
|
|
22
|
+
const quickCheck = FormatDetector.quickFormatCheck(xml);
|
|
23
|
+
if (quickCheck !== InvoiceFormat.UNKNOWN) {
|
|
24
|
+
return quickCheck;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// More thorough parsing-based checks
|
|
28
|
+
const doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
29
|
+
const root = doc.documentElement;
|
|
30
|
+
|
|
31
|
+
if (!root) {
|
|
32
|
+
return InvoiceFormat.UNKNOWN;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// UBL detection (Invoice or CreditNote root element)
|
|
36
|
+
if (FormatDetector.isUBLFormat(root)) {
|
|
37
|
+
// Check for XRechnung customization
|
|
38
|
+
if (FormatDetector.isXRechnungFormat(doc)) {
|
|
39
|
+
return InvoiceFormat.XRECHNUNG;
|
|
40
|
+
}
|
|
41
|
+
return InvoiceFormat.UBL;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Factur-X/ZUGFeRD detection (CrossIndustryInvoice root element)
|
|
45
|
+
if (FormatDetector.isCIIFormat(root)) {
|
|
46
|
+
return FormatDetector.detectCIIFormat(doc, xml);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ZUGFeRD v1 detection (CrossIndustryDocument root element)
|
|
50
|
+
if (FormatDetector.isZUGFeRDV1Format(root)) {
|
|
51
|
+
return InvoiceFormat.ZUGFERD;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// FatturaPA detection
|
|
55
|
+
if (FormatDetector.isFatturaPAFormat(root)) {
|
|
56
|
+
return InvoiceFormat.FATTURAPA;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return InvoiceFormat.UNKNOWN;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Error detecting format:', error);
|
|
62
|
+
return InvoiceFormat.UNKNOWN;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Performs a quick format check based on string content
|
|
68
|
+
* This is faster than full XML parsing for obvious cases
|
|
69
|
+
* @param xml XML string
|
|
70
|
+
* @returns Detected format or UNKNOWN if more analysis is needed
|
|
71
|
+
*/
|
|
72
|
+
private static quickFormatCheck(xml: string): InvoiceFormat {
|
|
73
|
+
const lowerXml = xml.toLowerCase();
|
|
74
|
+
|
|
75
|
+
// Check for obvious Factur-X indicators
|
|
76
|
+
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')
|
|
81
|
+
) {
|
|
82
|
+
return InvoiceFormat.FACTURX;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for obvious ZUGFeRD indicators
|
|
86
|
+
if (
|
|
87
|
+
lowerXml.includes('zugferd:') ||
|
|
88
|
+
lowerXml.includes('zugferd-invoice.xml') ||
|
|
89
|
+
lowerXml.includes('urn:ferd:') ||
|
|
90
|
+
lowerXml.includes('urn:zugferd')
|
|
91
|
+
) {
|
|
92
|
+
return InvoiceFormat.ZUGFERD;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for obvious XRechnung indicators
|
|
96
|
+
if (
|
|
97
|
+
lowerXml.includes('xrechnung') ||
|
|
98
|
+
lowerXml.includes('urn:xoev-de:kosit:standard:xrechnung')
|
|
99
|
+
) {
|
|
100
|
+
return InvoiceFormat.XRECHNUNG;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for obvious FatturaPA indicators
|
|
104
|
+
if (
|
|
105
|
+
lowerXml.includes('fatturapa') ||
|
|
106
|
+
lowerXml.includes('fattura elettronica') ||
|
|
107
|
+
lowerXml.includes('fatturaelettronica')
|
|
108
|
+
) {
|
|
109
|
+
return InvoiceFormat.FATTURAPA;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Need more analysis
|
|
113
|
+
return InvoiceFormat.UNKNOWN;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if the document is a UBL format
|
|
118
|
+
* @param root Root element
|
|
119
|
+
* @returns True if it's a UBL format
|
|
120
|
+
*/
|
|
121
|
+
private static isUBLFormat(root: Element): boolean {
|
|
122
|
+
return (
|
|
123
|
+
root.nodeName === 'Invoice' ||
|
|
124
|
+
root.nodeName === 'CreditNote' ||
|
|
125
|
+
root.nodeName === 'ubl:Invoice' ||
|
|
126
|
+
root.nodeName === 'ubl:CreditNote' ||
|
|
127
|
+
root.nodeName.endsWith(':Invoice') ||
|
|
128
|
+
root.nodeName.endsWith(':CreditNote')
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Checks if the document is an XRechnung format
|
|
134
|
+
* @param doc XML document
|
|
135
|
+
* @returns True if it's an XRechnung format
|
|
136
|
+
*/
|
|
137
|
+
private static isXRechnungFormat(doc: Document): boolean {
|
|
138
|
+
try {
|
|
139
|
+
// Set up namespaces for XPath queries
|
|
140
|
+
const namespaces = {
|
|
141
|
+
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
|
142
|
+
'ubl': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Create XPath selector with namespaces
|
|
146
|
+
const select = xpath.useNamespaces(namespaces);
|
|
147
|
+
|
|
148
|
+
// Use getElementsByTagName directly for more reliable results
|
|
149
|
+
const customizationNodes = doc.getElementsByTagName('cbc:CustomizationID');
|
|
150
|
+
|
|
151
|
+
// Check if any CustomizationID node contains "xrechnung"
|
|
152
|
+
for (let i = 0; i < customizationNodes.length; i++) {
|
|
153
|
+
const node = customizationNodes[i];
|
|
154
|
+
if (node.textContent && node.textContent.includes('xrechnung')) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.warn('Error checking for XRechnung format:', error);
|
|
162
|
+
// If direct DOM access fails, try a string-based approach
|
|
163
|
+
const xmlStr = new XMLSerializer().serializeToString(doc);
|
|
164
|
+
return xmlStr.includes('xrechnung') || xmlStr.includes('XRechnung');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Checks if the document is a CII format (Factur-X/ZUGFeRD v2+)
|
|
170
|
+
* @param root Root element
|
|
171
|
+
* @returns True if it's a CII format
|
|
172
|
+
*/
|
|
173
|
+
private static isCIIFormat(root: Element): boolean {
|
|
174
|
+
return (
|
|
175
|
+
root.nodeName === 'rsm:CrossIndustryInvoice' ||
|
|
176
|
+
root.nodeName === 'CrossIndustryInvoice' ||
|
|
177
|
+
root.nodeName.endsWith(':CrossIndustryInvoice')
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Checks if the document is a ZUGFeRD v1 format
|
|
183
|
+
* @param root Root element
|
|
184
|
+
* @returns True if it's a ZUGFeRD v1 format
|
|
185
|
+
*/
|
|
186
|
+
private static isZUGFeRDV1Format(root: Element): boolean {
|
|
187
|
+
return (
|
|
188
|
+
root.nodeName === 'rsm:CrossIndustryDocument' ||
|
|
189
|
+
root.nodeName === 'CrossIndustryDocument' ||
|
|
190
|
+
root.nodeName === 'ram:CrossIndustryDocument' ||
|
|
191
|
+
root.nodeName.endsWith(':CrossIndustryDocument')
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Checks if the document is a FatturaPA format
|
|
197
|
+
* @param root Root element
|
|
198
|
+
* @returns True if it's a FatturaPA format
|
|
199
|
+
*/
|
|
200
|
+
private static isFatturaPAFormat(root: Element): boolean {
|
|
201
|
+
return (
|
|
202
|
+
root.nodeName === 'FatturaElettronica' ||
|
|
203
|
+
(root.getAttribute('xmlns') && root.getAttribute('xmlns')!.includes('fatturapa.gov.it'))
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Detects the specific CII format (Factur-X vs ZUGFeRD)
|
|
209
|
+
* @param doc XML document
|
|
210
|
+
* @param xml Original XML string for fallback checks
|
|
211
|
+
* @returns Detected format
|
|
212
|
+
*/
|
|
213
|
+
private static detectCIIFormat(doc: Document, xml: string): InvoiceFormat {
|
|
214
|
+
try {
|
|
215
|
+
// Use direct DOM traversal instead of XPath for more reliable behavior
|
|
216
|
+
const contextNodes = doc.getElementsByTagNameNS(
|
|
217
|
+
'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
|
|
218
|
+
'ExchangedDocumentContext'
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (contextNodes.length === 0) {
|
|
222
|
+
// Try without namespace
|
|
223
|
+
const noNsContextNodes = doc.getElementsByTagName('ExchangedDocumentContext');
|
|
224
|
+
if (noNsContextNodes.length === 0) {
|
|
225
|
+
// Fallback to string-based detection
|
|
226
|
+
return FormatDetector.detectCIIFormatFromString(xml);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Loop through all potential context nodes
|
|
231
|
+
const allContextNodes = [...Array.from(contextNodes), ...Array.from(doc.getElementsByTagName('ExchangedDocumentContext'))];
|
|
232
|
+
|
|
233
|
+
for (const contextNode of allContextNodes) {
|
|
234
|
+
// Find guideline parameter
|
|
235
|
+
const guidelineNodes = contextNode.getElementsByTagName('ram:GuidelineSpecifiedDocumentContextParameter');
|
|
236
|
+
|
|
237
|
+
if (guidelineNodes.length === 0) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const guidelineNode of Array.from(guidelineNodes)) {
|
|
242
|
+
// Find ID element
|
|
243
|
+
const idNodes = guidelineNode.getElementsByTagName('ram:ID');
|
|
244
|
+
|
|
245
|
+
if (idNodes.length === 0) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const idNode of Array.from(idNodes)) {
|
|
250
|
+
const profileText = idNode.textContent || '';
|
|
251
|
+
|
|
252
|
+
// Check for ZUGFeRD profiles (v1 and v2)
|
|
253
|
+
if (
|
|
254
|
+
profileText.includes('zugferd') ||
|
|
255
|
+
profileText.includes('urn:ferd:') ||
|
|
256
|
+
profileText === CII_PROFILE_IDS.ZUGFERD_BASIC ||
|
|
257
|
+
profileText === CII_PROFILE_IDS.ZUGFERD_COMFORT ||
|
|
258
|
+
profileText === CII_PROFILE_IDS.ZUGFERD_EXTENDED ||
|
|
259
|
+
profileText === CII_PROFILE_IDS.ZUGFERD_V1_BASIC ||
|
|
260
|
+
profileText === CII_PROFILE_IDS.ZUGFERD_V1_COMFORT ||
|
|
261
|
+
profileText === CII_PROFILE_IDS.ZUGFERD_V1_EXTENDED
|
|
262
|
+
) {
|
|
263
|
+
return InvoiceFormat.ZUGFERD;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for Factur-X profiles
|
|
267
|
+
if (
|
|
268
|
+
profileText.includes('factur-x') ||
|
|
269
|
+
profileText === CII_PROFILE_IDS.FACTURX_MINIMUM ||
|
|
270
|
+
profileText === CII_PROFILE_IDS.FACTURX_BASIC ||
|
|
271
|
+
profileText === CII_PROFILE_IDS.FACTURX_EN16931
|
|
272
|
+
) {
|
|
273
|
+
return InvoiceFormat.FACTURX;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If we reach here, fall back to string checking
|
|
280
|
+
return FormatDetector.detectCIIFormatFromString(xml);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.warn('Error detecting CII format, falling back to generic CII:', error);
|
|
283
|
+
return FormatDetector.detectCIIFormatFromString(xml);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Fallback method to detect CII format from string content
|
|
289
|
+
* @param xml XML string
|
|
290
|
+
* @returns Detected format
|
|
291
|
+
*/
|
|
292
|
+
private static detectCIIFormatFromString(xml: string): InvoiceFormat {
|
|
293
|
+
// Check for Factur-X indicators
|
|
294
|
+
if (xml.includes('factur-x') || xml.includes('Factur-X')) {
|
|
295
|
+
return InvoiceFormat.FACTURX;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check for ZUGFeRD indicators
|
|
299
|
+
if (xml.includes('zugferd') || xml.includes('ZUGFeRD')) {
|
|
300
|
+
return InvoiceFormat.ZUGFERD;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Generic CII if we can't determine more specifically
|
|
304
|
+
return InvoiceFormat.CII;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EN16931 mandatory field validator
|
|
3
|
+
* Validates that invoices contain the minimum required fields per the EN16931 standard
|
|
4
|
+
*/
|
|
5
|
+
export class EN16931Validator {
|
|
6
|
+
/**
|
|
7
|
+
* Valid ISO 4217 currency codes (common subset)
|
|
8
|
+
*/
|
|
9
|
+
private static readonly VALID_CURRENCIES = [
|
|
10
|
+
'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN',
|
|
11
|
+
'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL',
|
|
12
|
+
'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY',
|
|
13
|
+
'COP', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD',
|
|
14
|
+
'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GGP', 'GHS',
|
|
15
|
+
'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF',
|
|
16
|
+
'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 'ISK', 'JEP', 'JMD', 'JOD',
|
|
17
|
+
'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT',
|
|
18
|
+
'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD',
|
|
19
|
+
'MMK', 'MNT', 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN',
|
|
20
|
+
'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', 'OMR', 'PAB', 'PEN', 'PGK',
|
|
21
|
+
'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', 'RSD', 'RUB', 'RWF', 'SAR',
|
|
22
|
+
'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLL', 'SOS', 'SPL', 'SRD',
|
|
23
|
+
'STN', 'SYP', 'SZL', 'THB', 'TJS', 'TMT', 'TND', 'TOP', 'TRY', 'TTD',
|
|
24
|
+
'TVD', 'TWD', 'TZS', 'UAH', 'UGX', 'USD', 'UYU', 'UZS', 'VEF', 'VND',
|
|
25
|
+
'VUV', 'WST', 'XAF', 'XCD', 'XDR', 'XOF', 'XPF', 'YER', 'ZAR', 'ZMW',
|
|
26
|
+
'ZWD'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates that an invoice object contains all mandatory EN16931 fields
|
|
31
|
+
* @param invoice The invoice object to validate
|
|
32
|
+
* @throws Error if mandatory fields are missing
|
|
33
|
+
*/
|
|
34
|
+
public static validateMandatoryFields(invoice: any): void {
|
|
35
|
+
const errors: string[] = [];
|
|
36
|
+
|
|
37
|
+
// BR-01: Invoice number is mandatory
|
|
38
|
+
if (!invoice.id && !invoice.accountingDocId && !invoice.invoiceId) {
|
|
39
|
+
errors.push('BR-01: Invoice number is mandatory');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// BR-02: Invoice issue date is mandatory
|
|
43
|
+
if (!invoice.date && !invoice.issueDate) {
|
|
44
|
+
errors.push('BR-02: Invoice issue date is mandatory');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// BR-06: Seller name is mandatory
|
|
48
|
+
if (!invoice.from?.name) {
|
|
49
|
+
errors.push('BR-06: Seller name is mandatory');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// BR-07: Buyer name is mandatory
|
|
53
|
+
if (!invoice.to?.name) {
|
|
54
|
+
errors.push('BR-07: Buyer name is mandatory');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// BR-16: Invoice line is mandatory (at least one)
|
|
58
|
+
if (!invoice.items || invoice.items.length === 0) {
|
|
59
|
+
errors.push('BR-16: At least one invoice line is mandatory');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// BR-03: Invoice type code is mandatory
|
|
63
|
+
if (!invoice.accountingDocType) {
|
|
64
|
+
errors.push('BR-03: Invoice type code is mandatory');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// BR-05: Invoice currency code is mandatory
|
|
68
|
+
if (!invoice.currency) {
|
|
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
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// BR-08: Seller postal address is mandatory
|
|
78
|
+
if (!invoice.from?.address?.city || !invoice.from?.address?.postalCode || (!invoice.from?.address?.countryCode && !invoice.from?.address?.country)) {
|
|
79
|
+
errors.push('BR-08: Seller postal address (city, postal code, country) is mandatory');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// BR-10: Buyer postal address is mandatory
|
|
83
|
+
if (!invoice.to?.address?.city || !invoice.to?.address?.postalCode || (!invoice.to?.address?.countryCode && !invoice.to?.address?.country)) {
|
|
84
|
+
errors.push('BR-10: Buyer postal address (city, postal code, country) is mandatory');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
throw new Error(`EN16931 validation failed:\n${errors.join('\n')}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validates mandatory fields for a parsed XML structure
|
|
94
|
+
* @param doc The parsed XML document
|
|
95
|
+
* @param format The invoice format
|
|
96
|
+
* @throws Error if mandatory fields are missing in XML
|
|
97
|
+
*/
|
|
98
|
+
public static validateXmlMandatoryFields(doc: Document, format: string): void {
|
|
99
|
+
const errors: string[] = [];
|
|
100
|
+
|
|
101
|
+
if (format === 'ubl' || format === 'xrechnung') {
|
|
102
|
+
// Check for UBL mandatory fields
|
|
103
|
+
const id = doc.getElementsByTagName('cbc:ID')[0]?.textContent;
|
|
104
|
+
if (!id) {
|
|
105
|
+
errors.push('BR-01: Invoice ID is missing in XML');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const issueDate = doc.getElementsByTagName('cbc:IssueDate')[0]?.textContent;
|
|
109
|
+
if (!issueDate) {
|
|
110
|
+
errors.push('BR-02: Invoice issue date is missing in XML');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sellerName = doc.querySelector('AccountingSupplierParty PartyName Name')?.textContent ||
|
|
114
|
+
doc.querySelector('AccountingSupplierParty PartyLegalEntity RegistrationName')?.textContent;
|
|
115
|
+
if (!sellerName) {
|
|
116
|
+
errors.push('BR-06: Seller name is missing in XML');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const buyerName = doc.querySelector('AccountingCustomerParty PartyName Name')?.textContent ||
|
|
120
|
+
doc.querySelector('AccountingCustomerParty PartyLegalEntity RegistrationName')?.textContent;
|
|
121
|
+
if (!buyerName) {
|
|
122
|
+
errors.push('BR-07: Buyer name is missing in XML');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const invoiceLines = doc.getElementsByTagName('InvoiceLine');
|
|
126
|
+
if (invoiceLines.length === 0) {
|
|
127
|
+
errors.push('BR-16: No invoice lines found in XML');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (errors.length > 0) {
|
|
132
|
+
throw new Error(`EN16931 XML validation failed:\n${errors.join('\n')}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|