@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,694 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
3
|
+
import type { EInvoice } from '../../einvoice.js';
|
|
4
|
+
import { CurrencyCalculator, areMonetaryValuesEqual } from '../utils/currency.utils.js';
|
|
5
|
+
import { DecimalCurrencyCalculator } from '../utils/currency.calculator.decimal.js';
|
|
6
|
+
import { Decimal } from '../utils/decimal.js';
|
|
7
|
+
import type { ValidationResult, ValidationOptions } from './validation.types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* EN16931 Business Rules Validator
|
|
11
|
+
* Implements the full set of EN16931 business rules for invoice validation
|
|
12
|
+
*/
|
|
13
|
+
export class EN16931BusinessRulesValidator {
|
|
14
|
+
private results: ValidationResult[] = [];
|
|
15
|
+
private currencyCalculator?: CurrencyCalculator;
|
|
16
|
+
private decimalCalculator?: DecimalCurrencyCalculator;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate an invoice against EN16931 business rules
|
|
20
|
+
*/
|
|
21
|
+
public validate(invoice: EInvoice, options: ValidationOptions = {}): ValidationResult[] {
|
|
22
|
+
this.results = [];
|
|
23
|
+
|
|
24
|
+
// Initialize currency calculators if currency is available
|
|
25
|
+
if (invoice.currency) {
|
|
26
|
+
this.currencyCalculator = new CurrencyCalculator(invoice.currency);
|
|
27
|
+
this.decimalCalculator = new DecimalCurrencyCalculator(invoice.currency);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Document level rules (BR-01 to BR-65)
|
|
31
|
+
this.validateDocumentRules(invoice);
|
|
32
|
+
|
|
33
|
+
// Calculation rules (BR-CO-*)
|
|
34
|
+
if (options.checkCalculations !== false) {
|
|
35
|
+
this.validateCalculationRules(invoice);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// VAT rules (BR-S-*, BR-Z-*, BR-E-*, BR-AE-*, BR-IC-*, BR-G-*, BR-O-*)
|
|
39
|
+
if (options.checkVAT !== false) {
|
|
40
|
+
this.validateVATRules(invoice);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Line level rules (BR-21 to BR-30)
|
|
44
|
+
this.validateLineRules(invoice);
|
|
45
|
+
|
|
46
|
+
// Allowances and charges rules
|
|
47
|
+
if (options.checkAllowances !== false) {
|
|
48
|
+
this.validateAllowancesCharges(invoice);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return this.results;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate document level rules (BR-01 to BR-65)
|
|
56
|
+
*/
|
|
57
|
+
private validateDocumentRules(invoice: EInvoice): void {
|
|
58
|
+
// BR-01: An Invoice shall have a Specification identifier (BT-24)
|
|
59
|
+
if (!invoice.metadata?.customizationId) {
|
|
60
|
+
this.addError('BR-01', 'Invoice must have a Specification identifier (CustomizationID)', 'customizationId');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// BR-02: An Invoice shall have an Invoice number (BT-1)
|
|
64
|
+
if (!invoice.accountingDocId) {
|
|
65
|
+
this.addError('BR-02', 'Invoice must have an Invoice number', 'accountingDocId');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// BR-03: An Invoice shall have an Invoice issue date (BT-2)
|
|
69
|
+
if (!invoice.date) {
|
|
70
|
+
this.addError('BR-03', 'Invoice must have an issue date', 'date');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// BR-04: An Invoice shall have an Invoice type code (BT-3)
|
|
74
|
+
if (!invoice.accountingDocType) {
|
|
75
|
+
this.addError('BR-04', 'Invoice must have a type code', 'accountingDocType');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// BR-05: An Invoice shall have an Invoice currency code (BT-5)
|
|
79
|
+
if (!invoice.currency) {
|
|
80
|
+
this.addError('BR-05', 'Invoice must have a currency code', 'currency');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// BR-06: An Invoice shall contain the Seller name (BT-27)
|
|
84
|
+
if (!invoice.from?.name) {
|
|
85
|
+
this.addError('BR-06', 'Invoice must contain the Seller name', 'from.name');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// BR-07: An Invoice shall contain the Buyer name (BT-44)
|
|
89
|
+
if (!invoice.to?.name) {
|
|
90
|
+
this.addError('BR-07', 'Invoice must contain the Buyer name', 'to.name');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// BR-08: An Invoice shall contain the Seller postal address (BG-5)
|
|
94
|
+
if (!invoice.from?.address) {
|
|
95
|
+
this.addError('BR-08', 'Invoice must contain the Seller postal address', 'from.address');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// BR-09: The Seller postal address shall contain a Seller country code (BT-40)
|
|
99
|
+
if (!invoice.from?.address?.countryCode) {
|
|
100
|
+
this.addError('BR-09', 'Seller postal address must contain a country code', 'from.address.countryCode');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// BR-10: An Invoice shall contain the Buyer postal address (BG-8)
|
|
104
|
+
if (!invoice.to?.address) {
|
|
105
|
+
this.addError('BR-10', 'Invoice must contain the Buyer postal address', 'to.address');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// BR-11: The Buyer postal address shall contain a Buyer country code (BT-55)
|
|
109
|
+
if (!invoice.to?.address?.countryCode) {
|
|
110
|
+
this.addError('BR-11', 'Buyer postal address must contain a country code', 'to.address.countryCode');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// BR-16: An Invoice shall have at least one Invoice line (BG-25)
|
|
114
|
+
if (!invoice.items || invoice.items.length === 0) {
|
|
115
|
+
this.addError('BR-16', 'Invoice must have at least one invoice line', 'items');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate calculation rules (BR-CO-*)
|
|
121
|
+
*/
|
|
122
|
+
private validateCalculationRules(invoice: EInvoice): void {
|
|
123
|
+
if (!invoice.items || invoice.items.length === 0) return;
|
|
124
|
+
|
|
125
|
+
// Use decimal calculator for precise calculations
|
|
126
|
+
const useDecimal = this.decimalCalculator !== undefined;
|
|
127
|
+
|
|
128
|
+
// BR-CO-10: Sum of Invoice line net amount = Σ(Invoice line net amount)
|
|
129
|
+
const calculatedLineTotal = useDecimal
|
|
130
|
+
? this.calculateLineTotalDecimal(invoice.items)
|
|
131
|
+
: this.calculateLineTotal(invoice.items);
|
|
132
|
+
const declaredLineTotal = useDecimal
|
|
133
|
+
? new Decimal(invoice.totalNet || 0)
|
|
134
|
+
: invoice.totalNet || 0;
|
|
135
|
+
|
|
136
|
+
const isEqual = useDecimal
|
|
137
|
+
? this.decimalCalculator!.areEqual(calculatedLineTotal, declaredLineTotal)
|
|
138
|
+
: this.currencyCalculator
|
|
139
|
+
? this.currencyCalculator.areEqual(calculatedLineTotal as number, declaredLineTotal as number)
|
|
140
|
+
: Math.abs((calculatedLineTotal as number) - (declaredLineTotal as number)) < 0.01;
|
|
141
|
+
|
|
142
|
+
if (!isEqual) {
|
|
143
|
+
this.addError(
|
|
144
|
+
'BR-CO-10',
|
|
145
|
+
`Sum of line net amounts (${useDecimal ? (calculatedLineTotal as Decimal).toFixed(2) : (calculatedLineTotal as number).toFixed(2)}) does not match declared total (${useDecimal ? (declaredLineTotal as Decimal).toFixed(2) : (declaredLineTotal as number).toFixed(2)})`,
|
|
146
|
+
'totalNet',
|
|
147
|
+
useDecimal ? (declaredLineTotal as Decimal).toNumber() : declaredLineTotal as number,
|
|
148
|
+
useDecimal ? (calculatedLineTotal as Decimal).toNumber() : calculatedLineTotal as number
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// BR-CO-11: Sum of allowances on document level
|
|
153
|
+
const documentAllowances = useDecimal
|
|
154
|
+
? this.calculateDocumentAllowancesDecimal(invoice)
|
|
155
|
+
: this.calculateDocumentAllowances(invoice);
|
|
156
|
+
|
|
157
|
+
// BR-CO-12: Sum of charges on document level
|
|
158
|
+
const documentCharges = useDecimal
|
|
159
|
+
? this.calculateDocumentChargesDecimal(invoice)
|
|
160
|
+
: this.calculateDocumentCharges(invoice);
|
|
161
|
+
|
|
162
|
+
// BR-CO-13: Invoice total without VAT = Σ(line) - allowances + charges
|
|
163
|
+
const expectedTaxExclusive = useDecimal
|
|
164
|
+
? (calculatedLineTotal as Decimal).subtract(documentAllowances).add(documentCharges)
|
|
165
|
+
: (calculatedLineTotal as number) - (documentAllowances as number) + (documentCharges as number);
|
|
166
|
+
const declaredTaxExclusive = useDecimal
|
|
167
|
+
? new Decimal(invoice.totalNet || 0)
|
|
168
|
+
: invoice.totalNet || 0;
|
|
169
|
+
|
|
170
|
+
const isTaxExclusiveEqual = useDecimal
|
|
171
|
+
? this.decimalCalculator!.areEqual(expectedTaxExclusive, declaredTaxExclusive)
|
|
172
|
+
: this.currencyCalculator
|
|
173
|
+
? this.currencyCalculator.areEqual(expectedTaxExclusive as number, declaredTaxExclusive as number)
|
|
174
|
+
: Math.abs((expectedTaxExclusive as number) - (declaredTaxExclusive as number)) < 0.01;
|
|
175
|
+
|
|
176
|
+
if (!isTaxExclusiveEqual) {
|
|
177
|
+
this.addError(
|
|
178
|
+
'BR-CO-13',
|
|
179
|
+
`Tax exclusive amount (${useDecimal ? (declaredTaxExclusive as Decimal).toFixed(2) : (declaredTaxExclusive as number).toFixed(2)}) does not match calculation (${useDecimal ? (expectedTaxExclusive as Decimal).toFixed(2) : (expectedTaxExclusive as number).toFixed(2)})`,
|
|
180
|
+
'totalNet',
|
|
181
|
+
useDecimal ? (declaredTaxExclusive as Decimal).toNumber() : declaredTaxExclusive as number,
|
|
182
|
+
useDecimal ? (expectedTaxExclusive as Decimal).toNumber() : expectedTaxExclusive as number
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// BR-CO-14: Invoice total VAT amount = Σ(VAT category tax amount)
|
|
187
|
+
const calculatedVAT = useDecimal
|
|
188
|
+
? this.calculateTotalVATDecimal(invoice)
|
|
189
|
+
: this.calculateTotalVAT(invoice);
|
|
190
|
+
const declaredVAT = useDecimal
|
|
191
|
+
? new Decimal(invoice.totalVat || 0)
|
|
192
|
+
: invoice.totalVat || 0;
|
|
193
|
+
|
|
194
|
+
const isVATEqual = useDecimal
|
|
195
|
+
? this.decimalCalculator!.areEqual(calculatedVAT, declaredVAT)
|
|
196
|
+
: this.currencyCalculator
|
|
197
|
+
? this.currencyCalculator.areEqual(calculatedVAT as number, declaredVAT as number)
|
|
198
|
+
: Math.abs((calculatedVAT as number) - (declaredVAT as number)) < 0.01;
|
|
199
|
+
|
|
200
|
+
if (!isVATEqual) {
|
|
201
|
+
this.addError(
|
|
202
|
+
'BR-CO-14',
|
|
203
|
+
`Total VAT (${useDecimal ? (declaredVAT as Decimal).toFixed(2) : (declaredVAT as number).toFixed(2)}) does not match calculation (${useDecimal ? (calculatedVAT as Decimal).toFixed(2) : (calculatedVAT as number).toFixed(2)})`,
|
|
204
|
+
'totalVat',
|
|
205
|
+
useDecimal ? (declaredVAT as Decimal).toNumber() : declaredVAT as number,
|
|
206
|
+
useDecimal ? (calculatedVAT as Decimal).toNumber() : calculatedVAT as number
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// BR-CO-15: Invoice total with VAT = Invoice total without VAT + Invoice total VAT
|
|
211
|
+
const expectedGrossTotal = useDecimal
|
|
212
|
+
? (expectedTaxExclusive as Decimal).add(calculatedVAT)
|
|
213
|
+
: (expectedTaxExclusive as number) + (calculatedVAT as number);
|
|
214
|
+
const declaredGrossTotal = useDecimal
|
|
215
|
+
? new Decimal(invoice.totalGross || 0)
|
|
216
|
+
: invoice.totalGross || 0;
|
|
217
|
+
|
|
218
|
+
const isGrossEqual = useDecimal
|
|
219
|
+
? this.decimalCalculator!.areEqual(expectedGrossTotal, declaredGrossTotal)
|
|
220
|
+
: this.currencyCalculator
|
|
221
|
+
? this.currencyCalculator.areEqual(expectedGrossTotal as number, declaredGrossTotal as number)
|
|
222
|
+
: Math.abs((expectedGrossTotal as number) - (declaredGrossTotal as number)) < 0.01;
|
|
223
|
+
|
|
224
|
+
if (!isGrossEqual) {
|
|
225
|
+
this.addError(
|
|
226
|
+
'BR-CO-15',
|
|
227
|
+
`Gross total (${useDecimal ? (declaredGrossTotal as Decimal).toFixed(2) : (declaredGrossTotal as number).toFixed(2)}) does not match calculation (${useDecimal ? (expectedGrossTotal as Decimal).toFixed(2) : (expectedGrossTotal as number).toFixed(2)})`,
|
|
228
|
+
'totalGross',
|
|
229
|
+
useDecimal ? (declaredGrossTotal as Decimal).toNumber() : declaredGrossTotal as number,
|
|
230
|
+
useDecimal ? (expectedGrossTotal as Decimal).toNumber() : expectedGrossTotal as number
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// BR-CO-16: Amount due for payment = Invoice total with VAT - Paid amount
|
|
235
|
+
const paidAmount = useDecimal
|
|
236
|
+
? new Decimal(invoice.metadata?.paidAmount || 0)
|
|
237
|
+
: invoice.metadata?.paidAmount || 0;
|
|
238
|
+
const expectedDueAmount = useDecimal
|
|
239
|
+
? (expectedGrossTotal as Decimal).subtract(paidAmount)
|
|
240
|
+
: (expectedGrossTotal as number) - (paidAmount as number);
|
|
241
|
+
const declaredDueAmount = useDecimal
|
|
242
|
+
? new Decimal(invoice.metadata?.amountDue || (useDecimal ? (expectedGrossTotal as Decimal).toNumber() : expectedGrossTotal))
|
|
243
|
+
: invoice.metadata?.amountDue || expectedGrossTotal;
|
|
244
|
+
|
|
245
|
+
const isDueEqual = useDecimal
|
|
246
|
+
? this.decimalCalculator!.areEqual(expectedDueAmount, declaredDueAmount)
|
|
247
|
+
: this.currencyCalculator
|
|
248
|
+
? this.currencyCalculator.areEqual(expectedDueAmount as number, declaredDueAmount as number)
|
|
249
|
+
: Math.abs((expectedDueAmount as number) - (declaredDueAmount as number)) < 0.01;
|
|
250
|
+
|
|
251
|
+
if (!isDueEqual) {
|
|
252
|
+
this.addError(
|
|
253
|
+
'BR-CO-16',
|
|
254
|
+
`Amount due (${useDecimal ? (declaredDueAmount as Decimal).toFixed(2) : (declaredDueAmount as number).toFixed(2)}) does not match calculation (${useDecimal ? (expectedDueAmount as Decimal).toFixed(2) : (expectedDueAmount as number).toFixed(2)})`,
|
|
255
|
+
'amountDue',
|
|
256
|
+
useDecimal ? (declaredDueAmount as Decimal).toNumber() : declaredDueAmount as number,
|
|
257
|
+
useDecimal ? (expectedDueAmount as Decimal).toNumber() : expectedDueAmount as number
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Validate VAT rules
|
|
264
|
+
*/
|
|
265
|
+
private validateVATRules(invoice: EInvoice): void {
|
|
266
|
+
const useDecimal = this.decimalCalculator !== undefined;
|
|
267
|
+
|
|
268
|
+
// Group items by VAT rate
|
|
269
|
+
const vatGroups = this.groupItemsByVAT(invoice.items || []);
|
|
270
|
+
|
|
271
|
+
// BR-S-01: An Invoice that contains an Invoice line where VAT category code is "Standard rated"
|
|
272
|
+
// shall contain in the VAT breakdown at least one VAT category code equal to "Standard rated"
|
|
273
|
+
const hasStandardRatedLine = invoice.items?.some(item =>
|
|
274
|
+
item.vatPercentage && item.vatPercentage > 0
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (hasStandardRatedLine) {
|
|
278
|
+
const hasStandardRatedBreakdown = invoice.taxBreakdown?.some(breakdown =>
|
|
279
|
+
breakdown.taxPercent && breakdown.taxPercent > 0
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (!hasStandardRatedBreakdown) {
|
|
283
|
+
this.addError(
|
|
284
|
+
'BR-S-01',
|
|
285
|
+
'Invoice with standard rated lines must have standard rated VAT breakdown',
|
|
286
|
+
'taxBreakdown'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// BR-S-02: VAT category taxable amount for standard rated
|
|
292
|
+
// BR-S-03: VAT category tax amount for standard rated
|
|
293
|
+
vatGroups.forEach((group, rate) => {
|
|
294
|
+
if (rate > 0) { // Standard rated
|
|
295
|
+
const expectedTaxableAmount = useDecimal
|
|
296
|
+
? group.reduce((sum, item) => {
|
|
297
|
+
const unitPrice = new Decimal(item.unitNetPrice);
|
|
298
|
+
const quantity = new Decimal(item.unitQuantity);
|
|
299
|
+
return sum.add(unitPrice.multiply(quantity));
|
|
300
|
+
}, Decimal.ZERO)
|
|
301
|
+
: group.reduce((sum, item) =>
|
|
302
|
+
sum + (item.unitNetPrice * item.unitQuantity), 0
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const expectedTaxAmount = useDecimal
|
|
306
|
+
? this.decimalCalculator!.calculateVAT(expectedTaxableAmount, new Decimal(rate))
|
|
307
|
+
: (expectedTaxableAmount as number) * (rate / 100);
|
|
308
|
+
|
|
309
|
+
// Find corresponding breakdown
|
|
310
|
+
const breakdown = invoice.taxBreakdown?.find(b =>
|
|
311
|
+
Math.abs((b.taxPercent || 0) - rate) < 0.01
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (breakdown) {
|
|
315
|
+
const isTaxableEqual = useDecimal
|
|
316
|
+
? this.decimalCalculator!.areEqual(expectedTaxableAmount, breakdown.netAmount)
|
|
317
|
+
: this.currencyCalculator
|
|
318
|
+
? this.currencyCalculator.areEqual(breakdown.netAmount, expectedTaxableAmount as number)
|
|
319
|
+
: Math.abs(breakdown.netAmount - (expectedTaxableAmount as number)) < 0.01;
|
|
320
|
+
|
|
321
|
+
if (!isTaxableEqual) {
|
|
322
|
+
this.addError(
|
|
323
|
+
'BR-S-02',
|
|
324
|
+
`VAT taxable amount for ${rate}% incorrect`,
|
|
325
|
+
'taxBreakdown.netAmount',
|
|
326
|
+
breakdown.netAmount,
|
|
327
|
+
useDecimal ? (expectedTaxableAmount as Decimal).toNumber() : expectedTaxableAmount as number
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const isTaxEqual = useDecimal
|
|
332
|
+
? this.decimalCalculator!.areEqual(expectedTaxAmount, breakdown.taxAmount)
|
|
333
|
+
: this.currencyCalculator
|
|
334
|
+
? this.currencyCalculator.areEqual(breakdown.taxAmount, expectedTaxAmount as number)
|
|
335
|
+
: Math.abs(breakdown.taxAmount - (expectedTaxAmount as number)) < 0.01;
|
|
336
|
+
|
|
337
|
+
if (!isTaxEqual) {
|
|
338
|
+
this.addError(
|
|
339
|
+
'BR-S-03',
|
|
340
|
+
`VAT tax amount for ${rate}% incorrect`,
|
|
341
|
+
'taxBreakdown.vatAmount',
|
|
342
|
+
breakdown.taxAmount,
|
|
343
|
+
useDecimal ? (expectedTaxAmount as Decimal).toNumber() : expectedTaxAmount as number
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// BR-Z-01: Zero rated VAT rules
|
|
351
|
+
const hasZeroRatedLine = invoice.items?.some(item =>
|
|
352
|
+
item.vatPercentage === 0
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (hasZeroRatedLine) {
|
|
356
|
+
const hasZeroRatedBreakdown = invoice.taxBreakdown?.some(breakdown =>
|
|
357
|
+
breakdown.taxPercent === 0
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (!hasZeroRatedBreakdown) {
|
|
361
|
+
this.addError(
|
|
362
|
+
'BR-Z-01',
|
|
363
|
+
'Invoice with zero rated lines must have zero rated VAT breakdown',
|
|
364
|
+
'taxBreakdown'
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Validate line level rules (BR-21 to BR-30)
|
|
372
|
+
*/
|
|
373
|
+
private validateLineRules(invoice: EInvoice): void {
|
|
374
|
+
invoice.items?.forEach((item, index) => {
|
|
375
|
+
// BR-21: Each Invoice line shall have an Invoice line identifier
|
|
376
|
+
if (!item.position && item.position !== 0) {
|
|
377
|
+
this.addError(
|
|
378
|
+
'BR-21',
|
|
379
|
+
`Invoice line ${index + 1} must have an identifier`,
|
|
380
|
+
`items[${index}].id`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// BR-22: Each Invoice line shall have an Item name
|
|
385
|
+
if (!item.name) {
|
|
386
|
+
this.addError(
|
|
387
|
+
'BR-22',
|
|
388
|
+
`Invoice line ${index + 1} must have an item name`,
|
|
389
|
+
`items[${index}].name`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// BR-23: An Invoice line shall have an Invoiced quantity
|
|
394
|
+
if (item.unitQuantity === undefined || item.unitQuantity === null) {
|
|
395
|
+
this.addError(
|
|
396
|
+
'BR-23',
|
|
397
|
+
`Invoice line ${index + 1} must have a quantity`,
|
|
398
|
+
`items[${index}].quantity`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// BR-24: An Invoice line shall have an Invoiced quantity unit of measure code
|
|
403
|
+
if (!item.unitType) {
|
|
404
|
+
this.addError(
|
|
405
|
+
'BR-24',
|
|
406
|
+
`Invoice line ${index + 1} must have a unit of measure code`,
|
|
407
|
+
`items[${index}].unitCode`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// BR-25: An Invoice line shall have an Invoice line net amount
|
|
412
|
+
const lineNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
413
|
+
if (isNaN(lineNetAmount)) {
|
|
414
|
+
this.addError(
|
|
415
|
+
'BR-25',
|
|
416
|
+
`Invoice line ${index + 1} must have a valid net amount`,
|
|
417
|
+
`items[${index}]`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// BR-26: Each Invoice line shall have an Invoice line VAT category code
|
|
422
|
+
if (item.vatPercentage === undefined) {
|
|
423
|
+
this.addError(
|
|
424
|
+
'BR-26',
|
|
425
|
+
`Invoice line ${index + 1} must have a VAT category code`,
|
|
426
|
+
`items[${index}].vatPercentage`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// BR-27: Invoice line net price shall be present
|
|
431
|
+
if (item.unitNetPrice === undefined || item.unitNetPrice === null) {
|
|
432
|
+
this.addError(
|
|
433
|
+
'BR-27',
|
|
434
|
+
`Invoice line ${index + 1} must have a net price`,
|
|
435
|
+
`items[${index}].unitPrice`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// BR-28: Item price base quantity shall be greater than zero
|
|
440
|
+
const baseQuantity = 1; // Default to 1 as TAccountingDocItem doesn't have priceBaseQuantity
|
|
441
|
+
if (baseQuantity <= 0) {
|
|
442
|
+
this.addError(
|
|
443
|
+
'BR-28',
|
|
444
|
+
`Invoice line ${index + 1} price base quantity must be greater than zero`,
|
|
445
|
+
`items[${index}].metadata.priceBaseQuantity`,
|
|
446
|
+
baseQuantity,
|
|
447
|
+
'> 0'
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Validate allowances and charges
|
|
455
|
+
*/
|
|
456
|
+
private validateAllowancesCharges(invoice: EInvoice): void {
|
|
457
|
+
// BR-31: Document level allowance shall have an amount
|
|
458
|
+
invoice.metadata?.allowances?.forEach((allowance: any, index: number) => {
|
|
459
|
+
if (!allowance.amount && allowance.amount !== 0) {
|
|
460
|
+
this.addError(
|
|
461
|
+
'BR-31',
|
|
462
|
+
`Document allowance ${index + 1} must have an amount`,
|
|
463
|
+
`metadata.allowances[${index}].amount`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// BR-32: Document level allowance shall have VAT category code
|
|
468
|
+
if (!allowance.vatCategoryCode) {
|
|
469
|
+
this.addError(
|
|
470
|
+
'BR-32',
|
|
471
|
+
`Document allowance ${index + 1} must have a VAT category code`,
|
|
472
|
+
`metadata.allowances[${index}].vatCategoryCode`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// BR-33: Document level allowance shall have a reason
|
|
477
|
+
if (!allowance.reason) {
|
|
478
|
+
this.addError(
|
|
479
|
+
'BR-33',
|
|
480
|
+
`Document allowance ${index + 1} must have a reason`,
|
|
481
|
+
`metadata.allowances[${index}].reason`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// BR-36: Document level charge shall have an amount
|
|
487
|
+
invoice.metadata?.charges?.forEach((charge: any, index: number) => {
|
|
488
|
+
if (!charge.amount && charge.amount !== 0) {
|
|
489
|
+
this.addError(
|
|
490
|
+
'BR-36',
|
|
491
|
+
`Document charge ${index + 1} must have an amount`,
|
|
492
|
+
`metadata.charges[${index}].amount`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// BR-37: Document level charge shall have VAT category code
|
|
497
|
+
if (!charge.vatCategoryCode) {
|
|
498
|
+
this.addError(
|
|
499
|
+
'BR-37',
|
|
500
|
+
`Document charge ${index + 1} must have a VAT category code`,
|
|
501
|
+
`metadata.charges[${index}].vatCategoryCode`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// BR-38: Document level charge shall have a reason
|
|
506
|
+
if (!charge.reason) {
|
|
507
|
+
this.addError(
|
|
508
|
+
'BR-38',
|
|
509
|
+
`Document charge ${index + 1} must have a reason`,
|
|
510
|
+
`metadata.charges[${index}].reason`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Helper methods
|
|
517
|
+
|
|
518
|
+
private calculateLineTotal(items: TAccountingDocItem[]): number {
|
|
519
|
+
return items.reduce((sum, item) => {
|
|
520
|
+
const lineTotal = (item.unitNetPrice || 0) * (item.unitQuantity || 0);
|
|
521
|
+
const rounded = this.currencyCalculator
|
|
522
|
+
? this.currencyCalculator.round(lineTotal)
|
|
523
|
+
: lineTotal;
|
|
524
|
+
return sum + rounded;
|
|
525
|
+
}, 0);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Calculate line total using decimal arithmetic for precision
|
|
530
|
+
*/
|
|
531
|
+
private calculateLineTotalDecimal(items: TAccountingDocItem[]): Decimal {
|
|
532
|
+
let total = Decimal.ZERO;
|
|
533
|
+
|
|
534
|
+
for (const item of items) {
|
|
535
|
+
const unitPrice = new Decimal(item.unitNetPrice || 0);
|
|
536
|
+
const quantity = new Decimal(item.unitQuantity || 0);
|
|
537
|
+
const lineTotal = unitPrice.multiply(quantity);
|
|
538
|
+
total = total.add(this.decimalCalculator!.round(lineTotal));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return total;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Calculate document allowances using decimal arithmetic
|
|
546
|
+
*/
|
|
547
|
+
private calculateDocumentAllowancesDecimal(invoice: EInvoice): Decimal {
|
|
548
|
+
if (!invoice.metadata?.allowances) {
|
|
549
|
+
return Decimal.ZERO;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let total = Decimal.ZERO;
|
|
553
|
+
for (const allowance of invoice.metadata.allowances) {
|
|
554
|
+
const amount = new Decimal(allowance.amount || 0);
|
|
555
|
+
total = total.add(this.decimalCalculator!.round(amount));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return total;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Calculate document charges using decimal arithmetic
|
|
563
|
+
*/
|
|
564
|
+
private calculateDocumentChargesDecimal(invoice: EInvoice): Decimal {
|
|
565
|
+
if (!invoice.metadata?.charges) {
|
|
566
|
+
return Decimal.ZERO;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let total = Decimal.ZERO;
|
|
570
|
+
for (const charge of invoice.metadata.charges) {
|
|
571
|
+
const amount = new Decimal(charge.amount || 0);
|
|
572
|
+
total = total.add(this.decimalCalculator!.round(amount));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return total;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Calculate total VAT using decimal arithmetic
|
|
580
|
+
*/
|
|
581
|
+
private calculateTotalVATDecimal(invoice: EInvoice): Decimal {
|
|
582
|
+
let totalVAT = Decimal.ZERO;
|
|
583
|
+
|
|
584
|
+
// Group items by VAT rate
|
|
585
|
+
const vatGroups = new Map<string, Decimal>();
|
|
586
|
+
|
|
587
|
+
for (const item of invoice.items || []) {
|
|
588
|
+
const vatRate = item.vatPercentage || 0;
|
|
589
|
+
const rateKey = vatRate.toString();
|
|
590
|
+
|
|
591
|
+
const unitPrice = new Decimal(item.unitNetPrice || 0);
|
|
592
|
+
const quantity = new Decimal(item.unitQuantity || 0);
|
|
593
|
+
const lineNet = unitPrice.multiply(quantity);
|
|
594
|
+
|
|
595
|
+
if (vatGroups.has(rateKey)) {
|
|
596
|
+
vatGroups.set(rateKey, vatGroups.get(rateKey)!.add(lineNet));
|
|
597
|
+
} else {
|
|
598
|
+
vatGroups.set(rateKey, lineNet);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Calculate VAT for each group
|
|
603
|
+
for (const [rateKey, baseAmount] of vatGroups) {
|
|
604
|
+
const rate = new Decimal(rateKey);
|
|
605
|
+
const vat = this.decimalCalculator!.calculateVAT(baseAmount, rate);
|
|
606
|
+
totalVAT = totalVAT.add(vat);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return totalVAT;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private calculateDocumentAllowances(invoice: EInvoice): number {
|
|
613
|
+
return invoice.metadata?.allowances?.reduce((sum: number, allowance: any) =>
|
|
614
|
+
sum + (allowance.amount || 0), 0
|
|
615
|
+
) || 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private calculateDocumentCharges(invoice: EInvoice): number {
|
|
619
|
+
return invoice.metadata?.charges?.reduce((sum: number, charge: any) =>
|
|
620
|
+
sum + (charge.amount || 0), 0
|
|
621
|
+
) || 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private calculateTotalVAT(invoice: EInvoice): number {
|
|
625
|
+
const vatGroups = this.groupItemsByVAT(invoice.items || []);
|
|
626
|
+
let totalVAT = 0;
|
|
627
|
+
|
|
628
|
+
vatGroups.forEach((items, rate) => {
|
|
629
|
+
const taxableAmount = items.reduce((sum, item) => {
|
|
630
|
+
const lineNet = item.unitNetPrice * item.unitQuantity;
|
|
631
|
+
return sum + (this.currencyCalculator ? this.currencyCalculator.round(lineNet) : lineNet);
|
|
632
|
+
}, 0);
|
|
633
|
+
|
|
634
|
+
const vatAmount = taxableAmount * (rate / 100);
|
|
635
|
+
const roundedVAT = this.currencyCalculator
|
|
636
|
+
? this.currencyCalculator.round(vatAmount)
|
|
637
|
+
: vatAmount;
|
|
638
|
+
|
|
639
|
+
totalVAT += roundedVAT;
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return totalVAT;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private groupItemsByVAT(items: TAccountingDocItem[]): Map<number, TAccountingDocItem[]> {
|
|
646
|
+
const groups = new Map<number, TAccountingDocItem[]>();
|
|
647
|
+
|
|
648
|
+
items.forEach(item => {
|
|
649
|
+
const rate = item.vatPercentage || 0;
|
|
650
|
+
if (!groups.has(rate)) {
|
|
651
|
+
groups.set(rate, []);
|
|
652
|
+
}
|
|
653
|
+
groups.get(rate)!.push(item);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
return groups;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private addError(
|
|
660
|
+
ruleId: string,
|
|
661
|
+
message: string,
|
|
662
|
+
field?: string,
|
|
663
|
+
value?: any,
|
|
664
|
+
expected?: any
|
|
665
|
+
): void {
|
|
666
|
+
this.results.push({
|
|
667
|
+
ruleId,
|
|
668
|
+
source: 'EN16931',
|
|
669
|
+
severity: 'error',
|
|
670
|
+
message,
|
|
671
|
+
field,
|
|
672
|
+
value,
|
|
673
|
+
expected
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private addWarning(
|
|
678
|
+
ruleId: string,
|
|
679
|
+
message: string,
|
|
680
|
+
field?: string,
|
|
681
|
+
value?: any,
|
|
682
|
+
expected?: any
|
|
683
|
+
): void {
|
|
684
|
+
this.results.push({
|
|
685
|
+
ruleId,
|
|
686
|
+
source: 'EN16931',
|
|
687
|
+
severity: 'warning',
|
|
688
|
+
message,
|
|
689
|
+
field,
|
|
690
|
+
value,
|
|
691
|
+
expected
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|