@fin.cx/einvoice 5.0.3 → 5.1.3

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.
Files changed (117) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +4 -8
  3. package/dist_ts/einvoice.js +46 -7
  4. package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
  5. package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
  6. package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
  7. package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
  8. package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
  9. package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
  10. package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
  11. package/dist_ts/formats/semantic/semantic.validator.js +596 -0
  12. package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
  13. package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
  14. package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
  15. package/dist_ts/formats/utils/currency.utils.js +250 -0
  16. package/dist_ts/formats/utils/decimal.d.ts +151 -0
  17. package/dist_ts/formats/utils/decimal.js +435 -0
  18. package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
  19. package/dist_ts/formats/validation/codelist.validator.js +188 -0
  20. package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
  21. package/dist_ts/formats/validation/conformance.harness.js +471 -0
  22. package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
  23. package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
  24. package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
  25. package/dist_ts/formats/validation/facturx.validator.js +522 -0
  26. package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
  27. package/dist_ts/formats/validation/integrated.validator.js +311 -0
  28. package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
  29. package/dist_ts/formats/validation/peppol.validator.js +529 -0
  30. package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
  31. package/dist_ts/formats/validation/schematron.downloader.js +254 -0
  32. package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
  33. package/dist_ts/formats/validation/schematron.integration.js +230 -0
  34. package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
  35. package/dist_ts/formats/validation/schematron.validator.js +288 -0
  36. package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
  37. package/dist_ts/formats/validation/schematron.worker.js +185 -0
  38. package/dist_ts/formats/validation/validation.types.d.ts +163 -0
  39. package/dist_ts/formats/validation/validation.types.js +181 -0
  40. package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
  41. package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
  42. package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
  43. package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
  44. package/dist_ts/interfaces/common.d.ts +1 -0
  45. package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
  46. package/dist_ts/interfaces/en16931-metadata.js +6 -0
  47. package/dist_ts/plugins.d.ts +3 -1
  48. package/dist_ts/plugins.js +7 -2
  49. package/dist_ts_install/download-schematron.d.ts +6 -0
  50. package/dist_ts_install/download-schematron.js +62 -0
  51. package/dist_ts_install/download-test-samples.d.ts +19 -0
  52. package/dist_ts_install/download-test-samples.js +169 -0
  53. package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
  54. package/dist_ts_install/download-xrechnung-rules.js +146 -0
  55. package/dist_ts_install/index.d.ts +8 -0
  56. package/dist_ts_install/index.js +246 -0
  57. package/npmextra.json +1 -1
  58. package/package.json +12 -6
  59. package/readme.md +255 -810
  60. package/ts/00_commitinfo_data.ts +1 -1
  61. package/ts/einvoice.ts +63 -14
  62. package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
  63. package/ts/formats/semantic/bt-bg.model.ts +524 -0
  64. package/ts/formats/semantic/semantic.adapter.ts +600 -0
  65. package/ts/formats/semantic/semantic.validator.ts +654 -0
  66. package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
  67. package/ts/formats/utils/currency.utils.ts +299 -0
  68. package/ts/formats/utils/decimal.ts +509 -0
  69. package/ts/formats/validation/codelist.validator.ts +317 -0
  70. package/ts/formats/validation/conformance.harness.ts +591 -0
  71. package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
  72. package/ts/formats/validation/facturx.validator.ts +579 -0
  73. package/ts/formats/validation/integrated.validator.ts +405 -0
  74. package/ts/formats/validation/peppol.validator.ts +589 -0
  75. package/ts/formats/validation/schematron.downloader.ts +304 -0
  76. package/ts/formats/validation/schematron.integration.ts +285 -0
  77. package/ts/formats/validation/schematron.validator.ts +348 -0
  78. package/ts/formats/validation/schematron.worker.ts +221 -0
  79. package/ts/formats/validation/validation.types.ts +274 -0
  80. package/ts/formats/validation/vat-categories.validator.ts +845 -0
  81. package/ts/formats/validation/xrechnung.validator.ts +494 -0
  82. package/ts/interfaces/common.ts +1 -0
  83. package/ts/interfaces/en16931-metadata.ts +104 -0
  84. package/ts/plugins.ts +8 -0
  85. package/ts/tspublish.json +3 -0
  86. package/dist_ts/classes.decoder.d.ts +0 -40
  87. package/dist_ts/classes.decoder.js +0 -320
  88. package/dist_ts/classes.encoder.d.ts +0 -51
  89. package/dist_ts/classes.encoder.js +0 -293
  90. package/dist_ts/classes.xinvoice.d.ts +0 -136
  91. package/dist_ts/classes.xinvoice.js +0 -352
  92. package/dist_ts/formats/base.decoder.d.ts +0 -19
  93. package/dist_ts/formats/base.decoder.js +0 -130
  94. package/dist_ts/formats/base.validator.d.ts +0 -44
  95. package/dist_ts/formats/base.validator.js +0 -39
  96. package/dist_ts/formats/decoder.factory.d.ts +0 -15
  97. package/dist_ts/formats/decoder.factory.js +0 -46
  98. package/dist_ts/formats/factorx.decoder.d.ts +0 -18
  99. package/dist_ts/formats/factorx.decoder.js +0 -178
  100. package/dist_ts/formats/factorx.encoder.d.ts +0 -51
  101. package/dist_ts/formats/factorx.encoder.js +0 -293
  102. package/dist_ts/formats/facturx.decoder.d.ts +0 -18
  103. package/dist_ts/formats/facturx.decoder.js +0 -210
  104. package/dist_ts/formats/facturx.encoder.d.ts +0 -51
  105. package/dist_ts/formats/facturx.encoder.js +0 -293
  106. package/dist_ts/formats/facturx.validator.d.ts +0 -67
  107. package/dist_ts/formats/facturx.validator.js +0 -266
  108. package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
  109. package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
  110. package/dist_ts/formats/ubl.validator.d.ts +0 -82
  111. package/dist_ts/formats/ubl.validator.js +0 -306
  112. package/dist_ts/formats/validator.factory.d.ts +0 -18
  113. package/dist_ts/formats/validator.factory.js +0 -78
  114. package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
  115. package/dist_ts/formats/xinvoice.decoder.js +0 -332
  116. package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
  117. 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
+ }