@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.
Files changed (116) 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/package.json +9 -3
  58. package/readme.md +255 -810
  59. package/ts/00_commitinfo_data.ts +1 -1
  60. package/ts/einvoice.ts +63 -14
  61. package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
  62. package/ts/formats/semantic/bt-bg.model.ts +524 -0
  63. package/ts/formats/semantic/semantic.adapter.ts +600 -0
  64. package/ts/formats/semantic/semantic.validator.ts +654 -0
  65. package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
  66. package/ts/formats/utils/currency.utils.ts +299 -0
  67. package/ts/formats/utils/decimal.ts +509 -0
  68. package/ts/formats/validation/codelist.validator.ts +317 -0
  69. package/ts/formats/validation/conformance.harness.ts +591 -0
  70. package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
  71. package/ts/formats/validation/facturx.validator.ts +579 -0
  72. package/ts/formats/validation/integrated.validator.ts +405 -0
  73. package/ts/formats/validation/peppol.validator.ts +589 -0
  74. package/ts/formats/validation/schematron.downloader.ts +304 -0
  75. package/ts/formats/validation/schematron.integration.ts +285 -0
  76. package/ts/formats/validation/schematron.validator.ts +348 -0
  77. package/ts/formats/validation/schematron.worker.ts +221 -0
  78. package/ts/formats/validation/validation.types.ts +274 -0
  79. package/ts/formats/validation/vat-categories.validator.ts +845 -0
  80. package/ts/formats/validation/xrechnung.validator.ts +494 -0
  81. package/ts/interfaces/common.ts +1 -0
  82. package/ts/interfaces/en16931-metadata.ts +104 -0
  83. package/ts/plugins.ts +8 -0
  84. package/ts/tspublish.json +3 -0
  85. package/dist_ts/classes.decoder.d.ts +0 -40
  86. package/dist_ts/classes.decoder.js +0 -320
  87. package/dist_ts/classes.encoder.d.ts +0 -51
  88. package/dist_ts/classes.encoder.js +0 -293
  89. package/dist_ts/classes.xinvoice.d.ts +0 -136
  90. package/dist_ts/classes.xinvoice.js +0 -352
  91. package/dist_ts/formats/base.decoder.d.ts +0 -19
  92. package/dist_ts/formats/base.decoder.js +0 -130
  93. package/dist_ts/formats/base.validator.d.ts +0 -44
  94. package/dist_ts/formats/base.validator.js +0 -39
  95. package/dist_ts/formats/decoder.factory.d.ts +0 -15
  96. package/dist_ts/formats/decoder.factory.js +0 -46
  97. package/dist_ts/formats/factorx.decoder.d.ts +0 -18
  98. package/dist_ts/formats/factorx.decoder.js +0 -178
  99. package/dist_ts/formats/factorx.encoder.d.ts +0 -51
  100. package/dist_ts/formats/factorx.encoder.js +0 -293
  101. package/dist_ts/formats/facturx.decoder.d.ts +0 -18
  102. package/dist_ts/formats/facturx.decoder.js +0 -210
  103. package/dist_ts/formats/facturx.encoder.d.ts +0 -51
  104. package/dist_ts/formats/facturx.encoder.js +0 -293
  105. package/dist_ts/formats/facturx.validator.d.ts +0 -67
  106. package/dist_ts/formats/facturx.validator.js +0 -266
  107. package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
  108. package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
  109. package/dist_ts/formats/ubl.validator.d.ts +0 -82
  110. package/dist_ts/formats/ubl.validator.js +0 -306
  111. package/dist_ts/formats/validator.factory.d.ts +0 -18
  112. package/dist_ts/formats/validator.factory.js +0 -78
  113. package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
  114. package/dist_ts/formats/xinvoice.decoder.js +0 -332
  115. package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
  116. package/dist_ts/formats/xinvoice.encoder.js +0 -282
@@ -0,0 +1,845 @@
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 } from '../utils/currency.utils.js';
5
+ import type { ValidationResult } from './validation.types.js';
6
+
7
+ /**
8
+ * VAT Category codes according to UNCL5305
9
+ */
10
+ export enum VATCategory {
11
+ S = 'S', // Standard rate
12
+ Z = 'Z', // Zero rated
13
+ E = 'E', // Exempt from tax
14
+ AE = 'AE', // VAT Reverse Charge
15
+ K = 'K', // VAT exempt for EEA intra-community supply
16
+ G = 'G', // Free export outside EU
17
+ O = 'O', // Services outside scope of tax
18
+ L = 'L', // Canary Islands general indirect tax
19
+ M = 'M' // Tax for production, services and importation in Ceuta and Melilla
20
+ }
21
+
22
+ /**
23
+ * Extended VAT information for EN16931
24
+ */
25
+ export interface VATBreakdown {
26
+ category: VATCategory;
27
+ rate: number;
28
+ taxableAmount: number;
29
+ taxAmount: number;
30
+ exemptionReason?: string;
31
+ exemptionReasonCode?: string;
32
+ }
33
+
34
+ /**
35
+ * Comprehensive VAT Category Rules Validator
36
+ * Implements all EN16931 VAT category-specific business rules
37
+ */
38
+ export class VATCategoriesValidator {
39
+ private results: ValidationResult[] = [];
40
+ private currencyCalculator?: CurrencyCalculator;
41
+
42
+ /**
43
+ * Validate VAT categories according to EN16931
44
+ */
45
+ public validate(invoice: EInvoice): ValidationResult[] {
46
+ this.results = [];
47
+
48
+ // Initialize currency calculator if currency is available
49
+ if (invoice.currency) {
50
+ this.currencyCalculator = new CurrencyCalculator(invoice.currency);
51
+ }
52
+
53
+ // Group items by VAT category
54
+ const itemsByCategory = this.groupItemsByVATCategory(invoice.items || []);
55
+ const breakdownsByCategory = this.groupBreakdownsByCategory(invoice.taxBreakdown || []);
56
+
57
+ // Validate each VAT category
58
+ this.validateStandardRate(itemsByCategory.get('S'), breakdownsByCategory.get('S'), invoice);
59
+ this.validateZeroRated(itemsByCategory.get('Z'), breakdownsByCategory.get('Z'), invoice);
60
+ this.validateExempt(itemsByCategory.get('E'), breakdownsByCategory.get('E'), invoice);
61
+ this.validateReverseCharge(itemsByCategory.get('AE'), breakdownsByCategory.get('AE'), invoice);
62
+ this.validateIntraCommunity(itemsByCategory.get('K'), breakdownsByCategory.get('K'), invoice);
63
+ this.validateExport(itemsByCategory.get('G'), breakdownsByCategory.get('G'), invoice);
64
+ this.validateOutOfScope(itemsByCategory.get('O'), breakdownsByCategory.get('O'), invoice);
65
+
66
+ // Cross-category validation
67
+ this.validateCrossCategoryRules(invoice, itemsByCategory, breakdownsByCategory);
68
+
69
+ return this.results;
70
+ }
71
+
72
+ /**
73
+ * Validate Standard Rate VAT (BR-S-*)
74
+ */
75
+ private validateStandardRate(
76
+ items?: TAccountingDocItem[],
77
+ breakdown?: any,
78
+ invoice?: EInvoice
79
+ ): void {
80
+ if (!items || items.length === 0) return;
81
+
82
+ // BR-S-01: Invoice with standard rated items must have standard rated breakdown
83
+ if (!breakdown) {
84
+ this.addError('BR-S-01',
85
+ 'Invoice with standard rated items must have a standard rated VAT breakdown',
86
+ 'taxBreakdown'
87
+ );
88
+ return;
89
+ }
90
+
91
+ // BR-S-02: Standard rate VAT category taxable amount
92
+ const expectedTaxable = this.calculateTaxableAmount(items);
93
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
94
+ this.addError('BR-S-02',
95
+ `Standard rate VAT taxable amount mismatch`,
96
+ 'taxBreakdown.netAmount',
97
+ breakdown.netAmount,
98
+ expectedTaxable
99
+ );
100
+ }
101
+
102
+ // BR-S-03: Standard rate VAT category tax amount
103
+ const rate = breakdown.taxPercent || 0;
104
+ const expectedTax = this.calculateVATAmount(expectedTaxable, rate);
105
+ if (!this.areAmountsEqual(breakdown.taxAmount, expectedTax)) {
106
+ this.addError('BR-S-03',
107
+ `Standard rate VAT tax amount mismatch`,
108
+ 'taxBreakdown.taxAmount',
109
+ breakdown.taxAmount,
110
+ expectedTax
111
+ );
112
+ }
113
+
114
+ // BR-S-04: Standard rate VAT category code must be "S"
115
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'S') {
116
+ this.addError('BR-S-04',
117
+ 'Standard rate VAT category code must be "S"',
118
+ 'taxBreakdown.categoryCode',
119
+ breakdown.categoryCode,
120
+ 'S'
121
+ );
122
+ }
123
+
124
+ // BR-S-05: Standard rate VAT rate must be greater than zero
125
+ if (rate <= 0) {
126
+ this.addError('BR-S-05',
127
+ 'Standard rate VAT rate must be greater than zero',
128
+ 'taxBreakdown.taxPercent',
129
+ rate,
130
+ '> 0'
131
+ );
132
+ }
133
+
134
+ // BR-S-08: No exemption reason for standard rate
135
+ if (breakdown.exemptionReason) {
136
+ this.addError('BR-S-08',
137
+ 'Standard rate VAT must not have an exemption reason',
138
+ 'taxBreakdown.exemptionReason'
139
+ );
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Validate Zero Rated VAT (BR-Z-*)
145
+ */
146
+ private validateZeroRated(
147
+ items?: TAccountingDocItem[],
148
+ breakdown?: any,
149
+ invoice?: EInvoice
150
+ ): void {
151
+ if (!items || items.length === 0) return;
152
+
153
+ // BR-Z-01: Invoice with zero rated items must have zero rated breakdown
154
+ if (!breakdown) {
155
+ this.addError('BR-Z-01',
156
+ 'Invoice with zero rated items must have a zero rated VAT breakdown',
157
+ 'taxBreakdown'
158
+ );
159
+ return;
160
+ }
161
+
162
+ // BR-Z-02: Zero rate VAT category taxable amount
163
+ const expectedTaxable = this.calculateTaxableAmount(items);
164
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
165
+ this.addError('BR-Z-02',
166
+ 'Zero rate VAT taxable amount mismatch',
167
+ 'taxBreakdown.netAmount',
168
+ breakdown.netAmount,
169
+ expectedTaxable
170
+ );
171
+ }
172
+
173
+ // BR-Z-03: Zero rate VAT tax amount must be zero
174
+ if (breakdown.taxAmount !== 0) {
175
+ this.addError('BR-Z-03',
176
+ 'Zero rate VAT tax amount must be zero',
177
+ 'taxBreakdown.taxAmount',
178
+ breakdown.taxAmount,
179
+ 0
180
+ );
181
+ }
182
+
183
+ // BR-Z-04: Zero rate VAT category code must be "Z"
184
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'Z') {
185
+ this.addError('BR-Z-04',
186
+ 'Zero rate VAT category code must be "Z"',
187
+ 'taxBreakdown.categoryCode',
188
+ breakdown.categoryCode,
189
+ 'Z'
190
+ );
191
+ }
192
+
193
+ // BR-Z-05: Zero rate VAT rate must be zero
194
+ if (breakdown.taxPercent !== 0) {
195
+ this.addError('BR-Z-05',
196
+ 'Zero rate VAT rate must be zero',
197
+ 'taxBreakdown.taxPercent',
198
+ breakdown.taxPercent,
199
+ 0
200
+ );
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Validate Exempt from Tax (BR-E-*)
206
+ */
207
+ private validateExempt(
208
+ items?: TAccountingDocItem[],
209
+ breakdown?: any,
210
+ invoice?: EInvoice
211
+ ): void {
212
+ if (!items || items.length === 0) return;
213
+
214
+ // BR-E-01: Invoice with exempt items must have exempt breakdown
215
+ if (!breakdown) {
216
+ this.addError('BR-E-01',
217
+ 'Invoice with tax exempt items must have an exempt VAT breakdown',
218
+ 'taxBreakdown'
219
+ );
220
+ return;
221
+ }
222
+
223
+ // BR-E-02: Exempt VAT category taxable amount
224
+ const expectedTaxable = this.calculateTaxableAmount(items);
225
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
226
+ this.addError('BR-E-02',
227
+ 'Exempt VAT taxable amount mismatch',
228
+ 'taxBreakdown.netAmount',
229
+ breakdown.netAmount,
230
+ expectedTaxable
231
+ );
232
+ }
233
+
234
+ // BR-E-03: Exempt VAT tax amount must be zero
235
+ if (breakdown.taxAmount !== 0) {
236
+ this.addError('BR-E-03',
237
+ 'Exempt VAT tax amount must be zero',
238
+ 'taxBreakdown.taxAmount',
239
+ breakdown.taxAmount,
240
+ 0
241
+ );
242
+ }
243
+
244
+ // BR-E-04: Exempt VAT category code must be "E"
245
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'E') {
246
+ this.addError('BR-E-04',
247
+ 'Exempt VAT category code must be "E"',
248
+ 'taxBreakdown.categoryCode',
249
+ breakdown.categoryCode,
250
+ 'E'
251
+ );
252
+ }
253
+
254
+ // BR-E-05: Exempt VAT rate must be zero
255
+ if (breakdown.taxPercent !== 0) {
256
+ this.addError('BR-E-05',
257
+ 'Exempt VAT rate must be zero',
258
+ 'taxBreakdown.taxPercent',
259
+ breakdown.taxPercent,
260
+ 0
261
+ );
262
+ }
263
+
264
+ // BR-E-06: Exempt VAT must have exemption reason
265
+ if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) {
266
+ this.addError('BR-E-06',
267
+ 'Exempt VAT must have an exemption reason or exemption reason code',
268
+ 'taxBreakdown.exemptionReason'
269
+ );
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Validate VAT Reverse Charge (BR-AE-*)
275
+ */
276
+ private validateReverseCharge(
277
+ items?: TAccountingDocItem[],
278
+ breakdown?: any,
279
+ invoice?: EInvoice
280
+ ): void {
281
+ if (!items || items.length === 0) return;
282
+
283
+ // BR-AE-01: Invoice with reverse charge items must have reverse charge breakdown
284
+ if (!breakdown) {
285
+ this.addError('BR-AE-01',
286
+ 'Invoice with reverse charge items must have a reverse charge VAT breakdown',
287
+ 'taxBreakdown'
288
+ );
289
+ return;
290
+ }
291
+
292
+ // BR-AE-02: Reverse charge VAT category taxable amount
293
+ const expectedTaxable = this.calculateTaxableAmount(items);
294
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
295
+ this.addError('BR-AE-02',
296
+ 'Reverse charge VAT taxable amount mismatch',
297
+ 'taxBreakdown.netAmount',
298
+ breakdown.netAmount,
299
+ expectedTaxable
300
+ );
301
+ }
302
+
303
+ // BR-AE-03: Reverse charge VAT tax amount must be zero
304
+ if (breakdown.taxAmount !== 0) {
305
+ this.addError('BR-AE-03',
306
+ 'Reverse charge VAT tax amount must be zero',
307
+ 'taxBreakdown.taxAmount',
308
+ breakdown.taxAmount,
309
+ 0
310
+ );
311
+ }
312
+
313
+ // BR-AE-04: Reverse charge VAT category code must be "AE"
314
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'AE') {
315
+ this.addError('BR-AE-04',
316
+ 'Reverse charge VAT category code must be "AE"',
317
+ 'taxBreakdown.categoryCode',
318
+ breakdown.categoryCode,
319
+ 'AE'
320
+ );
321
+ }
322
+
323
+ // BR-AE-05: Reverse charge VAT rate must be zero
324
+ if (breakdown.taxPercent !== 0) {
325
+ this.addError('BR-AE-05',
326
+ 'Reverse charge VAT rate must be zero',
327
+ 'taxBreakdown.taxPercent',
328
+ breakdown.taxPercent,
329
+ 0
330
+ );
331
+ }
332
+
333
+ // BR-AE-06: Reverse charge must have exemption reason
334
+ if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) {
335
+ this.addError('BR-AE-06',
336
+ 'Reverse charge VAT must have an exemption reason',
337
+ 'taxBreakdown.exemptionReason'
338
+ );
339
+ }
340
+
341
+ // BR-AE-08: Buyer must have VAT identifier for reverse charge
342
+ if (!invoice?.metadata?.buyerTaxId) {
343
+ this.addError('BR-AE-08',
344
+ 'Buyer must have a VAT identifier for reverse charge invoices',
345
+ 'metadata.buyerTaxId'
346
+ );
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Validate Intra-Community Supply (BR-K-*)
352
+ */
353
+ private validateIntraCommunity(
354
+ items?: TAccountingDocItem[],
355
+ breakdown?: any,
356
+ invoice?: EInvoice
357
+ ): void {
358
+ if (!items || items.length === 0) return;
359
+
360
+ // BR-K-01: Invoice with intra-community items must have intra-community breakdown
361
+ if (!breakdown) {
362
+ this.addError('BR-K-01',
363
+ 'Invoice with intra-community supply must have corresponding VAT breakdown',
364
+ 'taxBreakdown'
365
+ );
366
+ return;
367
+ }
368
+
369
+ // BR-K-02: Intra-community VAT category taxable amount
370
+ const expectedTaxable = this.calculateTaxableAmount(items);
371
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
372
+ this.addError('BR-K-02',
373
+ 'Intra-community VAT taxable amount mismatch',
374
+ 'taxBreakdown.netAmount',
375
+ breakdown.netAmount,
376
+ expectedTaxable
377
+ );
378
+ }
379
+
380
+ // BR-K-03: Intra-community VAT tax amount must be zero
381
+ if (breakdown.taxAmount !== 0) {
382
+ this.addError('BR-K-03',
383
+ 'Intra-community VAT tax amount must be zero',
384
+ 'taxBreakdown.taxAmount',
385
+ breakdown.taxAmount,
386
+ 0
387
+ );
388
+ }
389
+
390
+ // BR-K-04: Intra-community VAT category code must be "K"
391
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'K') {
392
+ this.addError('BR-K-04',
393
+ 'Intra-community VAT category code must be "K"',
394
+ 'taxBreakdown.categoryCode',
395
+ breakdown.categoryCode,
396
+ 'K'
397
+ );
398
+ }
399
+
400
+ // BR-K-05: Intra-community VAT rate must be zero
401
+ if (breakdown.taxPercent !== 0) {
402
+ this.addError('BR-K-05',
403
+ 'Intra-community VAT rate must be zero',
404
+ 'taxBreakdown.taxPercent',
405
+ breakdown.taxPercent,
406
+ 0
407
+ );
408
+ }
409
+
410
+ // BR-K-06: Must have exemption reason
411
+ if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) {
412
+ this.addError('BR-K-06',
413
+ 'Intra-community supply must have an exemption reason',
414
+ 'taxBreakdown.exemptionReason'
415
+ );
416
+ }
417
+
418
+ // BR-K-08: Both seller and buyer must have VAT identifiers
419
+ if (!invoice?.metadata?.sellerTaxId) {
420
+ this.addError('BR-K-08',
421
+ 'Seller must have a VAT identifier for intra-community supply',
422
+ 'metadata.sellerTaxId'
423
+ );
424
+ }
425
+
426
+ if (!invoice?.metadata?.buyerTaxId) {
427
+ this.addError('BR-K-09',
428
+ 'Buyer must have a VAT identifier for intra-community supply',
429
+ 'metadata.buyerTaxId'
430
+ );
431
+ }
432
+
433
+ // BR-K-10: Must be in different EU member states
434
+ if (invoice?.from?.address?.countryCode === invoice?.to?.address?.countryCode) {
435
+ this.addWarning('BR-K-10',
436
+ 'Intra-community supply should be between different EU member states',
437
+ 'address.countryCode'
438
+ );
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Validate Export Outside EU (BR-G-*)
444
+ */
445
+ private validateExport(
446
+ items?: TAccountingDocItem[],
447
+ breakdown?: any,
448
+ invoice?: EInvoice
449
+ ): void {
450
+ if (!items || items.length === 0) return;
451
+
452
+ // BR-G-01: Invoice with export items must have export breakdown
453
+ if (!breakdown) {
454
+ this.addError('BR-G-01',
455
+ 'Invoice with export items must have an export VAT breakdown',
456
+ 'taxBreakdown'
457
+ );
458
+ return;
459
+ }
460
+
461
+ // BR-G-02: Export VAT category taxable amount
462
+ const expectedTaxable = this.calculateTaxableAmount(items);
463
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
464
+ this.addError('BR-G-02',
465
+ 'Export VAT taxable amount mismatch',
466
+ 'taxBreakdown.netAmount',
467
+ breakdown.netAmount,
468
+ expectedTaxable
469
+ );
470
+ }
471
+
472
+ // BR-G-03: Export VAT tax amount must be zero
473
+ if (breakdown.taxAmount !== 0) {
474
+ this.addError('BR-G-03',
475
+ 'Export VAT tax amount must be zero',
476
+ 'taxBreakdown.taxAmount',
477
+ breakdown.taxAmount,
478
+ 0
479
+ );
480
+ }
481
+
482
+ // BR-G-04: Export VAT category code must be "G"
483
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'G') {
484
+ this.addError('BR-G-04',
485
+ 'Export VAT category code must be "G"',
486
+ 'taxBreakdown.categoryCode',
487
+ breakdown.categoryCode,
488
+ 'G'
489
+ );
490
+ }
491
+
492
+ // BR-G-05: Export VAT rate must be zero
493
+ if (breakdown.taxPercent !== 0) {
494
+ this.addError('BR-G-05',
495
+ 'Export VAT rate must be zero',
496
+ 'taxBreakdown.taxPercent',
497
+ breakdown.taxPercent,
498
+ 0
499
+ );
500
+ }
501
+
502
+ // BR-G-06: Must have exemption reason
503
+ if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) {
504
+ this.addError('BR-G-06',
505
+ 'Export must have an exemption reason',
506
+ 'taxBreakdown.exemptionReason'
507
+ );
508
+ }
509
+
510
+ // BR-G-08: Buyer should be outside EU
511
+ const buyerCountry = invoice?.to?.address?.countryCode;
512
+ if (buyerCountry && this.isEUCountry(buyerCountry)) {
513
+ this.addWarning('BR-G-08',
514
+ 'Export category should be used for buyers outside EU',
515
+ 'to.address.countryCode',
516
+ buyerCountry,
517
+ 'non-EU'
518
+ );
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Validate Out of Scope Services (BR-O-*)
524
+ */
525
+ private validateOutOfScope(
526
+ items?: TAccountingDocItem[],
527
+ breakdown?: any,
528
+ invoice?: EInvoice
529
+ ): void {
530
+ if (!items || items.length === 0) return;
531
+
532
+ // BR-O-01: Invoice with out of scope items must have out of scope breakdown
533
+ if (!breakdown) {
534
+ this.addError('BR-O-01',
535
+ 'Invoice with out of scope items must have corresponding VAT breakdown',
536
+ 'taxBreakdown'
537
+ );
538
+ return;
539
+ }
540
+
541
+ // BR-O-02: Out of scope VAT category taxable amount
542
+ const expectedTaxable = this.calculateTaxableAmount(items);
543
+ if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) {
544
+ this.addError('BR-O-02',
545
+ 'Out of scope VAT taxable amount mismatch',
546
+ 'taxBreakdown.netAmount',
547
+ breakdown.netAmount,
548
+ expectedTaxable
549
+ );
550
+ }
551
+
552
+ // BR-O-03: Out of scope VAT tax amount must be zero
553
+ if (breakdown.taxAmount !== 0) {
554
+ this.addError('BR-O-03',
555
+ 'Out of scope VAT tax amount must be zero',
556
+ 'taxBreakdown.taxAmount',
557
+ breakdown.taxAmount,
558
+ 0
559
+ );
560
+ }
561
+
562
+ // BR-O-04: Out of scope VAT category code must be "O"
563
+ if (breakdown.categoryCode && breakdown.categoryCode !== 'O') {
564
+ this.addError('BR-O-04',
565
+ 'Out of scope VAT category code must be "O"',
566
+ 'taxBreakdown.categoryCode',
567
+ breakdown.categoryCode,
568
+ 'O'
569
+ );
570
+ }
571
+
572
+ // BR-O-05: Out of scope VAT rate must be zero
573
+ if (breakdown.taxPercent !== 0) {
574
+ this.addError('BR-O-05',
575
+ 'Out of scope VAT rate must be zero',
576
+ 'taxBreakdown.taxPercent',
577
+ breakdown.taxPercent,
578
+ 0
579
+ );
580
+ }
581
+
582
+ // BR-O-06: Must have exemption reason
583
+ if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) {
584
+ this.addError('BR-O-06',
585
+ 'Out of scope services must have an exemption reason',
586
+ 'taxBreakdown.exemptionReason'
587
+ );
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Cross-category validation rules
593
+ */
594
+ private validateCrossCategoryRules(
595
+ invoice: EInvoice,
596
+ itemsByCategory: Map<string, TAccountingDocItem[]>,
597
+ breakdownsByCategory: Map<string, any>
598
+ ): void {
599
+ // BR-CO-17: VAT category tax amount = Σ(VAT category taxable amount × VAT rate)
600
+ breakdownsByCategory.forEach((breakdown, category) => {
601
+ if (category === 'S' && breakdown.taxPercent > 0) {
602
+ const expectedTax = this.calculateVATAmount(breakdown.netAmount, breakdown.taxPercent);
603
+ if (!this.areAmountsEqual(breakdown.taxAmount, expectedTax)) {
604
+ this.addError('BR-CO-17',
605
+ `VAT tax amount calculation error for category ${category}`,
606
+ 'taxBreakdown.taxAmount',
607
+ breakdown.taxAmount,
608
+ expectedTax
609
+ );
610
+ }
611
+ }
612
+ });
613
+
614
+ // BR-CO-18: Invoice with mixed VAT categories
615
+ const categoriesUsed = new Set<string>();
616
+ itemsByCategory.forEach((items, category) => {
617
+ if (items.length > 0) categoriesUsed.add(category);
618
+ });
619
+
620
+ // BR-IC-01: Supply to EU countries without VAT ID should use standard rate
621
+ if (categoriesUsed.has('K') && !invoice.metadata?.buyerTaxId) {
622
+ this.addError('BR-IC-01',
623
+ 'Intra-community supply requires buyer VAT identifier',
624
+ 'metadata.buyerTaxId'
625
+ );
626
+ }
627
+
628
+ // BR-IC-02: Reverse charge requires specific conditions
629
+ if (categoriesUsed.has('AE')) {
630
+ // Check for service codes that qualify for reverse charge
631
+ const hasQualifyingServices = invoice.items?.some(item =>
632
+ this.isReverseChargeService(item)
633
+ );
634
+
635
+ if (!hasQualifyingServices) {
636
+ this.addWarning('BR-IC-02',
637
+ 'Reverse charge should only be used for qualifying services',
638
+ 'items'
639
+ );
640
+ }
641
+ }
642
+
643
+ // BR-CO-19: Sum of VAT breakdown taxable amounts must equal invoice tax exclusive total
644
+ let totalTaxable = 0;
645
+ breakdownsByCategory.forEach(breakdown => {
646
+ totalTaxable += breakdown.netAmount || 0;
647
+ });
648
+
649
+ const declaredTotal = invoice.totalNet || 0;
650
+ if (!this.areAmountsEqual(totalTaxable, declaredTotal)) {
651
+ this.addError('BR-CO-19',
652
+ 'Sum of VAT breakdown taxable amounts must equal invoice total without VAT',
653
+ 'totalNet',
654
+ declaredTotal,
655
+ totalTaxable
656
+ );
657
+ }
658
+ }
659
+
660
+ // Helper methods
661
+
662
+ private groupItemsByVATCategory(items: TAccountingDocItem[]): Map<string, TAccountingDocItem[]> {
663
+ const groups = new Map<string, TAccountingDocItem[]>();
664
+
665
+ items.forEach(item => {
666
+ const category = this.determineVATCategory(item);
667
+ if (!groups.has(category)) {
668
+ groups.set(category, []);
669
+ }
670
+ groups.get(category)!.push(item);
671
+ });
672
+
673
+ return groups;
674
+ }
675
+
676
+ private groupBreakdownsByCategory(breakdowns: any[]): Map<string, any> {
677
+ const groups = new Map<string, any>();
678
+
679
+ breakdowns.forEach(breakdown => {
680
+ const category = breakdown.categoryCode || this.inferCategoryFromRate(breakdown.taxPercent);
681
+ groups.set(category, breakdown);
682
+ });
683
+
684
+ return groups;
685
+ }
686
+
687
+ private determineVATCategory(item: TAccountingDocItem): string {
688
+ // Determine VAT category from item metadata or rate
689
+ const metadata = (item as any).metadata;
690
+ if (metadata?.vatCategory) {
691
+ return metadata.vatCategory;
692
+ }
693
+
694
+ // Infer from rate
695
+ if (item.vatPercentage === undefined || item.vatPercentage === null) {
696
+ return 'S'; // Default to standard
697
+ } else if (item.vatPercentage > 0) {
698
+ return 'S'; // Standard rate
699
+ } else if (item.vatPercentage === 0) {
700
+ // Could be Z, E, AE, K, G, or O - need more context
701
+ if (metadata?.exemptionReason) {
702
+ if (metadata.exemptionReason.includes('reverse')) return 'AE';
703
+ if (metadata.exemptionReason.includes('intra')) return 'K';
704
+ if (metadata.exemptionReason.includes('export')) return 'G';
705
+ if (metadata.exemptionReason.includes('scope')) return 'O';
706
+ return 'E'; // Default exempt
707
+ }
708
+ return 'Z'; // Default zero-rated
709
+ }
710
+
711
+ return 'S'; // Default
712
+ }
713
+
714
+ private inferCategoryFromRate(rate?: number): string {
715
+ if (!rate || rate === 0) return 'Z';
716
+ if (rate > 0) return 'S';
717
+ return 'S';
718
+ }
719
+
720
+ private calculateTaxableAmount(items: TAccountingDocItem[]): number {
721
+ const total = items.reduce((sum, item) => {
722
+ const lineNet = (item.unitNetPrice || 0) * (item.unitQuantity || 0);
723
+ return sum + (this.currencyCalculator ? this.currencyCalculator.round(lineNet) : lineNet);
724
+ }, 0);
725
+
726
+ return this.currencyCalculator ? this.currencyCalculator.round(total) : total;
727
+ }
728
+
729
+ private calculateVATAmount(taxableAmount: number, rate: number): number {
730
+ const vat = taxableAmount * (rate / 100);
731
+ return this.currencyCalculator ? this.currencyCalculator.round(vat) : vat;
732
+ }
733
+
734
+ private areAmountsEqual(value1: number, value2: number): boolean {
735
+ if (this.currencyCalculator) {
736
+ return this.currencyCalculator.areEqual(value1, value2);
737
+ }
738
+ return Math.abs(value1 - value2) < 0.01;
739
+ }
740
+
741
+ private isEUCountry(countryCode: string): boolean {
742
+ const euCountries = [
743
+ 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
744
+ 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
745
+ 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
746
+ ];
747
+ return euCountries.includes(countryCode);
748
+ }
749
+
750
+ private isReverseChargeService(item: TAccountingDocItem): boolean {
751
+ // Check if item qualifies for reverse charge
752
+ // This would typically check service codes
753
+ const metadata = (item as any).metadata;
754
+ if (metadata?.serviceCode) {
755
+ // Construction services, telecommunication, etc.
756
+ const reverseChargeServices = ['44', '45', '61', '62'];
757
+ return reverseChargeServices.some(code =>
758
+ metadata.serviceCode.startsWith(code)
759
+ );
760
+ }
761
+ return false;
762
+ }
763
+
764
+ private addError(
765
+ ruleId: string,
766
+ message: string,
767
+ field?: string,
768
+ value?: any,
769
+ expected?: any
770
+ ): void {
771
+ this.results.push({
772
+ ruleId,
773
+ source: 'EN16931',
774
+ severity: 'error',
775
+ message,
776
+ field,
777
+ value,
778
+ expected,
779
+ btReference: this.getBTReference(ruleId),
780
+ bgReference: 'BG-23' // VAT breakdown
781
+ });
782
+ }
783
+
784
+ private addWarning(
785
+ ruleId: string,
786
+ message: string,
787
+ field?: string,
788
+ value?: any,
789
+ expected?: any
790
+ ): void {
791
+ this.results.push({
792
+ ruleId,
793
+ source: 'EN16931',
794
+ severity: 'warning',
795
+ message,
796
+ field,
797
+ value,
798
+ expected,
799
+ btReference: this.getBTReference(ruleId),
800
+ bgReference: 'BG-23'
801
+ });
802
+ }
803
+
804
+ private getBTReference(ruleId: string): string | undefined {
805
+ const btMap: Record<string, string> = {
806
+ 'BR-S-': 'BT-118', // VAT category rate
807
+ 'BR-Z-': 'BT-118',
808
+ 'BR-E-': 'BT-120', // VAT exemption reason
809
+ 'BR-AE-': 'BT-120',
810
+ 'BR-K-': 'BT-120',
811
+ 'BR-G-': 'BT-120',
812
+ 'BR-O-': 'BT-120',
813
+ 'BR-CO-17': 'BT-117', // VAT category tax amount
814
+ 'BR-CO-18': 'BT-118',
815
+ 'BR-CO-19': 'BT-116' // VAT category taxable amount
816
+ };
817
+
818
+ for (const [prefix, bt] of Object.entries(btMap)) {
819
+ if (ruleId.startsWith(prefix)) {
820
+ return bt;
821
+ }
822
+ }
823
+
824
+ return undefined;
825
+ }
826
+ }
827
+
828
+ /**
829
+ * Get VAT category name
830
+ */
831
+ export function getVATCategoryName(category: VATCategory): string {
832
+ const names: Record<VATCategory, string> = {
833
+ [VATCategory.S]: 'Standard rate',
834
+ [VATCategory.Z]: 'Zero rated',
835
+ [VATCategory.E]: 'Exempt from tax',
836
+ [VATCategory.AE]: 'VAT Reverse Charge',
837
+ [VATCategory.K]: 'VAT exempt for EEA intra-community supply',
838
+ [VATCategory.G]: 'Free export outside EU',
839
+ [VATCategory.O]: 'Services outside scope of tax',
840
+ [VATCategory.L]: 'Canary Islands general indirect tax',
841
+ [VATCategory.M]: 'Tax for production, services and importation in Ceuta and Melilla'
842
+ };
843
+
844
+ return names[category] || 'Unknown';
845
+ }