@fin.cx/einvoice 5.0.3 → 5.1.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.js +1 -1
- package/dist_ts/einvoice.d.ts +4 -8
- package/dist_ts/einvoice.js +46 -7
- package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
- package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
- package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
- package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
- package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
- package/dist_ts/formats/semantic/semantic.validator.js +596 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
- package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
- package/dist_ts/formats/utils/currency.utils.js +250 -0
- package/dist_ts/formats/utils/decimal.d.ts +151 -0
- package/dist_ts/formats/utils/decimal.js +435 -0
- package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
- package/dist_ts/formats/validation/codelist.validator.js +188 -0
- package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
- package/dist_ts/formats/validation/conformance.harness.js +471 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
- package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
- package/dist_ts/formats/validation/facturx.validator.js +522 -0
- package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
- package/dist_ts/formats/validation/integrated.validator.js +311 -0
- package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
- package/dist_ts/formats/validation/peppol.validator.js +529 -0
- package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
- package/dist_ts/formats/validation/schematron.downloader.js +254 -0
- package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
- package/dist_ts/formats/validation/schematron.integration.js +230 -0
- package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
- package/dist_ts/formats/validation/schematron.validator.js +288 -0
- package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
- package/dist_ts/formats/validation/schematron.worker.js +185 -0
- package/dist_ts/formats/validation/validation.types.d.ts +163 -0
- package/dist_ts/formats/validation/validation.types.js +181 -0
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
- package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
- package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
- package/dist_ts/interfaces/en16931-metadata.js +6 -0
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.d.ts +6 -0
- package/dist_ts_install/download-schematron.js +62 -0
- package/dist_ts_install/download-test-samples.d.ts +19 -0
- package/dist_ts_install/download-test-samples.js +169 -0
- package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
- package/dist_ts_install/download-xrechnung-rules.js +146 -0
- package/dist_ts_install/index.d.ts +8 -0
- package/dist_ts_install/index.js +246 -0
- package/package.json +9 -3
- package/readme.md +255 -810
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +63 -14
- package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
- package/ts/formats/semantic/bt-bg.model.ts +524 -0
- package/ts/formats/semantic/semantic.adapter.ts +600 -0
- package/ts/formats/semantic/semantic.validator.ts +654 -0
- package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
- package/ts/formats/utils/currency.utils.ts +299 -0
- package/ts/formats/utils/decimal.ts +509 -0
- package/ts/formats/validation/codelist.validator.ts +317 -0
- package/ts/formats/validation/conformance.harness.ts +591 -0
- package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
- package/ts/formats/validation/facturx.validator.ts +579 -0
- package/ts/formats/validation/integrated.validator.ts +405 -0
- package/ts/formats/validation/peppol.validator.ts +589 -0
- package/ts/formats/validation/schematron.downloader.ts +304 -0
- package/ts/formats/validation/schematron.integration.ts +285 -0
- package/ts/formats/validation/schematron.validator.ts +348 -0
- package/ts/formats/validation/schematron.worker.ts +221 -0
- package/ts/formats/validation/validation.types.ts +274 -0
- package/ts/formats/validation/vat-categories.validator.ts +845 -0
- package/ts/formats/validation/xrechnung.validator.ts +494 -0
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +104 -0
- package/ts/plugins.ts +8 -0
- package/ts/tspublish.json +3 -0
- package/dist_ts/classes.decoder.d.ts +0 -40
- package/dist_ts/classes.decoder.js +0 -320
- package/dist_ts/classes.encoder.d.ts +0 -51
- package/dist_ts/classes.encoder.js +0 -293
- package/dist_ts/classes.xinvoice.d.ts +0 -136
- package/dist_ts/classes.xinvoice.js +0 -352
- package/dist_ts/formats/base.decoder.d.ts +0 -19
- package/dist_ts/formats/base.decoder.js +0 -130
- package/dist_ts/formats/base.validator.d.ts +0 -44
- package/dist_ts/formats/base.validator.js +0 -39
- package/dist_ts/formats/decoder.factory.d.ts +0 -15
- package/dist_ts/formats/decoder.factory.js +0 -46
- package/dist_ts/formats/factorx.decoder.d.ts +0 -18
- package/dist_ts/formats/factorx.decoder.js +0 -178
- package/dist_ts/formats/factorx.encoder.d.ts +0 -51
- package/dist_ts/formats/factorx.encoder.js +0 -293
- package/dist_ts/formats/facturx.decoder.d.ts +0 -18
- package/dist_ts/formats/facturx.decoder.js +0 -210
- package/dist_ts/formats/facturx.encoder.d.ts +0 -51
- package/dist_ts/formats/facturx.encoder.js +0 -293
- package/dist_ts/formats/facturx.validator.d.ts +0 -67
- package/dist_ts/formats/facturx.validator.js +0 -266
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
- package/dist_ts/formats/ubl.validator.d.ts +0 -82
- package/dist_ts/formats/ubl.validator.js +0 -306
- package/dist_ts/formats/validator.factory.d.ts +0 -18
- package/dist_ts/formats/validator.factory.js +0 -78
- package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
- package/dist_ts/formats/xinvoice.decoder.js +0 -332
- package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
- package/dist_ts/formats/xinvoice.encoder.js +0 -282
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Model Validator
|
|
3
|
+
* Validates invoices against EN16931 Business Terms and Business Groups
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ValidationResult } from '../validation/validation.types.js';
|
|
7
|
+
import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
|
|
8
|
+
import type { EInvoice } from '../../einvoice.js';
|
|
9
|
+
import { SemanticModelAdapter } from './semantic.adapter.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Business Term validation rules
|
|
13
|
+
*/
|
|
14
|
+
interface BTValidationRule {
|
|
15
|
+
btId: string;
|
|
16
|
+
description: string;
|
|
17
|
+
mandatory: boolean;
|
|
18
|
+
validate: (model: EN16931SemanticModel) => ValidationResult | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Semantic Model Validator
|
|
23
|
+
* Validates against all EN16931 Business Terms (BT) and Business Groups (BG)
|
|
24
|
+
*/
|
|
25
|
+
export class SemanticModelValidator {
|
|
26
|
+
private adapter: SemanticModelAdapter;
|
|
27
|
+
private btRules: BTValidationRule[];
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.adapter = new SemanticModelAdapter();
|
|
31
|
+
this.btRules = this.initializeBusinessTermRules();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate an invoice using the semantic model
|
|
36
|
+
*/
|
|
37
|
+
public validate(invoice: EInvoice): ValidationResult[] {
|
|
38
|
+
const results: ValidationResult[] = [];
|
|
39
|
+
|
|
40
|
+
// Convert to semantic model
|
|
41
|
+
const model = this.adapter.toSemanticModel(invoice);
|
|
42
|
+
|
|
43
|
+
// Validate all business terms
|
|
44
|
+
for (const rule of this.btRules) {
|
|
45
|
+
const result = rule.validate(model);
|
|
46
|
+
if (result) {
|
|
47
|
+
results.push(result);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate business groups
|
|
52
|
+
results.push(...this.validateBusinessGroups(model));
|
|
53
|
+
|
|
54
|
+
// Validate cardinality constraints
|
|
55
|
+
results.push(...this.validateCardinality(model));
|
|
56
|
+
|
|
57
|
+
// Validate conditional rules
|
|
58
|
+
results.push(...this.validateConditionalRules(model));
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize Business Term validation rules
|
|
65
|
+
*/
|
|
66
|
+
private initializeBusinessTermRules(): BTValidationRule[] {
|
|
67
|
+
return [
|
|
68
|
+
// Document level mandatory fields
|
|
69
|
+
{
|
|
70
|
+
btId: 'BT-1',
|
|
71
|
+
description: 'Invoice number',
|
|
72
|
+
mandatory: true,
|
|
73
|
+
validate: (model) => {
|
|
74
|
+
if (!model.documentInformation.invoiceNumber) {
|
|
75
|
+
return {
|
|
76
|
+
ruleId: 'BT-1',
|
|
77
|
+
severity: 'error',
|
|
78
|
+
message: 'Invoice number is mandatory',
|
|
79
|
+
field: 'documentInformation.invoiceNumber',
|
|
80
|
+
btReference: 'BT-1',
|
|
81
|
+
source: 'SEMANTIC'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
btId: 'BT-2',
|
|
89
|
+
description: 'Invoice issue date',
|
|
90
|
+
mandatory: true,
|
|
91
|
+
validate: (model) => {
|
|
92
|
+
if (!model.documentInformation.issueDate) {
|
|
93
|
+
return {
|
|
94
|
+
ruleId: 'BT-2',
|
|
95
|
+
severity: 'error',
|
|
96
|
+
message: 'Invoice issue date is mandatory',
|
|
97
|
+
field: 'documentInformation.issueDate',
|
|
98
|
+
btReference: 'BT-2',
|
|
99
|
+
source: 'SEMANTIC'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
btId: 'BT-3',
|
|
107
|
+
description: 'Invoice type code',
|
|
108
|
+
mandatory: true,
|
|
109
|
+
validate: (model) => {
|
|
110
|
+
if (!model.documentInformation.typeCode) {
|
|
111
|
+
return {
|
|
112
|
+
ruleId: 'BT-3',
|
|
113
|
+
severity: 'error',
|
|
114
|
+
message: 'Invoice type code is mandatory',
|
|
115
|
+
field: 'documentInformation.typeCode',
|
|
116
|
+
btReference: 'BT-3',
|
|
117
|
+
source: 'SEMANTIC'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const validCodes = ['380', '381', '383', '384', '386', '389'];
|
|
121
|
+
if (!validCodes.includes(model.documentInformation.typeCode)) {
|
|
122
|
+
return {
|
|
123
|
+
ruleId: 'BT-3',
|
|
124
|
+
severity: 'error',
|
|
125
|
+
message: `Invalid invoice type code. Must be one of: ${validCodes.join(', ')}`,
|
|
126
|
+
field: 'documentInformation.typeCode',
|
|
127
|
+
value: model.documentInformation.typeCode,
|
|
128
|
+
btReference: 'BT-3',
|
|
129
|
+
source: 'SEMANTIC'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
btId: 'BT-5',
|
|
137
|
+
description: 'Invoice currency code',
|
|
138
|
+
mandatory: true,
|
|
139
|
+
validate: (model) => {
|
|
140
|
+
if (!model.documentInformation.currencyCode) {
|
|
141
|
+
return {
|
|
142
|
+
ruleId: 'BT-5',
|
|
143
|
+
severity: 'error',
|
|
144
|
+
message: 'Invoice currency code is mandatory',
|
|
145
|
+
field: 'documentInformation.currencyCode',
|
|
146
|
+
btReference: 'BT-5',
|
|
147
|
+
source: 'SEMANTIC'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Validate ISO 4217 currency code
|
|
151
|
+
if (!/^[A-Z]{3}$/.test(model.documentInformation.currencyCode)) {
|
|
152
|
+
return {
|
|
153
|
+
ruleId: 'BT-5',
|
|
154
|
+
severity: 'error',
|
|
155
|
+
message: 'Currency code must be a valid ISO 4217 code',
|
|
156
|
+
field: 'documentInformation.currencyCode',
|
|
157
|
+
value: model.documentInformation.currencyCode,
|
|
158
|
+
btReference: 'BT-5',
|
|
159
|
+
source: 'SEMANTIC'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Seller mandatory fields
|
|
167
|
+
{
|
|
168
|
+
btId: 'BT-27',
|
|
169
|
+
description: 'Seller name',
|
|
170
|
+
mandatory: true,
|
|
171
|
+
validate: (model) => {
|
|
172
|
+
if (!model.seller?.name) {
|
|
173
|
+
return {
|
|
174
|
+
ruleId: 'BT-27',
|
|
175
|
+
severity: 'error',
|
|
176
|
+
message: 'Seller name is mandatory',
|
|
177
|
+
field: 'seller.name',
|
|
178
|
+
btReference: 'BT-27',
|
|
179
|
+
source: 'SEMANTIC'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
btId: 'BT-40',
|
|
187
|
+
description: 'Seller country code',
|
|
188
|
+
mandatory: true,
|
|
189
|
+
validate: (model) => {
|
|
190
|
+
if (!model.seller?.postalAddress?.countryCode) {
|
|
191
|
+
return {
|
|
192
|
+
ruleId: 'BT-40',
|
|
193
|
+
severity: 'error',
|
|
194
|
+
message: 'Seller country code is mandatory',
|
|
195
|
+
field: 'seller.postalAddress.countryCode',
|
|
196
|
+
btReference: 'BT-40',
|
|
197
|
+
source: 'SEMANTIC'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Validate ISO 3166-1 alpha-2 country code
|
|
201
|
+
if (!/^[A-Z]{2}$/.test(model.seller.postalAddress.countryCode)) {
|
|
202
|
+
return {
|
|
203
|
+
ruleId: 'BT-40',
|
|
204
|
+
severity: 'error',
|
|
205
|
+
message: 'Country code must be a valid ISO 3166-1 alpha-2 code',
|
|
206
|
+
field: 'seller.postalAddress.countryCode',
|
|
207
|
+
value: model.seller.postalAddress.countryCode,
|
|
208
|
+
btReference: 'BT-40',
|
|
209
|
+
source: 'SEMANTIC'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Buyer mandatory fields
|
|
217
|
+
{
|
|
218
|
+
btId: 'BT-44',
|
|
219
|
+
description: 'Buyer name',
|
|
220
|
+
mandatory: true,
|
|
221
|
+
validate: (model) => {
|
|
222
|
+
if (!model.buyer?.name) {
|
|
223
|
+
return {
|
|
224
|
+
ruleId: 'BT-44',
|
|
225
|
+
severity: 'error',
|
|
226
|
+
message: 'Buyer name is mandatory',
|
|
227
|
+
field: 'buyer.name',
|
|
228
|
+
btReference: 'BT-44',
|
|
229
|
+
source: 'SEMANTIC'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
btId: 'BT-55',
|
|
237
|
+
description: 'Buyer country code',
|
|
238
|
+
mandatory: true,
|
|
239
|
+
validate: (model) => {
|
|
240
|
+
if (!model.buyer?.postalAddress?.countryCode) {
|
|
241
|
+
return {
|
|
242
|
+
ruleId: 'BT-55',
|
|
243
|
+
severity: 'error',
|
|
244
|
+
message: 'Buyer country code is mandatory',
|
|
245
|
+
field: 'buyer.postalAddress.countryCode',
|
|
246
|
+
btReference: 'BT-55',
|
|
247
|
+
source: 'SEMANTIC'
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Validate ISO 3166-1 alpha-2 country code
|
|
251
|
+
if (!/^[A-Z]{2}$/.test(model.buyer.postalAddress.countryCode)) {
|
|
252
|
+
return {
|
|
253
|
+
ruleId: 'BT-55',
|
|
254
|
+
severity: 'error',
|
|
255
|
+
message: 'Country code must be a valid ISO 3166-1 alpha-2 code',
|
|
256
|
+
field: 'buyer.postalAddress.countryCode',
|
|
257
|
+
value: model.buyer.postalAddress.countryCode,
|
|
258
|
+
btReference: 'BT-55',
|
|
259
|
+
source: 'SEMANTIC'
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// Payment means
|
|
267
|
+
{
|
|
268
|
+
btId: 'BT-81',
|
|
269
|
+
description: 'Payment means type code',
|
|
270
|
+
mandatory: true,
|
|
271
|
+
validate: (model) => {
|
|
272
|
+
if (!model.paymentInstructions?.paymentMeansTypeCode) {
|
|
273
|
+
return {
|
|
274
|
+
ruleId: 'BT-81',
|
|
275
|
+
severity: 'error',
|
|
276
|
+
message: 'Payment means type code is mandatory',
|
|
277
|
+
field: 'paymentInstructions.paymentMeansTypeCode',
|
|
278
|
+
btReference: 'BT-81',
|
|
279
|
+
source: 'SEMANTIC'
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Document totals
|
|
287
|
+
{
|
|
288
|
+
btId: 'BT-106',
|
|
289
|
+
description: 'Sum of invoice line net amount',
|
|
290
|
+
mandatory: true,
|
|
291
|
+
validate: (model) => {
|
|
292
|
+
if (model.documentTotals?.lineExtensionAmount === undefined) {
|
|
293
|
+
return {
|
|
294
|
+
ruleId: 'BT-106',
|
|
295
|
+
severity: 'error',
|
|
296
|
+
message: 'Sum of invoice line net amount is mandatory',
|
|
297
|
+
field: 'documentTotals.lineExtensionAmount',
|
|
298
|
+
btReference: 'BT-106',
|
|
299
|
+
source: 'SEMANTIC'
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
btId: 'BT-109',
|
|
307
|
+
description: 'Invoice total amount without VAT',
|
|
308
|
+
mandatory: true,
|
|
309
|
+
validate: (model) => {
|
|
310
|
+
if (model.documentTotals?.taxExclusiveAmount === undefined) {
|
|
311
|
+
return {
|
|
312
|
+
ruleId: 'BT-109',
|
|
313
|
+
severity: 'error',
|
|
314
|
+
message: 'Invoice total amount without VAT is mandatory',
|
|
315
|
+
field: 'documentTotals.taxExclusiveAmount',
|
|
316
|
+
btReference: 'BT-109',
|
|
317
|
+
source: 'SEMANTIC'
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
btId: 'BT-112',
|
|
325
|
+
description: 'Invoice total amount with VAT',
|
|
326
|
+
mandatory: true,
|
|
327
|
+
validate: (model) => {
|
|
328
|
+
if (model.documentTotals?.taxInclusiveAmount === undefined) {
|
|
329
|
+
return {
|
|
330
|
+
ruleId: 'BT-112',
|
|
331
|
+
severity: 'error',
|
|
332
|
+
message: 'Invoice total amount with VAT is mandatory',
|
|
333
|
+
field: 'documentTotals.taxInclusiveAmount',
|
|
334
|
+
btReference: 'BT-112',
|
|
335
|
+
source: 'SEMANTIC'
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
btId: 'BT-115',
|
|
343
|
+
description: 'Amount due for payment',
|
|
344
|
+
mandatory: true,
|
|
345
|
+
validate: (model) => {
|
|
346
|
+
if (model.documentTotals?.payableAmount === undefined) {
|
|
347
|
+
return {
|
|
348
|
+
ruleId: 'BT-115',
|
|
349
|
+
severity: 'error',
|
|
350
|
+
message: 'Amount due for payment is mandatory',
|
|
351
|
+
field: 'documentTotals.payableAmount',
|
|
352
|
+
btReference: 'BT-115',
|
|
353
|
+
source: 'SEMANTIC'
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Validate Business Groups
|
|
364
|
+
*/
|
|
365
|
+
private validateBusinessGroups(model: EN16931SemanticModel): ValidationResult[] {
|
|
366
|
+
const results: ValidationResult[] = [];
|
|
367
|
+
|
|
368
|
+
// BG-4: Seller
|
|
369
|
+
if (!model.seller) {
|
|
370
|
+
results.push({
|
|
371
|
+
ruleId: 'BG-4',
|
|
372
|
+
severity: 'error',
|
|
373
|
+
message: 'Seller information is mandatory',
|
|
374
|
+
field: 'seller',
|
|
375
|
+
bgReference: 'BG-4',
|
|
376
|
+
source: 'SEMANTIC'
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// BG-5: Seller postal address
|
|
381
|
+
if (!model.seller?.postalAddress) {
|
|
382
|
+
results.push({
|
|
383
|
+
ruleId: 'BG-5',
|
|
384
|
+
severity: 'error',
|
|
385
|
+
message: 'Seller postal address is mandatory',
|
|
386
|
+
field: 'seller.postalAddress',
|
|
387
|
+
bgReference: 'BG-5',
|
|
388
|
+
source: 'SEMANTIC'
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// BG-7: Buyer
|
|
393
|
+
if (!model.buyer) {
|
|
394
|
+
results.push({
|
|
395
|
+
ruleId: 'BG-7',
|
|
396
|
+
severity: 'error',
|
|
397
|
+
message: 'Buyer information is mandatory',
|
|
398
|
+
field: 'buyer',
|
|
399
|
+
bgReference: 'BG-7',
|
|
400
|
+
source: 'SEMANTIC'
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// BG-8: Buyer postal address
|
|
405
|
+
if (!model.buyer?.postalAddress) {
|
|
406
|
+
results.push({
|
|
407
|
+
ruleId: 'BG-8',
|
|
408
|
+
severity: 'error',
|
|
409
|
+
message: 'Buyer postal address is mandatory',
|
|
410
|
+
field: 'buyer.postalAddress',
|
|
411
|
+
bgReference: 'BG-8',
|
|
412
|
+
source: 'SEMANTIC'
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// BG-16: Payment instructions
|
|
417
|
+
if (!model.paymentInstructions) {
|
|
418
|
+
results.push({
|
|
419
|
+
ruleId: 'BG-16',
|
|
420
|
+
severity: 'error',
|
|
421
|
+
message: 'Payment instructions are mandatory',
|
|
422
|
+
field: 'paymentInstructions',
|
|
423
|
+
bgReference: 'BG-16',
|
|
424
|
+
source: 'SEMANTIC'
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// BG-22: Document totals
|
|
429
|
+
if (!model.documentTotals) {
|
|
430
|
+
results.push({
|
|
431
|
+
ruleId: 'BG-22',
|
|
432
|
+
severity: 'error',
|
|
433
|
+
message: 'Document totals are mandatory',
|
|
434
|
+
field: 'documentTotals',
|
|
435
|
+
bgReference: 'BG-22',
|
|
436
|
+
source: 'SEMANTIC'
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// BG-25: Invoice lines
|
|
441
|
+
if (!model.invoiceLines || model.invoiceLines.length === 0) {
|
|
442
|
+
results.push({
|
|
443
|
+
ruleId: 'BG-25',
|
|
444
|
+
severity: 'error',
|
|
445
|
+
message: 'At least one invoice line is mandatory',
|
|
446
|
+
field: 'invoiceLines',
|
|
447
|
+
bgReference: 'BG-25',
|
|
448
|
+
source: 'SEMANTIC'
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Validate each invoice line
|
|
453
|
+
model.invoiceLines?.forEach((line, index) => {
|
|
454
|
+
// BT-126: Line identifier
|
|
455
|
+
if (!line.identifier) {
|
|
456
|
+
results.push({
|
|
457
|
+
ruleId: 'BT-126',
|
|
458
|
+
severity: 'error',
|
|
459
|
+
message: `Invoice line ${index + 1}: Identifier is mandatory`,
|
|
460
|
+
field: `invoiceLines[${index}].identifier`,
|
|
461
|
+
btReference: 'BT-126',
|
|
462
|
+
source: 'SEMANTIC'
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// BT-129: Invoiced quantity
|
|
467
|
+
if (line.invoicedQuantity === undefined) {
|
|
468
|
+
results.push({
|
|
469
|
+
ruleId: 'BT-129',
|
|
470
|
+
severity: 'error',
|
|
471
|
+
message: `Invoice line ${index + 1}: Invoiced quantity is mandatory`,
|
|
472
|
+
field: `invoiceLines[${index}].invoicedQuantity`,
|
|
473
|
+
btReference: 'BT-129',
|
|
474
|
+
source: 'SEMANTIC'
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// BT-131: Line net amount
|
|
479
|
+
if (line.lineExtensionAmount === undefined) {
|
|
480
|
+
results.push({
|
|
481
|
+
ruleId: 'BT-131',
|
|
482
|
+
severity: 'error',
|
|
483
|
+
message: `Invoice line ${index + 1}: Line net amount is mandatory`,
|
|
484
|
+
field: `invoiceLines[${index}].lineExtensionAmount`,
|
|
485
|
+
btReference: 'BT-131',
|
|
486
|
+
source: 'SEMANTIC'
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// BT-153: Item name
|
|
491
|
+
if (!line.itemInformation?.name) {
|
|
492
|
+
results.push({
|
|
493
|
+
ruleId: 'BT-153',
|
|
494
|
+
severity: 'error',
|
|
495
|
+
message: `Invoice line ${index + 1}: Item name is mandatory`,
|
|
496
|
+
field: `invoiceLines[${index}].itemInformation.name`,
|
|
497
|
+
btReference: 'BT-153',
|
|
498
|
+
source: 'SEMANTIC'
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
return results;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Validate cardinality constraints
|
|
508
|
+
*/
|
|
509
|
+
private validateCardinality(model: EN16931SemanticModel): ValidationResult[] {
|
|
510
|
+
const results: ValidationResult[] = [];
|
|
511
|
+
|
|
512
|
+
// Check for duplicate invoice lines
|
|
513
|
+
const lineIds = model.invoiceLines?.map(l => l.identifier) || [];
|
|
514
|
+
const uniqueIds = new Set(lineIds);
|
|
515
|
+
if (lineIds.length !== uniqueIds.size) {
|
|
516
|
+
results.push({
|
|
517
|
+
ruleId: 'CARD-01',
|
|
518
|
+
severity: 'error',
|
|
519
|
+
message: 'Invoice line identifiers must be unique',
|
|
520
|
+
field: 'invoiceLines',
|
|
521
|
+
source: 'SEMANTIC'
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Check VAT breakdown cardinality
|
|
526
|
+
if (model.vatBreakdown) {
|
|
527
|
+
const vatCategories = model.vatBreakdown.map(v => v.vatCategoryCode);
|
|
528
|
+
const uniqueCategories = new Set(vatCategories);
|
|
529
|
+
if (vatCategories.length !== uniqueCategories.size) {
|
|
530
|
+
results.push({
|
|
531
|
+
ruleId: 'CARD-02',
|
|
532
|
+
severity: 'error',
|
|
533
|
+
message: 'Each VAT category code must appear only once in VAT breakdown',
|
|
534
|
+
field: 'vatBreakdown',
|
|
535
|
+
source: 'SEMANTIC'
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return results;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Validate conditional rules
|
|
545
|
+
*/
|
|
546
|
+
private validateConditionalRules(model: EN16931SemanticModel): ValidationResult[] {
|
|
547
|
+
const results: ValidationResult[] = [];
|
|
548
|
+
|
|
549
|
+
// If VAT accounting currency code is present, VAT amount in accounting currency must be present
|
|
550
|
+
if (model.documentInformation.currencyCode !== model.documentInformation.currencyCode) {
|
|
551
|
+
if (!model.documentTotals?.taxInclusiveAmount) {
|
|
552
|
+
results.push({
|
|
553
|
+
ruleId: 'COND-01',
|
|
554
|
+
severity: 'error',
|
|
555
|
+
message: 'When VAT accounting currency differs from invoice currency, VAT amount in accounting currency is mandatory',
|
|
556
|
+
field: 'documentTotals.taxInclusiveAmount',
|
|
557
|
+
source: 'SEMANTIC'
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// If credit note, there should be a preceding invoice reference
|
|
563
|
+
if (model.documentInformation.typeCode === '381') {
|
|
564
|
+
if (!model.references?.precedingInvoices || model.references.precedingInvoices.length === 0) {
|
|
565
|
+
results.push({
|
|
566
|
+
ruleId: 'COND-02',
|
|
567
|
+
severity: 'warning',
|
|
568
|
+
message: 'Credit notes should reference the original invoice',
|
|
569
|
+
field: 'references.precedingInvoices',
|
|
570
|
+
source: 'SEMANTIC'
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// If tax representative is present, certain fields are mandatory
|
|
576
|
+
if (model.taxRepresentative) {
|
|
577
|
+
if (!model.taxRepresentative.vatIdentifier) {
|
|
578
|
+
results.push({
|
|
579
|
+
ruleId: 'COND-03',
|
|
580
|
+
severity: 'error',
|
|
581
|
+
message: 'Tax representative VAT identifier is mandatory when tax representative is present',
|
|
582
|
+
field: 'taxRepresentative.vatIdentifier',
|
|
583
|
+
source: 'SEMANTIC'
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// VAT exemption requires exemption reason
|
|
589
|
+
if (model.vatBreakdown) {
|
|
590
|
+
for (const vat of model.vatBreakdown) {
|
|
591
|
+
if (vat.vatCategoryCode === 'E' && !vat.vatExemptionReasonText && !vat.vatExemptionReasonCode) {
|
|
592
|
+
results.push({
|
|
593
|
+
ruleId: 'COND-04',
|
|
594
|
+
severity: 'error',
|
|
595
|
+
message: 'VAT exemption requires exemption reason text or code',
|
|
596
|
+
field: 'vatBreakdown.vatExemptionReasonText',
|
|
597
|
+
source: 'SEMANTIC'
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return results;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get semantic model from invoice
|
|
608
|
+
*/
|
|
609
|
+
public getSemanticModel(invoice: EInvoice): EN16931SemanticModel {
|
|
610
|
+
return this.adapter.toSemanticModel(invoice);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Create invoice from semantic model
|
|
615
|
+
*/
|
|
616
|
+
public createInvoice(model: EN16931SemanticModel): EInvoice {
|
|
617
|
+
return this.adapter.fromSemanticModel(model);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Get BT/BG mapping for an invoice
|
|
622
|
+
*/
|
|
623
|
+
public getBusinessTermMapping(invoice: EInvoice): Map<string, any> {
|
|
624
|
+
const model = this.adapter.toSemanticModel(invoice);
|
|
625
|
+
const mapping = new Map<string, any>();
|
|
626
|
+
|
|
627
|
+
// Map all business terms
|
|
628
|
+
mapping.set('BT-1', model.documentInformation.invoiceNumber);
|
|
629
|
+
mapping.set('BT-2', model.documentInformation.issueDate);
|
|
630
|
+
mapping.set('BT-3', model.documentInformation.typeCode);
|
|
631
|
+
mapping.set('BT-5', model.documentInformation.currencyCode);
|
|
632
|
+
mapping.set('BT-10', model.references?.buyerReference);
|
|
633
|
+
mapping.set('BT-27', model.seller?.name);
|
|
634
|
+
mapping.set('BT-40', model.seller?.postalAddress?.countryCode);
|
|
635
|
+
mapping.set('BT-44', model.buyer?.name);
|
|
636
|
+
mapping.set('BT-55', model.buyer?.postalAddress?.countryCode);
|
|
637
|
+
mapping.set('BT-81', model.paymentInstructions?.paymentMeansTypeCode);
|
|
638
|
+
mapping.set('BT-106', model.documentTotals?.lineExtensionAmount);
|
|
639
|
+
mapping.set('BT-109', model.documentTotals?.taxExclusiveAmount);
|
|
640
|
+
mapping.set('BT-112', model.documentTotals?.taxInclusiveAmount);
|
|
641
|
+
mapping.set('BT-115', model.documentTotals?.payableAmount);
|
|
642
|
+
|
|
643
|
+
// Map business groups
|
|
644
|
+
mapping.set('BG-4', model.seller);
|
|
645
|
+
mapping.set('BG-5', model.seller?.postalAddress);
|
|
646
|
+
mapping.set('BG-7', model.buyer);
|
|
647
|
+
mapping.set('BG-8', model.buyer?.postalAddress);
|
|
648
|
+
mapping.set('BG-16', model.paymentInstructions);
|
|
649
|
+
mapping.set('BG-22', model.documentTotals);
|
|
650
|
+
mapping.set('BG-25', model.invoiceLines);
|
|
651
|
+
|
|
652
|
+
return mapping;
|
|
653
|
+
}
|
|
654
|
+
}
|