@fin.cx/einvoice 8.3.1 → 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.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/einvoice.d.ts +26 -0
- package/dist_ts/einvoice.js +61 -3
- package/dist_ts/formats/base/base.decoder.d.ts +18 -1
- package/dist_ts/formats/base/base.decoder.js +30 -17
- package/dist_ts/formats/cii/cii.decoder.d.ts +89 -6
- package/dist_ts/formats/cii/cii.decoder.js +125 -30
- package/dist_ts/formats/cii/cii.encoder.d.ts +71 -1
- package/dist_ts/formats/cii/cii.encoder.js +195 -12
- package/dist_ts/formats/cii/cii.types.d.ts +2 -5
- package/dist_ts/formats/cii/cii.types.js +7 -9
- package/dist_ts/formats/cii/cii.validator.d.ts +13 -6
- package/dist_ts/formats/cii/cii.validator.js +29 -30
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +19 -3
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +0 -6
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +17 -26
- package/dist_ts/formats/cii/facturx/facturx.types.d.ts +0 -5
- package/dist_ts/formats/cii/facturx/facturx.types.js +1 -8
- package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +0 -5
- package/dist_ts/formats/cii/facturx/facturx.validator.js +5 -29
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +19 -2
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +0 -7
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +15 -68
- package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +0 -5
- package/dist_ts/formats/cii/zugferd/zugferd.types.js +1 -8
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +26 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +47 -1
- package/dist_ts/formats/cii/zugferd/zugferd.validator.js +6 -2
- package/dist_ts/formats/semantic/semantic.adapter.js +29 -20
- package/dist_ts/formats/semantic/semantic.validator.js +17 -2
- package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +0 -4
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -42
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +29 -26
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +9 -1
- package/dist_ts/formats/ubl/ubl.encoder.js +13 -5
- package/dist_ts/formats/ubl/ubl.validator.d.ts +13 -0
- package/dist_ts/formats/ubl/ubl.validator.js +28 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +14 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -7
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +30 -45
- package/dist_ts/formats/ubl/xrechnung.validator.d.ts +5 -4
- package/dist_ts/formats/ubl/xrechnung.validator.js +71 -68
- package/dist_ts/formats/utils/date.value.d.ts +11 -0
- package/dist_ts/formats/utils/date.value.js +33 -1
- package/dist_ts/formats/utils/document.totals.d.ts +1 -3
- package/dist_ts/formats/utils/document.totals.js +1 -2
- package/dist_ts/formats/utils/format.detector.js +16 -10
- package/dist_ts/formats/utils/paid.amount.d.ts +44 -0
- package/dist_ts/formats/utils/paid.amount.js +124 -0
- package/dist_ts/formats/utils/party.contact.d.ts +16 -0
- package/dist_ts/formats/utils/party.contact.js +16 -0
- package/dist_ts/formats/utils/party.identifier.d.ts +28 -0
- package/dist_ts/formats/utils/party.identifier.js +49 -0
- package/dist_ts/formats/utils/peppol.profile.d.ts +10 -0
- package/dist_ts/formats/utils/peppol.profile.js +12 -0
- package/dist_ts/formats/utils/preceding.invoice.d.ts +12 -2
- package/dist_ts/formats/utils/preceding.invoice.js +22 -3
- package/dist_ts/formats/utils/seller.identifier.d.ts +46 -0
- package/dist_ts/formats/utils/seller.identifier.js +78 -0
- package/dist_ts/formats/utils/stated.values.d.ts +80 -0
- package/dist_ts/formats/utils/stated.values.js +418 -0
- package/dist_ts/formats/utils/vat.category.d.ts +30 -2
- package/dist_ts/formats/utils/vat.category.js +36 -6
- package/dist_ts/formats/utils/vat.id.d.ts +18 -0
- package/dist_ts/formats/utils/vat.id.js +22 -0
- package/dist_ts/formats/validation/conformance.harness.js +6 -6
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -19
- package/dist_ts/formats/validation/facturx.validator.js +6 -6
- package/dist_ts/formats/validation/integrated.validator.js +4 -13
- package/dist_ts/formats/validation/peppol.validator.js +6 -13
- package/dist_ts/formats/validation/validation.types.d.ts +5 -0
- package/dist_ts/formats/validation/validation.types.js +6 -1
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +21 -42
- package/dist_ts/formats/validation/vat-categories.validator.js +137 -431
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +11 -58
- package/dist_ts/formats/validation/xrechnung.validator.js +58 -324
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +1 -1
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +0 -5
- package/dist_ts/interfaces/stated.values.d.ts +93 -0
- package/dist_ts/interfaces/stated.values.js +2 -0
- package/package.json +2 -2
- package/readme.md +182 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +70 -2
- package/ts/formats/base/base.decoder.ts +32 -29
- package/ts/formats/cii/cii.decoder.ts +159 -32
- package/ts/formats/cii/cii.encoder.ts +210 -14
- package/ts/formats/cii/cii.types.ts +7 -9
- package/ts/formats/cii/cii.validator.ts +30 -32
- package/ts/formats/cii/facturx/facturx.decoder.ts +21 -2
- package/ts/formats/cii/facturx/facturx.encoder.ts +19 -26
- package/ts/formats/cii/facturx/facturx.types.ts +0 -9
- package/ts/formats/cii/facturx/facturx.validator.ts +5 -43
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +21 -1
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +16 -73
- package/ts/formats/cii/zugferd/zugferd.types.ts +0 -9
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +66 -0
- package/ts/formats/cii/zugferd/zugferd.validator.ts +5 -1
- package/ts/formats/semantic/semantic.adapter.ts +28 -19
- package/ts/formats/semantic/semantic.validator.ts +17 -1
- package/ts/formats/ubl/en16931.ubl.validator.ts +5 -64
- package/ts/formats/ubl/generic/ubl.encoder.ts +31 -25
- package/ts/formats/ubl/ubl.decoder.ts +12 -0
- package/ts/formats/ubl/ubl.encoder.ts +12 -4
- package/ts/formats/ubl/ubl.validator.ts +29 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +13 -0
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +33 -45
- package/ts/formats/ubl/xrechnung.validator.ts +75 -127
- package/ts/formats/utils/date.value.ts +33 -0
- package/ts/formats/utils/document.totals.ts +1 -4
- package/ts/formats/utils/format.detector.ts +15 -9
- package/ts/formats/utils/paid.amount.ts +160 -0
- package/ts/formats/utils/party.contact.ts +30 -0
- package/ts/formats/utils/party.identifier.ts +61 -0
- package/ts/formats/utils/peppol.profile.ts +13 -0
- package/ts/formats/utils/preceding.invoice.ts +23 -2
- package/ts/formats/utils/seller.identifier.ts +102 -0
- package/ts/formats/utils/stated.values.ts +499 -0
- package/ts/formats/utils/vat.category.ts +47 -5
- package/ts/formats/utils/vat.id.ts +24 -0
- package/ts/formats/validation/conformance.harness.ts +5 -5
- package/ts/formats/validation/en16931.business-rules.validator.ts +3 -26
- package/ts/formats/validation/facturx.validator.ts +5 -5
- package/ts/formats/validation/integrated.validator.ts +3 -16
- package/ts/formats/validation/peppol.validator.ts +5 -16
- package/ts/formats/validation/validation.types.ts +7 -1
- package/ts/formats/validation/vat-categories.validator.ts +179 -761
- package/ts/formats/validation/xrechnung.validator.ts +61 -382
- package/ts/index.ts +9 -0
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +3 -8
- package/ts/interfaces/stated.values.ts +94 -0
- package/ts/readme.md +1 -1
- package/dist_ts/formats/utils/eu.memberstates.d.ts +0 -11
- package/dist_ts/formats/utils/eu.memberstates.js +0 -16
- package/ts/formats/utils/eu.memberstates.ts +0 -16
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
-
*
|
|
38
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.
|
|
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
|
-
*
|
|
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
|
|
536
|
-
items
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
'
|
|
596
|
-
|
|
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
|
-
*
|
|
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
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
782
|
-
|
|
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
|
/**
|