@fin.cx/einvoice 9.0.0 → 10.0.0

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