@fin.cx/einvoice 9.0.0 → 10.0.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/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/einvoice.d.ts +12 -0
- package/dist_ts/einvoice.js +45 -3
- package/dist_ts/formats/base/base.decoder.d.ts +27 -2
- package/dist_ts/formats/base/base.decoder.js +52 -27
- package/dist_ts/formats/cii/cii.decoder.d.ts +89 -6
- package/dist_ts/formats/cii/cii.decoder.js +125 -30
- package/dist_ts/formats/cii/cii.encoder.d.ts +71 -1
- package/dist_ts/formats/cii/cii.encoder.js +191 -12
- package/dist_ts/formats/cii/cii.types.d.ts +2 -5
- package/dist_ts/formats/cii/cii.types.js +7 -9
- package/dist_ts/formats/cii/cii.validator.d.ts +13 -6
- package/dist_ts/formats/cii/cii.validator.js +29 -30
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +15 -3
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +0 -6
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +11 -25
- package/dist_ts/formats/cii/facturx/facturx.types.d.ts +0 -5
- package/dist_ts/formats/cii/facturx/facturx.types.js +1 -8
- package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +0 -5
- package/dist_ts/formats/cii/facturx/facturx.validator.js +5 -29
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +15 -2
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +0 -7
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +9 -67
- package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +0 -5
- package/dist_ts/formats/cii/zugferd/zugferd.types.js +1 -8
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +26 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +43 -1
- package/dist_ts/formats/cii/zugferd/zugferd.validator.js +6 -2
- package/dist_ts/formats/semantic/semantic.adapter.js +20 -18
- package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +0 -4
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -42
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +18 -24
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +9 -1
- package/dist_ts/formats/ubl/ubl.encoder.js +9 -5
- package/dist_ts/formats/ubl/ubl.validator.d.ts +13 -0
- package/dist_ts/formats/ubl/ubl.validator.js +28 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +10 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -7
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +30 -45
- package/dist_ts/formats/ubl/xrechnung.validator.d.ts +5 -4
- package/dist_ts/formats/ubl/xrechnung.validator.js +71 -68
- package/dist_ts/formats/utils/date.value.d.ts +35 -0
- package/dist_ts/formats/utils/date.value.js +98 -1
- package/dist_ts/formats/utils/format.detector.js +16 -10
- package/dist_ts/formats/utils/party.contact.d.ts +16 -0
- package/dist_ts/formats/utils/party.contact.js +16 -0
- package/dist_ts/formats/utils/party.identifier.d.ts +28 -0
- package/dist_ts/formats/utils/party.identifier.js +49 -0
- package/dist_ts/formats/utils/peppol.profile.d.ts +10 -0
- package/dist_ts/formats/utils/peppol.profile.js +12 -0
- package/dist_ts/formats/utils/seller.identifier.d.ts +46 -0
- package/dist_ts/formats/utils/seller.identifier.js +78 -0
- package/dist_ts/formats/utils/stated.values.d.ts +80 -0
- package/dist_ts/formats/utils/stated.values.js +418 -0
- package/dist_ts/formats/utils/vat.category.d.ts +30 -2
- package/dist_ts/formats/utils/vat.category.js +36 -6
- package/dist_ts/formats/utils/vat.id.d.ts +18 -0
- package/dist_ts/formats/utils/vat.id.js +22 -0
- package/dist_ts/formats/validation/conformance.harness.js +6 -6
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -21
- package/dist_ts/formats/validation/facturx.validator.js +6 -6
- package/dist_ts/formats/validation/integrated.validator.js +4 -13
- package/dist_ts/formats/validation/peppol.validator.js +6 -13
- package/dist_ts/formats/validation/validation.types.d.ts +5 -0
- package/dist_ts/formats/validation/validation.types.js +6 -1
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +21 -42
- package/dist_ts/formats/validation/vat-categories.validator.js +137 -431
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +11 -58
- package/dist_ts/formats/validation/xrechnung.validator.js +58 -324
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +1 -1
- package/dist_ts/interfaces/en16931-metadata.d.ts +0 -4
- package/dist_ts/interfaces/stated.values.d.ts +93 -0
- package/dist_ts/interfaces/stated.values.js +2 -0
- package/package.json +4 -4
- package/readme.md +152 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +52 -2
- package/ts/formats/base/base.decoder.ts +55 -45
- package/ts/formats/cii/cii.decoder.ts +159 -32
- package/ts/formats/cii/cii.encoder.ts +206 -14
- package/ts/formats/cii/cii.types.ts +7 -9
- package/ts/formats/cii/cii.validator.ts +30 -32
- package/ts/formats/cii/facturx/facturx.decoder.ts +16 -2
- package/ts/formats/cii/facturx/facturx.encoder.ts +13 -25
- package/ts/formats/cii/facturx/facturx.types.ts +0 -9
- package/ts/formats/cii/facturx/facturx.validator.ts +5 -43
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +16 -1
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +10 -72
- package/ts/formats/cii/zugferd/zugferd.types.ts +0 -9
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +56 -0
- package/ts/formats/cii/zugferd/zugferd.validator.ts +5 -1
- package/ts/formats/semantic/semantic.adapter.ts +19 -17
- package/ts/formats/ubl/en16931.ubl.validator.ts +5 -64
- package/ts/formats/ubl/generic/ubl.encoder.ts +19 -23
- package/ts/formats/ubl/ubl.decoder.ts +12 -0
- package/ts/formats/ubl/ubl.encoder.ts +8 -4
- package/ts/formats/ubl/ubl.validator.ts +29 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +8 -0
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +33 -45
- package/ts/formats/ubl/xrechnung.validator.ts +75 -127
- package/ts/formats/utils/date.value.ts +106 -0
- package/ts/formats/utils/format.detector.ts +15 -9
- package/ts/formats/utils/party.contact.ts +30 -0
- package/ts/formats/utils/party.identifier.ts +61 -0
- package/ts/formats/utils/peppol.profile.ts +13 -0
- package/ts/formats/utils/seller.identifier.ts +102 -0
- package/ts/formats/utils/stated.values.ts +499 -0
- package/ts/formats/utils/vat.category.ts +47 -5
- package/ts/formats/utils/vat.id.ts +24 -0
- package/ts/formats/validation/conformance.harness.ts +5 -5
- package/ts/formats/validation/en16931.business-rules.validator.ts +3 -28
- package/ts/formats/validation/facturx.validator.ts +5 -5
- package/ts/formats/validation/integrated.validator.ts +3 -16
- package/ts/formats/validation/peppol.validator.ts +5 -16
- package/ts/formats/validation/validation.types.ts +7 -1
- package/ts/formats/validation/vat-categories.validator.ts +179 -761
- package/ts/formats/validation/xrechnung.validator.ts +61 -382
- package/ts/index.ts +9 -0
- package/ts/interfaces/en16931-metadata.ts +3 -7
- package/ts/interfaces/stated.values.ts +94 -0
- package/ts/readme.md +1 -1
- package/dist_ts/formats/utils/eu.memberstates.d.ts +0 -11
- package/dist_ts/formats/utils/eu.memberstates.js +0 -16
- package/ts/formats/utils/eu.memberstates.ts +0 -16
|
@@ -7,33 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { EInvoice } from '../../einvoice.js';
|
|
10
|
-
import type
|
|
10
|
+
import { EINVOICE_RULE_SOURCE, type ValidationResult } from './validation.types.js';
|
|
11
11
|
import { UBL_CUSTOMIZATION_IDS } from '../ubl/ubl.types.js';
|
|
12
|
+
import { getPartyContact } from '../utils/party.contact.js';
|
|
13
|
+
import { GERMAN_VAT_ID_FORMAT_CODE, germanVatIdFormatMessage, isMalformedGermanVatId } from '../utils/vat.id.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* XRechnung-specific validator implementing German CIUS rules
|
|
15
17
|
*/
|
|
16
18
|
export class XRechnungValidator {
|
|
17
|
-
private static readonly LEITWEG_ID_PATTERN = /^[0-9]{2,3}-[0-9]{1,12}-[0-9]{2,30}$/;
|
|
18
|
-
private static readonly IBAN_PATTERNS: Record<string, { length: number; pattern: RegExp }> = {
|
|
19
|
-
DE: { length: 22, pattern: /^DE[0-9]{2}[0-9]{8}[0-9]{10}$/ },
|
|
20
|
-
AT: { length: 20, pattern: /^AT[0-9]{2}[0-9]{5}[0-9]{11}$/ },
|
|
21
|
-
CH: { length: 21, pattern: /^CH[0-9]{2}[0-9]{5}[0-9A-Z]{12}$/ },
|
|
22
|
-
FR: { length: 27, pattern: /^FR[0-9]{2}[0-9]{5}[0-9]{5}[0-9A-Z]{11}[0-9]{2}$/ },
|
|
23
|
-
NL: { length: 18, pattern: /^NL[0-9]{2}[A-Z]{4}[0-9]{10}$/ },
|
|
24
|
-
BE: { length: 16, pattern: /^BE[0-9]{2}[0-9]{3}[0-9]{7}[0-9]{2}$/ },
|
|
25
|
-
IT: { length: 27, pattern: /^IT[0-9]{2}[A-Z][0-9]{5}[0-9]{5}[0-9A-Z]{12}$/ },
|
|
26
|
-
ES: { length: 24, pattern: /^ES[0-9]{2}[0-9]{4}[0-9]{4}[0-9]{2}[0-9]{10}$/ }
|
|
27
|
-
};
|
|
28
|
-
private static readonly BIC_PATTERN = /^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/;
|
|
29
|
-
|
|
30
|
-
// SEPA countries
|
|
31
|
-
private static readonly SEPA_COUNTRIES = new Set([
|
|
32
|
-
'AD', 'AT', 'BE', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI',
|
|
33
|
-
'FR', 'GB', 'GI', 'GR', 'HR', 'HU', 'IE', 'IS', 'IT', 'LI', 'LT', 'LU',
|
|
34
|
-
'LV', 'MC', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 'SM', 'VA'
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
19
|
/**
|
|
38
20
|
* Validate XRechnung-specific requirements
|
|
39
21
|
*/
|
|
@@ -46,9 +28,7 @@ export class XRechnungValidator {
|
|
|
46
28
|
}
|
|
47
29
|
|
|
48
30
|
// Validate mandatory fields
|
|
49
|
-
results.push(...this.validateLeitwegId(invoice));
|
|
50
31
|
results.push(...this.validateBuyerReference(invoice));
|
|
51
|
-
results.push(...this.validatePaymentDetails(invoice));
|
|
52
32
|
results.push(...this.validateSellerContact(invoice));
|
|
53
33
|
results.push(...this.validateTaxRegistration(invoice));
|
|
54
34
|
|
|
@@ -78,300 +58,69 @@ export class XRechnungValidator {
|
|
|
78
58
|
}
|
|
79
59
|
|
|
80
60
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* Rule: XR-DE-01
|
|
84
|
-
*/
|
|
85
|
-
private validateLeitwegId(invoice: EInvoice): ValidationResult[] {
|
|
86
|
-
const results: ValidationResult[] = [];
|
|
87
|
-
|
|
88
|
-
// Leitweg-ID is typically in buyer reference (BT-10) for B2G
|
|
89
|
-
const buyerReference = invoice.metadata?.buyerReference || invoice.buyerReference || '';
|
|
90
|
-
|
|
91
|
-
// Check if it looks like a Leitweg-ID
|
|
92
|
-
if (buyerReference && this.looksLikeLeitwegId(buyerReference)) {
|
|
93
|
-
if (!XRechnungValidator.LEITWEG_ID_PATTERN.test(buyerReference.trim())) {
|
|
94
|
-
results.push({
|
|
95
|
-
ruleId: 'XR-DE-01',
|
|
96
|
-
severity: 'error',
|
|
97
|
-
source: 'XRECHNUNG',
|
|
98
|
-
message: `Invalid Leitweg-ID format: ${buyerReference}. Expected pattern: [0-9]{2,3}-[0-9]{1,12}-[0-9]{2,30}`,
|
|
99
|
-
btReference: 'BT-10',
|
|
100
|
-
field: 'buyerReference',
|
|
101
|
-
value: buyerReference
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// For B2G invoices, Leitweg-ID might be mandatory
|
|
107
|
-
if (this.isB2GInvoice(invoice) && !buyerReference) {
|
|
108
|
-
results.push({
|
|
109
|
-
ruleId: 'XR-DE-15',
|
|
110
|
-
severity: 'error',
|
|
111
|
-
source: 'XRECHNUNG',
|
|
112
|
-
message: 'Buyer reference (Leitweg-ID) is mandatory for B2G invoices in Germany',
|
|
113
|
-
btReference: 'BT-10',
|
|
114
|
-
field: 'buyerReference'
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return results;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check if string looks like a Leitweg-ID
|
|
123
|
-
*/
|
|
124
|
-
private looksLikeLeitwegId(value: string): boolean {
|
|
125
|
-
// Contains dashes and numbers in the right proportion
|
|
126
|
-
return value.includes('-') && /^\d+-\d+-\d+$/.test(value.trim());
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Check if this is a B2G invoice
|
|
131
|
-
*/
|
|
132
|
-
private isB2GInvoice(invoice: EInvoice): boolean {
|
|
133
|
-
// Check if buyer is a public entity (simplified check)
|
|
134
|
-
const buyerName = invoice.to?.name?.toLowerCase() || '';
|
|
135
|
-
const buyerType = invoice.metadata?.extensions?.buyerType?.toLowerCase() || '';
|
|
136
|
-
|
|
137
|
-
const publicIndicators = [
|
|
138
|
-
'bundesamt', 'landesamt', 'stadtverwaltung', 'gemeinde',
|
|
139
|
-
'ministerium', 'behörde', 'öffentlich', 'public', 'government'
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
return publicIndicators.some(indicator =>
|
|
143
|
-
buyerName.includes(indicator) || buyerType.includes(indicator)
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Validate mandatory buyer reference (BT-10)
|
|
149
|
-
* Rule: XR-DE-15
|
|
61
|
+
* The buyer reference (BT-10) is stated (BR-DE-15). In German public procurement it carries
|
|
62
|
+
* the Leitweg-ID; the XRechnung rules do not check its format.
|
|
150
63
|
*/
|
|
151
64
|
private validateBuyerReference(invoice: EInvoice): ValidationResult[] {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Skip if B2G invoice - already handled in validateLeitwegId
|
|
157
|
-
if (this.isB2GInvoice(invoice)) {
|
|
158
|
-
return results;
|
|
65
|
+
const buyerReference = invoice.buyerReference || '';
|
|
66
|
+
if (buyerReference.trim().length > 0) {
|
|
67
|
+
return [];
|
|
159
68
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
field: 'buyerReference'
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return results;
|
|
69
|
+
return [{
|
|
70
|
+
ruleId: 'BR-DE-15',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
source: 'XRECHNUNG',
|
|
73
|
+
message: 'The buyer reference (BT-10) is stated',
|
|
74
|
+
btReference: 'BT-10',
|
|
75
|
+
field: 'buyerReference'
|
|
76
|
+
}];
|
|
173
77
|
}
|
|
174
78
|
|
|
175
79
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const results: ValidationResult[] = [];
|
|
181
|
-
|
|
182
|
-
// Check payment means
|
|
183
|
-
const paymentMeans = invoice.metadata?.extensions?.paymentMeans as Array<{
|
|
184
|
-
type?: string;
|
|
185
|
-
iban?: string;
|
|
186
|
-
bic?: string;
|
|
187
|
-
accountName?: string;
|
|
188
|
-
}> | undefined;
|
|
189
|
-
if (!paymentMeans || paymentMeans.length === 0) {
|
|
190
|
-
return results; // No payment details to validate
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
for (const payment of paymentMeans) {
|
|
194
|
-
// Validate IBAN if present
|
|
195
|
-
if (payment.iban) {
|
|
196
|
-
const ibanResult = this.validateIBAN(payment.iban);
|
|
197
|
-
if (!ibanResult.valid) {
|
|
198
|
-
results.push({
|
|
199
|
-
ruleId: 'XR-DE-19',
|
|
200
|
-
severity: 'error',
|
|
201
|
-
source: 'XRECHNUNG',
|
|
202
|
-
message: `Invalid IBAN: ${ibanResult.message}`,
|
|
203
|
-
btReference: 'BT-84',
|
|
204
|
-
field: 'iban',
|
|
205
|
-
value: payment.iban
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Check if IBAN country is in SEPA zone
|
|
210
|
-
const countryCode = payment.iban.substring(0, 2);
|
|
211
|
-
if (!XRechnungValidator.SEPA_COUNTRIES.has(countryCode)) {
|
|
212
|
-
results.push({
|
|
213
|
-
ruleId: 'XR-DE-19',
|
|
214
|
-
severity: 'warning',
|
|
215
|
-
source: 'XRECHNUNG',
|
|
216
|
-
message: `IBAN country ${countryCode} is not in SEPA zone`,
|
|
217
|
-
btReference: 'BT-84',
|
|
218
|
-
field: 'iban',
|
|
219
|
-
value: payment.iban
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Validate BIC if present
|
|
225
|
-
if (payment.bic) {
|
|
226
|
-
const bicResult = this.validateBIC(payment.bic);
|
|
227
|
-
if (!bicResult.valid) {
|
|
228
|
-
results.push({
|
|
229
|
-
ruleId: 'XR-DE-20',
|
|
230
|
-
severity: 'error',
|
|
231
|
-
source: 'XRECHNUNG',
|
|
232
|
-
message: `Invalid BIC: ${bicResult.message}`,
|
|
233
|
-
btReference: 'BT-86',
|
|
234
|
-
field: 'bic',
|
|
235
|
-
value: payment.bic
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// For German domestic payments, BIC is optional if IBAN starts with DE
|
|
241
|
-
if (payment.iban?.startsWith('DE') && !payment.bic) {
|
|
242
|
-
// This is fine, BIC is optional for domestic German payments
|
|
243
|
-
} else if (payment.iban && !payment.iban.startsWith('DE') && !payment.bic) {
|
|
244
|
-
results.push({
|
|
245
|
-
ruleId: 'XR-DE-20',
|
|
246
|
-
severity: 'warning',
|
|
247
|
-
source: 'XRECHNUNG',
|
|
248
|
-
message: 'BIC is recommended for international SEPA transfers',
|
|
249
|
-
btReference: 'BT-86',
|
|
250
|
-
field: 'bic'
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return results;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Validate IBAN format and checksum
|
|
260
|
-
*/
|
|
261
|
-
private validateIBAN(iban: string): { valid: boolean; message?: string } {
|
|
262
|
-
// Remove spaces and convert to uppercase
|
|
263
|
-
const cleanIBAN = iban.replace(/\s/g, '').toUpperCase();
|
|
264
|
-
|
|
265
|
-
// Check basic format
|
|
266
|
-
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(cleanIBAN)) {
|
|
267
|
-
return { valid: false, message: 'Invalid IBAN format' };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Get country code
|
|
271
|
-
const countryCode = cleanIBAN.substring(0, 2);
|
|
272
|
-
|
|
273
|
-
// Check country-specific format
|
|
274
|
-
const countryFormat = XRechnungValidator.IBAN_PATTERNS[countryCode];
|
|
275
|
-
if (countryFormat) {
|
|
276
|
-
if (cleanIBAN.length !== countryFormat.length) {
|
|
277
|
-
return {
|
|
278
|
-
valid: false,
|
|
279
|
-
message: `Invalid IBAN length for ${countryCode}: expected ${countryFormat.length}, got ${cleanIBAN.length}`
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (!countryFormat.pattern.test(cleanIBAN)) {
|
|
284
|
-
return {
|
|
285
|
-
valid: false,
|
|
286
|
-
message: `Invalid IBAN format for ${countryCode}`
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Validate checksum using mod-97 algorithm
|
|
292
|
-
const rearranged = cleanIBAN.substring(4) + cleanIBAN.substring(0, 4);
|
|
293
|
-
const numeric = rearranged.replace(/[A-Z]/g, char => (char.charCodeAt(0) - 55).toString());
|
|
294
|
-
|
|
295
|
-
// Calculate mod 97 for large numbers
|
|
296
|
-
let remainder = 0;
|
|
297
|
-
for (let i = 0; i < numeric.length; i++) {
|
|
298
|
-
remainder = (remainder * 10 + parseInt(numeric[i])) % 97;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (remainder !== 1) {
|
|
302
|
-
return { valid: false, message: 'Invalid IBAN checksum' };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return { valid: true };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Validate BIC format
|
|
310
|
-
*/
|
|
311
|
-
private validateBIC(bic: string): { valid: boolean; message?: string } {
|
|
312
|
-
const cleanBIC = bic.replace(/\s/g, '').toUpperCase();
|
|
313
|
-
|
|
314
|
-
if (!XRechnungValidator.BIC_PATTERN.test(cleanBIC)) {
|
|
315
|
-
return {
|
|
316
|
-
valid: false,
|
|
317
|
-
message: 'Invalid BIC format. Expected 8 or 11 alphanumeric characters'
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Additional validation could check if BIC exists in SWIFT directory
|
|
322
|
-
// but that requires external data
|
|
323
|
-
|
|
324
|
-
return { valid: true };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Validate seller contact details
|
|
329
|
-
* Rule: XR-DE-02
|
|
80
|
+
* The seller contact (BG-6), as the XRechnung encoder writes it from the seller
|
|
81
|
+
* (`metadata.contactInformation`, otherwise its name, telephone number and email address):
|
|
82
|
+
* BR-DE-2 asks for it, BR-DE-27 and BR-DE-28 ask a telephone number (BT-42) and an email
|
|
83
|
+
* address (BT-43) to have the forms the XRechnung rules state (warnings).
|
|
330
84
|
*/
|
|
331
85
|
private validateSellerContact(invoice: EInvoice): ValidationResult[] {
|
|
332
86
|
const results: ValidationResult[] = [];
|
|
87
|
+
const sellerContact = getPartyContact(invoice.from);
|
|
333
88
|
|
|
334
|
-
|
|
335
|
-
const sellerContact = invoice.metadata?.extensions?.sellerContact as {
|
|
336
|
-
name?: string;
|
|
337
|
-
email?: string;
|
|
338
|
-
phone?: string;
|
|
339
|
-
} | undefined;
|
|
340
|
-
|
|
341
|
-
if (!sellerContact || (!sellerContact.name && !sellerContact.email && !sellerContact.phone)) {
|
|
89
|
+
if (!sellerContact) {
|
|
342
90
|
results.push({
|
|
343
|
-
ruleId: '
|
|
91
|
+
ruleId: 'BR-DE-2',
|
|
344
92
|
severity: 'error',
|
|
345
93
|
source: 'XRECHNUNG',
|
|
346
|
-
message: '
|
|
94
|
+
message: 'The seller contact (BG-6) is stated',
|
|
347
95
|
bgReference: 'BG-6',
|
|
348
|
-
field: '
|
|
96
|
+
field: 'from'
|
|
349
97
|
});
|
|
98
|
+
return results;
|
|
350
99
|
}
|
|
351
100
|
|
|
352
|
-
//
|
|
353
|
-
if (sellerContact
|
|
101
|
+
// XR-TELEPHONE-REGEX of the XRechnung rules: at least three digits
|
|
102
|
+
if (sellerContact.phone && !/([0-9].*){3,}/.test(sellerContact.phone.trim())) {
|
|
354
103
|
results.push({
|
|
355
|
-
ruleId: '
|
|
104
|
+
ruleId: 'BR-DE-27',
|
|
356
105
|
severity: 'warning',
|
|
357
106
|
source: 'XRECHNUNG',
|
|
358
|
-
message: `
|
|
359
|
-
btReference: 'BT-
|
|
360
|
-
field: '
|
|
361
|
-
value: sellerContact.
|
|
107
|
+
message: `The seller contact telephone number (BT-42) contains at least three digits: ${sellerContact.phone}`,
|
|
108
|
+
btReference: 'BT-42',
|
|
109
|
+
field: 'from.phone',
|
|
110
|
+
value: sellerContact.phone
|
|
362
111
|
});
|
|
363
112
|
}
|
|
364
113
|
|
|
365
|
-
//
|
|
366
|
-
if (sellerContact
|
|
114
|
+
// XR-EMAIL-REGEX of the XRechnung rules
|
|
115
|
+
if (sellerContact.email && !/^[^@\s]+@([^@.\s]+\.)+[^@.\s]+$/.test(sellerContact.email.trim())) {
|
|
367
116
|
results.push({
|
|
368
|
-
ruleId: '
|
|
117
|
+
ruleId: 'BR-DE-28',
|
|
369
118
|
severity: 'warning',
|
|
370
119
|
source: 'XRECHNUNG',
|
|
371
|
-
message: `
|
|
372
|
-
btReference: 'BT-
|
|
373
|
-
field: '
|
|
374
|
-
value: sellerContact.
|
|
120
|
+
message: `The seller contact email address (BT-43) has exactly one @, with a domain after it: ${sellerContact.email}`,
|
|
121
|
+
btReference: 'BT-43',
|
|
122
|
+
field: 'from.email',
|
|
123
|
+
value: sellerContact.email
|
|
375
124
|
});
|
|
376
125
|
}
|
|
377
126
|
|
|
@@ -379,114 +128,44 @@ export class XRechnungValidator {
|
|
|
379
128
|
}
|
|
380
129
|
|
|
381
130
|
/**
|
|
382
|
-
*
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return emailPattern.test(email);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Validate phone format (basic)
|
|
391
|
-
*/
|
|
392
|
-
private isValidPhone(phone: string): boolean {
|
|
393
|
-
// Remove common formatting characters
|
|
394
|
-
const cleanPhone = phone.replace(/[\s\-\(\)\.]/g, '');
|
|
395
|
-
// Check if it contains only numbers and optional + at start
|
|
396
|
-
return /^\+?[0-9]{6,15}$/.test(cleanPhone);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Validate tax registration details
|
|
401
|
-
* Rules: XR-DE-03, XR-DE-04
|
|
131
|
+
* The seller VAT identifier (BT-31) or tax registration identifier (BT-32) of the seller's
|
|
132
|
+
* registration details, `vatId` and `taxNumber` (BR-DE-16). The rule applies when a line
|
|
133
|
+
* states a VAT category code S, Z, E, AE, K, G, L or M; the encoders write S or AE for every
|
|
134
|
+
* line. It also accepts a tax representative (BG-11), which the envelope cannot state.
|
|
402
135
|
*/
|
|
403
136
|
private validateTaxRegistration(invoice: EInvoice): ValidationResult[] {
|
|
404
137
|
const results: ValidationResult[] = [];
|
|
405
138
|
|
|
406
|
-
const sellerVatId = invoice.
|
|
407
|
-
|
|
408
|
-
invoice.metadata?.extensions?.sellerVatId;
|
|
409
|
-
const sellerTaxId = invoice.metadata?.extensions?.sellerTaxId;
|
|
139
|
+
const sellerVatId = invoice.from?.registrationDetails?.vatId?.trim() || '';
|
|
140
|
+
const sellerTaxNumber = invoice.from?.registrationDetails?.taxNumber?.trim() || '';
|
|
410
141
|
|
|
411
|
-
|
|
412
|
-
if (!sellerVatId && !sellerTaxId) {
|
|
142
|
+
if ((invoice.items ?? []).length > 0 && !sellerVatId && !sellerTaxNumber) {
|
|
413
143
|
results.push({
|
|
414
|
-
ruleId: '
|
|
144
|
+
ruleId: 'BR-DE-16',
|
|
415
145
|
severity: 'error',
|
|
416
146
|
source: 'XRECHNUNG',
|
|
417
|
-
message: '
|
|
147
|
+
message: 'The seller states its VAT identifier (BT-31) or tax registration identifier (BT-32), from.registrationDetails.vatId or .taxNumber',
|
|
418
148
|
btReference: 'BT-31',
|
|
419
|
-
field: '
|
|
149
|
+
field: 'from.registrationDetails'
|
|
420
150
|
});
|
|
421
151
|
}
|
|
422
152
|
|
|
423
|
-
//
|
|
424
|
-
if (sellerVatId
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Validate German Tax ID format if present
|
|
439
|
-
if (sellerTaxId && this.looksLikeGermanTaxId(sellerTaxId)) {
|
|
440
|
-
if (!this.isValidGermanTaxId(sellerTaxId)) {
|
|
441
|
-
results.push({
|
|
442
|
-
ruleId: 'XR-DE-04',
|
|
443
|
-
severity: 'warning',
|
|
444
|
-
source: 'XRECHNUNG',
|
|
445
|
-
message: `Invalid German Tax ID format: ${sellerTaxId}`,
|
|
446
|
-
btReference: 'BT-32',
|
|
447
|
-
field: 'taxId',
|
|
448
|
-
value: sellerTaxId
|
|
449
|
-
});
|
|
450
|
-
}
|
|
153
|
+
// The package's own check of the German VAT identification number format (no official rule)
|
|
154
|
+
if (isMalformedGermanVatId(sellerVatId)) {
|
|
155
|
+
results.push({
|
|
156
|
+
ruleId: GERMAN_VAT_ID_FORMAT_CODE,
|
|
157
|
+
severity: 'error',
|
|
158
|
+
source: EINVOICE_RULE_SOURCE,
|
|
159
|
+
message: germanVatIdFormatMessage(sellerVatId),
|
|
160
|
+
btReference: 'BT-31',
|
|
161
|
+
field: 'from.registrationDetails.vatId',
|
|
162
|
+
value: sellerVatId
|
|
163
|
+
});
|
|
451
164
|
}
|
|
452
165
|
|
|
453
166
|
return results;
|
|
454
167
|
}
|
|
455
168
|
|
|
456
|
-
/**
|
|
457
|
-
* Validate German VAT ID format
|
|
458
|
-
*/
|
|
459
|
-
private isValidGermanVatId(vatId: string): boolean {
|
|
460
|
-
// German VAT ID: DE followed by 9 digits
|
|
461
|
-
const germanVatPattern = /^DE[0-9]{9}$/;
|
|
462
|
-
return germanVatPattern.test(vatId.replace(/\s/g, ''));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Check if value looks like a German Tax ID
|
|
467
|
-
*/
|
|
468
|
-
private looksLikeGermanTaxId(value: string): boolean {
|
|
469
|
-
const clean = value.replace(/[\s\/\-]/g, '');
|
|
470
|
-
return /^[0-9]{10,11}$/.test(clean);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Validate German Tax ID format
|
|
475
|
-
*/
|
|
476
|
-
private isValidGermanTaxId(taxId: string): boolean {
|
|
477
|
-
// German Tax ID: 11 digits with specific checksum algorithm
|
|
478
|
-
const clean = taxId.replace(/[\s\/\-]/g, '');
|
|
479
|
-
|
|
480
|
-
if (!/^[0-9]{11}$/.test(clean)) {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Simplified validation - full algorithm would require checksum calculation
|
|
485
|
-
// At least check that not all digits are the same
|
|
486
|
-
const firstDigit = clean[0];
|
|
487
|
-
return !clean.split('').every(digit => digit === firstDigit);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
169
|
/**
|
|
491
170
|
* Create XRechnung profile validator instance
|
|
492
171
|
*/
|
package/ts/index.ts
CHANGED
|
@@ -99,6 +99,15 @@ export type {
|
|
|
99
99
|
EInvoiceOptions
|
|
100
100
|
} from './interfaces/common.js';
|
|
101
101
|
|
|
102
|
+
// The values a received document states (EInvoice.statedValues), with their EN 16931 business terms
|
|
103
|
+
export type {
|
|
104
|
+
IEInvoiceStatedValues,
|
|
105
|
+
IStatedDocumentTotals,
|
|
106
|
+
IStatedVatBreakdown,
|
|
107
|
+
IStatedLine,
|
|
108
|
+
IStatedAllowanceCharge,
|
|
109
|
+
} from './interfaces/stated.values.js';
|
|
110
|
+
|
|
102
111
|
export { ValidationLevel, InvoiceFormat } from './interfaces/common.js';
|
|
103
112
|
|
|
104
113
|
// Export interfaces (legacy support)
|
|
@@ -14,19 +14,15 @@ export interface IEInvoiceMetadata {
|
|
|
14
14
|
format?: InvoiceFormat;
|
|
15
15
|
version?: string;
|
|
16
16
|
profile?: string;
|
|
17
|
-
customizationId?: string;
|
|
17
|
+
customizationId?: string; // BT-24 specification identifier; the decoders set it
|
|
18
18
|
|
|
19
19
|
// EN16931 Business Terms
|
|
20
20
|
vatAccountingCurrency?: string; // BT-6
|
|
21
21
|
documentTypeCode?: string; // BT-3
|
|
22
22
|
paymentMeansCode?: string; // BT-81
|
|
23
|
-
amountDue?: number; // BT-115
|
|
24
23
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
buyerTaxId?: string; // BT-48
|
|
28
|
-
buyerReference?: string; // BT-10
|
|
29
|
-
profileId?: string; // BT-23
|
|
24
|
+
// References
|
|
25
|
+
profileId?: string; // BT-23 business process type (UBL cbc:ProfileID)
|
|
30
26
|
paymentTerms?: string; // BT-20
|
|
31
27
|
|
|
32
28
|
// Delivery information (BG-13)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The amounts, VAT breakdown, line amounts and document level allowances and
|
|
3
|
+
* charges a received document states, read as they are written, with their
|
|
4
|
+
* EN 16931 business terms. They are not computed and not updated when the
|
|
5
|
+
* envelope changes, and no encoder writes them: the envelope computes its
|
|
6
|
+
* totals from quantity × net price (`computeDocumentTotals`), and cannot yet
|
|
7
|
+
* express document or line allowances and charges or a price base quantity.
|
|
8
|
+
* Every field is undefined when the document does not state it.
|
|
9
|
+
*/
|
|
10
|
+
export interface IEInvoiceStatedValues {
|
|
11
|
+
/** Document totals (BG-22) */
|
|
12
|
+
totals: IStatedDocumentTotals;
|
|
13
|
+
/** VAT breakdown (BG-23), in document order */
|
|
14
|
+
vatBreakdown: IStatedVatBreakdown[];
|
|
15
|
+
/** Invoice lines (BG-25), in document order */
|
|
16
|
+
lines: IStatedLine[];
|
|
17
|
+
/** Document level allowances (BG-20), in document order */
|
|
18
|
+
allowances: IStatedAllowanceCharge[];
|
|
19
|
+
/** Document level charges (BG-21), in document order */
|
|
20
|
+
charges: IStatedAllowanceCharge[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Document totals (BG-22) as a document states them */
|
|
24
|
+
export interface IStatedDocumentTotals {
|
|
25
|
+
/** Sum of Invoice line net amount (BT-106) */
|
|
26
|
+
lineTotal?: number;
|
|
27
|
+
/** Sum of allowances on document level (BT-107) */
|
|
28
|
+
allowanceTotal?: number;
|
|
29
|
+
/** Sum of charges on document level (BT-108) */
|
|
30
|
+
chargeTotal?: number;
|
|
31
|
+
/** Invoice total amount without VAT (BT-109) */
|
|
32
|
+
taxExclusiveTotal?: number;
|
|
33
|
+
/** Invoice total VAT amount (BT-110), in the document currency */
|
|
34
|
+
taxTotal?: number;
|
|
35
|
+
/** Invoice total VAT amount in accounting currency (BT-111), with the VAT accounting currency code (BT-6) */
|
|
36
|
+
taxTotalInAccountingCurrency?: { amount: number; currency: string };
|
|
37
|
+
/** Invoice total amount with VAT (BT-112) */
|
|
38
|
+
taxInclusiveTotal?: number;
|
|
39
|
+
/** Paid amount (BT-113) */
|
|
40
|
+
paidAmount?: number;
|
|
41
|
+
/** Rounding amount (BT-114) */
|
|
42
|
+
roundingAmount?: number;
|
|
43
|
+
/** Amount due for payment (BT-115) */
|
|
44
|
+
amountDue?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One VAT breakdown (BG-23) as a document states it */
|
|
48
|
+
export interface IStatedVatBreakdown {
|
|
49
|
+
/** VAT category taxable amount (BT-116) */
|
|
50
|
+
taxableAmount?: number;
|
|
51
|
+
/** VAT category tax amount (BT-117) */
|
|
52
|
+
taxAmount?: number;
|
|
53
|
+
/** VAT category code (BT-118), UNTDID 5305 */
|
|
54
|
+
categoryCode?: string;
|
|
55
|
+
/** VAT category rate (BT-119); a category not subject to VAT states none */
|
|
56
|
+
rate?: number;
|
|
57
|
+
/** VAT exemption reason text (BT-120) */
|
|
58
|
+
exemptionReason?: string;
|
|
59
|
+
/** VAT exemption reason code (BT-121) */
|
|
60
|
+
exemptionReasonCode?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The amounts and the VAT category of one invoice line (BG-25) as a document states them */
|
|
64
|
+
export interface IStatedLine {
|
|
65
|
+
/** Invoice line identifier (BT-126) */
|
|
66
|
+
lineId?: string;
|
|
67
|
+
/** Invoice line net amount (BT-131) */
|
|
68
|
+
netAmount?: number;
|
|
69
|
+
/** Invoiced item VAT category code (BT-151) */
|
|
70
|
+
vatCategoryCode?: string;
|
|
71
|
+
/** Invoiced item VAT rate (BT-152) */
|
|
72
|
+
vatRate?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One document level allowance (BG-20: BT-92 to BT-98) or charge (BG-21:
|
|
77
|
+
* BT-99 to BT-105) as a document states it
|
|
78
|
+
*/
|
|
79
|
+
export interface IStatedAllowanceCharge {
|
|
80
|
+
/** Amount (BT-92, BT-99) */
|
|
81
|
+
amount?: number;
|
|
82
|
+
/** Base amount (BT-93, BT-100) */
|
|
83
|
+
baseAmount?: number;
|
|
84
|
+
/** Percentage (BT-94, BT-101) */
|
|
85
|
+
percentage?: number;
|
|
86
|
+
/** VAT category code (BT-95, BT-102) */
|
|
87
|
+
vatCategoryCode?: string;
|
|
88
|
+
/** VAT rate (BT-96, BT-103) */
|
|
89
|
+
vatRate?: number;
|
|
90
|
+
/** Reason (BT-97, BT-104) */
|
|
91
|
+
reason?: string;
|
|
92
|
+
/** Reason code (BT-98, BT-105) */
|
|
93
|
+
reasonCode?: string;
|
|
94
|
+
}
|
package/ts/readme.md
CHANGED
|
@@ -40,7 +40,7 @@ const xml = await invoice.exportXml('facturx');
|
|
|
40
40
|
| `xrechnung` | Yes | Yes | Yes | UBL export; UBL and CII import |
|
|
41
41
|
| `cii` | Yes | Yes | Yes | Generic export currently routes through the Factur-X encoder path |
|
|
42
42
|
| `facturx` | Yes | Yes | Yes | Main CII generation path |
|
|
43
|
-
| `zugferd` | Yes | Yes | Yes | Input supports v1 and v2+; the v1 decoder reads nothing out of a v1 header, so a v1 document always decodes as `invoice` (known defect) |
|
|
43
|
+
| `zugferd` | Yes | Yes | Yes | Input supports v1 and v2+; export writes the EN 16931 profile, and v2 reads back as `facturx`, as XML and from a PDF; the v1 decoder reads nothing out of a v1 header, so a v1 document always decodes as `invoice` (known defect) |
|
|
44
44
|
| `fatturapa` | Yes | No | No | Detection only at the moment |
|
|
45
45
|
|
|
46
46
|
## Important implementation notes
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The member states of the European Union, by ISO 3166-1 alpha-2 code (27
|
|
3
|
-
* since 1 February 2020). Greece is `GR` here, as in an address; its VAT
|
|
4
|
-
* identification numbers carry the prefix `EL` instead.
|
|
5
|
-
*/
|
|
6
|
-
export declare const EU_MEMBER_STATES: ReadonlySet<string>;
|
|
7
|
-
/**
|
|
8
|
-
* Whether an ISO 3166-1 alpha-2 country code is that of an EU member state
|
|
9
|
-
* @param countryCode The country code, in upper case
|
|
10
|
-
*/
|
|
11
|
-
export declare const isEuMemberState: (countryCode: string) => boolean;
|