@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +4 -8
- package/dist_ts/einvoice.js +46 -7
- package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
- package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
- package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
- package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
- package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
- package/dist_ts/formats/semantic/semantic.validator.js +596 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
- package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
- package/dist_ts/formats/utils/currency.utils.js +250 -0
- package/dist_ts/formats/utils/decimal.d.ts +151 -0
- package/dist_ts/formats/utils/decimal.js +435 -0
- package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
- package/dist_ts/formats/validation/codelist.validator.js +188 -0
- package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
- package/dist_ts/formats/validation/conformance.harness.js +471 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
- package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
- package/dist_ts/formats/validation/facturx.validator.js +522 -0
- package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
- package/dist_ts/formats/validation/integrated.validator.js +311 -0
- package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
- package/dist_ts/formats/validation/peppol.validator.js +529 -0
- package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
- package/dist_ts/formats/validation/schematron.downloader.js +254 -0
- package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
- package/dist_ts/formats/validation/schematron.integration.js +230 -0
- package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
- package/dist_ts/formats/validation/schematron.validator.js +288 -0
- package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
- package/dist_ts/formats/validation/schematron.worker.js +185 -0
- package/dist_ts/formats/validation/validation.types.d.ts +163 -0
- package/dist_ts/formats/validation/validation.types.js +181 -0
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
- package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
- package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
- package/dist_ts/interfaces/en16931-metadata.js +6 -0
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.d.ts +6 -0
- package/dist_ts_install/download-schematron.js +62 -0
- package/dist_ts_install/download-test-samples.d.ts +19 -0
- package/dist_ts_install/download-test-samples.js +169 -0
- package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
- package/dist_ts_install/download-xrechnung-rules.js +146 -0
- package/dist_ts_install/index.d.ts +8 -0
- package/dist_ts_install/index.js +246 -0
- package/npmextra.json +1 -1
- package/package.json +12 -6
- package/readme.md +255 -810
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +63 -14
- package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
- package/ts/formats/semantic/bt-bg.model.ts +524 -0
- package/ts/formats/semantic/semantic.adapter.ts +600 -0
- package/ts/formats/semantic/semantic.validator.ts +654 -0
- package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
- package/ts/formats/utils/currency.utils.ts +299 -0
- package/ts/formats/utils/decimal.ts +509 -0
- package/ts/formats/validation/codelist.validator.ts +317 -0
- package/ts/formats/validation/conformance.harness.ts +591 -0
- package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
- package/ts/formats/validation/facturx.validator.ts +579 -0
- package/ts/formats/validation/integrated.validator.ts +405 -0
- package/ts/formats/validation/peppol.validator.ts +589 -0
- package/ts/formats/validation/schematron.downloader.ts +304 -0
- package/ts/formats/validation/schematron.integration.ts +285 -0
- package/ts/formats/validation/schematron.validator.ts +348 -0
- package/ts/formats/validation/schematron.worker.ts +221 -0
- package/ts/formats/validation/validation.types.ts +274 -0
- package/ts/formats/validation/vat-categories.validator.ts +845 -0
- package/ts/formats/validation/xrechnung.validator.ts +494 -0
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +104 -0
- package/ts/plugins.ts +8 -0
- package/ts/tspublish.json +3 -0
- package/dist_ts/classes.decoder.d.ts +0 -40
- package/dist_ts/classes.decoder.js +0 -320
- package/dist_ts/classes.encoder.d.ts +0 -51
- package/dist_ts/classes.encoder.js +0 -293
- package/dist_ts/classes.xinvoice.d.ts +0 -136
- package/dist_ts/classes.xinvoice.js +0 -352
- package/dist_ts/formats/base.decoder.d.ts +0 -19
- package/dist_ts/formats/base.decoder.js +0 -130
- package/dist_ts/formats/base.validator.d.ts +0 -44
- package/dist_ts/formats/base.validator.js +0 -39
- package/dist_ts/formats/decoder.factory.d.ts +0 -15
- package/dist_ts/formats/decoder.factory.js +0 -46
- package/dist_ts/formats/factorx.decoder.d.ts +0 -18
- package/dist_ts/formats/factorx.decoder.js +0 -178
- package/dist_ts/formats/factorx.encoder.d.ts +0 -51
- package/dist_ts/formats/factorx.encoder.js +0 -293
- package/dist_ts/formats/facturx.decoder.d.ts +0 -18
- package/dist_ts/formats/facturx.decoder.js +0 -210
- package/dist_ts/formats/facturx.encoder.d.ts +0 -51
- package/dist_ts/formats/facturx.encoder.js +0 -293
- package/dist_ts/formats/facturx.validator.d.ts +0 -67
- package/dist_ts/formats/facturx.validator.js +0 -266
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
- package/dist_ts/formats/ubl.validator.d.ts +0 -82
- package/dist_ts/formats/ubl.validator.js +0 -306
- package/dist_ts/formats/validator.factory.d.ts +0 -18
- package/dist_ts/formats/validator.factory.js +0 -78
- package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
- package/dist_ts/formats/xinvoice.decoder.js +0 -332
- package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
- package/dist_ts/formats/xinvoice.encoder.js +0 -282
|
@@ -0,0 +1,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
|
+
}
|