@fin.cx/einvoice 10.0.0 → 10.1.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 (50) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +2 -0
  3. package/dist_ts/einvoice.js +7 -13
  4. package/dist_ts/formats/base/base.decoder.d.ts +9 -1
  5. package/dist_ts/formats/base/base.decoder.js +24 -12
  6. package/dist_ts/formats/cii/cii.decoder.d.ts +11 -3
  7. package/dist_ts/formats/cii/cii.decoder.js +27 -8
  8. package/dist_ts/formats/cii/cii.encoder.d.ts +12 -0
  9. package/dist_ts/formats/cii/cii.encoder.js +36 -1
  10. package/dist_ts/formats/cii/cii.types.js +2 -1
  11. package/dist_ts/formats/cii/facturx/facturx.encoder.js +12 -8
  12. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +12 -8
  13. package/dist_ts/formats/semantic/semantic.adapter.d.ts +0 -4
  14. package/dist_ts/formats/semantic/semantic.adapter.js +7 -18
  15. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +6 -1
  16. package/dist_ts/formats/ubl/generic/ubl.encoder.js +40 -8
  17. package/dist_ts/formats/ubl/ubl.decoder.d.ts +11 -0
  18. package/dist_ts/formats/ubl/ubl.decoder.js +30 -1
  19. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +3 -3
  20. package/dist_ts/formats/utils/date.value.d.ts +24 -0
  21. package/dist_ts/formats/utils/date.value.js +77 -12
  22. package/dist_ts/formats/utils/delivery.address.d.ts +18 -0
  23. package/dist_ts/formats/utils/delivery.address.js +30 -0
  24. package/dist_ts/formats/utils/document.totals.d.ts +5 -4
  25. package/dist_ts/formats/utils/document.totals.js +24 -8
  26. package/dist_ts/formats/utils/stated.values.js +4 -2
  27. package/dist_ts/formats/utils/vat.category.d.ts +146 -55
  28. package/dist_ts/formats/utils/vat.category.js +357 -69
  29. package/dist_ts/formats/validation/vat-categories.validator.d.ts +14 -15
  30. package/dist_ts/formats/validation/vat-categories.validator.js +123 -85
  31. package/package.json +5 -5
  32. package/readme.md +61 -20
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/einvoice.ts +8 -14
  35. package/ts/formats/base/base.decoder.ts +24 -17
  36. package/ts/formats/cii/cii.decoder.ts +28 -7
  37. package/ts/formats/cii/cii.encoder.ts +36 -0
  38. package/ts/formats/cii/cii.types.ts +1 -0
  39. package/ts/formats/cii/facturx/facturx.encoder.ts +12 -7
  40. package/ts/formats/cii/zugferd/zugferd.encoder.ts +12 -7
  41. package/ts/formats/semantic/semantic.adapter.ts +6 -15
  42. package/ts/formats/ubl/generic/ubl.encoder.ts +40 -7
  43. package/ts/formats/ubl/ubl.decoder.ts +31 -0
  44. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +2 -2
  45. package/ts/formats/utils/date.value.ts +84 -11
  46. package/ts/formats/utils/delivery.address.ts +36 -0
  47. package/ts/formats/utils/document.totals.ts +38 -11
  48. package/ts/formats/utils/stated.values.ts +4 -1
  49. package/ts/formats/utils/vat.category.ts +426 -95
  50. package/ts/formats/validation/vat-categories.validator.ts +169 -88
@@ -1,47 +1,152 @@
1
1
  import type { TAccountingDoc } from '../../interfaces/common.js';
2
+ import type { finance } from '@tsclass/tsclass';
3
+
4
+ type TAccountingDocItem = finance.TAccountingDocItem;
2
5
  import { EInvoiceFormatError } from '../../errors.js';
6
+ import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
7
+
8
+ /**
9
+ * The VAT category codes (UNTDID 5305, EN 16931 BT-118/BT-151) an item can
10
+ * state (`TAccountingDocItem.vatCategory`): standard rated (S), zero rated (Z),
11
+ * exempt (E), reverse charge (AE), intra-community supply (K), export outside
12
+ * the EU (G), not subject to VAT (O), IGIC (L) and IPSI (M).
13
+ */
14
+ export type TVatCategoryCode = finance.TVatCategory;
15
+
16
+ /** Every VAT category code EN 16931 allows, in the order of `TVatCategoryCode` */
17
+ export const VAT_CATEGORY_CODES: readonly TVatCategoryCode[] = ['S', 'Z', 'E', 'AE', 'K', 'G', 'O', 'L', 'M'];
3
18
 
4
19
  /**
5
- * The VAT category codes (UNTDID 5305, EN 16931 BT-118/BT-151) the encoders
6
- * write: standard rated, or reverse charge. The envelope states reverse charge
7
- * for the whole document (`reverseCharge`); an item carries no category of its
8
- * own, so a document cannot mix reverse charge lines with standard rated ones.
20
+ * Whether a value is a VAT category code EN 16931 allows
21
+ * @param value The value
9
22
  */
10
- export type TVatCategoryCode = 'S' | 'AE';
23
+ export const isVatCategoryCode = (value: unknown): value is TVatCategoryCode =>
24
+ typeof value === 'string' && (VAT_CATEGORY_CODES as readonly string[]).includes(value);
11
25
 
12
- /** The VAT exemption reason (BT-121 code, BT-120 text) a VAT category states */
26
+ /**
27
+ * The categories whose VAT breakdown states an exemption reason, as a code
28
+ * (BT-121) or a text (BT-120): exempt (BR-E-10), reverse charge (BR-AE-10),
29
+ * intra-community supply (BR-IC-10), export (BR-G-10) and not subject to VAT
30
+ * (BR-O-10). The others state none (BR-S-10, BR-Z-10, BR-AF-10, BR-AG-10).
31
+ */
32
+ export const VAT_CATEGORIES_WITH_EXEMPTION: readonly TVatCategoryCode[] = ['E', 'AE', 'K', 'G', 'O'];
33
+
34
+ /**
35
+ * The VAT exemption reason code (BT-121) and text (BT-120) a VAT breakdown
36
+ * states; at least one of them
37
+ */
13
38
  export interface IVatExemption {
14
- code: string;
15
- reason: string;
39
+ code?: string;
40
+ reason?: string;
16
41
  }
17
42
 
18
43
  /**
19
- * The VAT category of every line of a document: reverse charge (`AE`) when the
20
- * document states it, standard rated (`S`) otherwise.
44
+ * The CEF VATEX code list, as the CEN/TC 434 EN 16931 validation artefacts
45
+ * 1.3.16 check a VAT exemption reason code (BT-121) against it (BR-CL-22,
46
+ * compared in upper case)
47
+ */
48
+ export const VATEX_CODES: ReadonlySet<string> = new Set(
49
+ (
50
+ 'VATEX-EU-79-C VATEX-EU-132 VATEX-EU-132-1A VATEX-EU-132-1B VATEX-EU-132-1C VATEX-EU-132-1D VATEX-EU-132-1E ' +
51
+ 'VATEX-EU-132-1F VATEX-EU-132-1G VATEX-EU-132-1H VATEX-EU-132-1I VATEX-EU-132-1J VATEX-EU-132-1K VATEX-EU-132-1L ' +
52
+ 'VATEX-EU-132-1M VATEX-EU-132-1N VATEX-EU-132-1O VATEX-EU-132-1P VATEX-EU-132-1Q VATEX-EU-135-1 VATEX-EU-143 ' +
53
+ 'VATEX-EU-143-1A VATEX-EU-143-1B VATEX-EU-143-1C VATEX-EU-143-1D VATEX-EU-143-1E VATEX-EU-143-1F VATEX-EU-143-1FA ' +
54
+ 'VATEX-EU-143-1G VATEX-EU-143-1H VATEX-EU-143-1I VATEX-EU-143-1J VATEX-EU-143-1K VATEX-EU-143-1L VATEX-EU-144 ' +
55
+ 'VATEX-EU-146-1E VATEX-EU-159 VATEX-EU-309 VATEX-EU-148 VATEX-EU-148-A VATEX-EU-148-B VATEX-EU-148-C VATEX-EU-148-D ' +
56
+ 'VATEX-EU-148-E VATEX-EU-148-F VATEX-EU-148-G VATEX-EU-151 VATEX-EU-151-1A VATEX-EU-151-1AA VATEX-EU-151-1B ' +
57
+ 'VATEX-EU-151-1C VATEX-EU-151-1D VATEX-EU-151-1E VATEX-EU-G VATEX-EU-O VATEX-EU-IC VATEX-EU-AE VATEX-EU-D VATEX-EU-F ' +
58
+ 'VATEX-EU-I VATEX-EU-J VATEX-FR-FRANCHISE VATEX-FR-CNWVAT VATEX-EU-153 VATEX-FR-CGI261-1 VATEX-FR-CGI261-2 ' +
59
+ 'VATEX-FR-CGI261-3 VATEX-FR-CGI261-4 VATEX-FR-CGI261-5 VATEX-FR-CGI261-7 VATEX-FR-CGI261-8 VATEX-FR-CGI261A ' +
60
+ 'VATEX-FR-CGI261B VATEX-FR-CGI261C-1 VATEX-FR-CGI261C-2 VATEX-FR-CGI261C-3 VATEX-FR-CGI261D-1 VATEX-FR-CGI261D-1BIS ' +
61
+ 'VATEX-FR-CGI261D-2 VATEX-FR-CGI261D-3 VATEX-FR-CGI261D-4 VATEX-FR-CGI261E-1 VATEX-FR-CGI261E-2 VATEX-FR-CGI277A ' +
62
+ 'VATEX-FR-CGI275 VATEX-FR-298SEXDECIESA VATEX-FR-CGI295 VATEX-FR-AE'
63
+ ).split(' '),
64
+ );
65
+
66
+ /**
67
+ * Whether a VAT exemption reason code is on the CEF VATEX code list (BR-CL-22)
68
+ * @param code The code
69
+ */
70
+ export const isVatexCode = (code: string): boolean => VATEX_CODES.has(code.trim().toUpperCase());
71
+
72
+ /**
73
+ * The VAT category of an item: the one it states (`vatCategory`, BT-151), else
74
+ * reverse charge (AE) when the document states `reverseCharge`, standard rated
75
+ * (S) otherwise.
21
76
  * @param accountingDoc The document
77
+ * @param item The item
22
78
  */
23
- export const getVatCategory = (accountingDoc: { reverseCharge?: boolean }): TVatCategoryCode =>
24
- accountingDoc.reverseCharge ? 'AE' : 'S';
79
+ export const getItemVatCategory = (
80
+ accountingDoc: { reverseCharge?: boolean },
81
+ item: Pick<TAccountingDocItem, 'vatCategory'>,
82
+ ): TVatCategoryCode => item.vatCategory ?? (accountingDoc.reverseCharge ? 'AE' : 'S');
25
83
 
26
84
  /**
27
- * The exemption reason a reverse charge VAT breakdown states (BR-AE-10): the
28
- * code `VATEX-EU-AE` and the wording the law prescribes, "Steuerschuldnerschaft
29
- * des Leistungsempfängers" (§ 14a Abs. 1 and 5 UStG). A document in another
30
- * language may use the wording of Article 226 Nr. 11a of the VAT Directive in
31
- * that language, "Reverse charge" in English (Abschnitt 14a.1 Abs. 6 Satz 2
32
- * UStAE). Other categories state none.
85
+ * The exemption reason an item states: its VATEX code (BT-121) and its text
86
+ * (BT-120), each trimmed; undefined when it states neither
87
+ * @param item The item
88
+ */
89
+ export const getItemVatExemption = (
90
+ item: Pick<TAccountingDocItem, 'vatExemptionReason' | 'vatExemptionReasonCode'>,
91
+ ): IVatExemption | undefined => {
92
+ const code = item.vatExemptionReasonCode?.trim();
93
+ const reason = item.vatExemptionReason?.trim();
94
+ if (!code && !reason) {
95
+ return undefined;
96
+ }
97
+ return { ...(code ? { code } : {}), ...(reason ? { reason } : {}) };
98
+ };
99
+
100
+ /**
101
+ * The exemption reason a VAT breakdown of a category states when none of its
102
+ * items states one.
103
+ * - Reverse charge (AE): the code `VATEX-EU-AE` and the wording the law
104
+ * prescribes, "Steuerschuldnerschaft des Leistungsempfängers" (§ 14a Abs. 1
105
+ * and 5 UStG). A document in another language may use the wording of
106
+ * Article 226 Nr. 11a of the VAT Directive in that language, "Reverse charge"
107
+ * in English (Abschnitt 14a.1 Abs. 6 Satz 2 UStAE).
108
+ * - Intra-community supply (K), export outside the EU (G) and not subject to
109
+ * VAT (O): the VATEX code that means the category, `VATEX-EU-IC`,
110
+ * `VATEX-EU-G` and `VATEX-EU-O`, which BR-IC-10, BR-G-10 and BR-O-10 accept.
111
+ * - Exempt (E): none. The reason depends on the exemption applied, which only
112
+ * the issuer knows; a document with an exempt item that states none is
113
+ * refused (BR-E-10).
33
114
  * @param category The VAT category
34
115
  * @param language The document language
35
116
  */
36
- export const getVatExemption = (category: TVatCategoryCode, language: string | undefined): IVatExemption | undefined =>
37
- category === 'AE'
38
- ? {
117
+ export const getDefaultVatExemption = (category: TVatCategoryCode, language: string | undefined): IVatExemption | undefined => {
118
+ switch (category) {
119
+ case 'AE':
120
+ return {
39
121
  code: 'VATEX-EU-AE',
40
- reason: (language ?? '').toLowerCase().startsWith('de')
41
- ? 'Steuerschuldnerschaft des Leistungsempfängers'
42
- : 'Reverse charge',
43
- }
44
- : undefined;
122
+ reason: (language ?? '').toLowerCase().startsWith('de') ? 'Steuerschuldnerschaft des Leistungsempfängers' : 'Reverse charge',
123
+ };
124
+ case 'K':
125
+ return { code: 'VATEX-EU-IC' };
126
+ case 'G':
127
+ return { code: 'VATEX-EU-G' };
128
+ case 'O':
129
+ return { code: 'VATEX-EU-O' };
130
+ default:
131
+ return undefined;
132
+ }
133
+ };
134
+
135
+ /**
136
+ * The exemption reason a VAT breakdown states: the code and the text its items
137
+ * state, each completed by the category's default where they state none
138
+ * (`getDefaultVatExemption`); undefined when there is neither
139
+ * @param stated The exemption the items state
140
+ * @param categoryDefault The category's default
141
+ */
142
+ export const mergeVatExemption = (stated: IVatExemption | undefined, categoryDefault: IVatExemption | undefined): IVatExemption | undefined => {
143
+ const code = stated?.code ?? categoryDefault?.code;
144
+ const reason = stated?.reason ?? categoryDefault?.reason;
145
+ if (!code && !reason) {
146
+ return undefined;
147
+ }
148
+ return { ...(code ? { code } : {}), ...(reason ? { reason } : {}) };
149
+ };
45
150
 
46
151
  /**
47
152
  * Whether a VAT rate is a number not greater than zero; a rate that is no
@@ -50,87 +155,313 @@ export const getVatExemption = (category: TVatCategoryCode, language: string | u
50
155
  */
51
156
  export const isRateNotAboveZero = (rate: unknown): boolean => typeof rate === 'number' && Number.isFinite(rate) && !(rate > 0);
52
157
 
53
- /** A line written as standard rated (S) whose VAT rate is not greater than zero */
54
- export interface IStandardRatedLineWithoutRate {
55
- index: number;
56
- vatPercentage: number;
158
+ /**
159
+ * The refusal or finding for a standard rated line whose VAT rate is not
160
+ * greater than zero (BR-S-05)
161
+ * @param line The line index and its rate
162
+ * @param statedCategory Whether the item states the category S itself
163
+ */
164
+ export const standardRatedLineWithoutRateMessage = (
165
+ line: { index: number; vatPercentage: number },
166
+ statedCategory = false,
167
+ ): string =>
168
+ `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; items[${line.index}].vatPercentage is ${String(line.vatPercentage)}. ` +
169
+ (statedCategory
170
+ ? `items[${line.index}].vatCategory states S`
171
+ : `An item outside reverse charge that states no category (items[${line.index}].vatCategory) is standard rated (S); an item that is zero rated, exempt, an intra-community supply, an export or not subject to VAT states its category, Z, E, K, G or O`);
172
+
173
+ /** One rule a VAT category finding names */
174
+ export interface IVatCategoryViolation {
175
+ /** the EN 16931 rule ID, or the package's own code for a check no official rule states */
176
+ ruleId: string;
177
+ /** what is wrong, with the field */
178
+ message: string;
179
+ /** the envelope field concerned */
180
+ field: string;
57
181
  }
58
182
 
183
+ /** The package's own check: the items of one VAT breakdown state different exemption reasons */
184
+ export const VAT_EXEMPTION_DIFFERS = 'EINVOICE-VAT-EXEMPTION-DIFFERS';
185
+
59
186
  /**
60
- * The first line of a document that is not reverse charge whose VAT rate is not
61
- * greater than zero. The encoders write every such line as standard rated (S),
62
- * and EN 16931 BR-S-05 asks a standard rated line for a rate greater than zero;
63
- * the category the line has (zero rated Z, exempt E, intra-community supply K,
64
- * export G, not subject to VAT O) cannot be stated until an item carries a VAT
65
- * category of its own.
66
- * @param items The document's items
187
+ * The item VAT rate each category allows: greater than zero for S (BR-S-05);
188
+ * 0 for Z, E, AE, K and G (BR-Z-05, BR-E-05, BR-AE-05, BR-IC-05, BR-G-05);
189
+ * 0 or greater for L and M (BR-AF-05, BR-AG-05). An item not subject to VAT
190
+ * (O) is written without a rate (BR-O-05), so it states 0.
67
191
  */
68
- export const findStandardRatedLineWithoutRate = (
69
- items: TAccountingDoc['items'] | undefined,
70
- ): IStandardRatedLineWithoutRate | undefined => {
71
- const index = (items ?? []).findIndex((item) => isRateNotAboveZero(item.vatPercentage));
72
- return index < 0 ? undefined : { index, vatPercentage: items![index].vatPercentage };
192
+ const RATE_RULES: Record<TVatCategoryCode, { ruleId: string; allows: (rate: number) => boolean; expected: string }> = {
193
+ S: { ruleId: 'BR-S-05', allows: (rate) => rate > 0, expected: 'greater than zero' },
194
+ Z: { ruleId: 'BR-Z-05', allows: (rate) => rate === 0, expected: '0' },
195
+ E: { ruleId: 'BR-E-05', allows: (rate) => rate === 0, expected: '0' },
196
+ AE: { ruleId: 'BR-AE-05', allows: (rate) => rate === 0, expected: '0' },
197
+ K: { ruleId: 'BR-IC-05', allows: (rate) => rate === 0, expected: '0' },
198
+ G: { ruleId: 'BR-G-05', allows: (rate) => rate === 0, expected: '0' },
199
+ O: { ruleId: 'BR-O-05', allows: (rate) => rate === 0, expected: '0, as an item not subject to VAT is written without a VAT rate' },
200
+ L: { ruleId: 'BR-AF-05', allows: (rate) => rate >= 0, expected: '0 or greater' },
201
+ M: { ruleId: 'BR-AG-05', allows: (rate) => rate >= 0, expected: '0 or greater' },
202
+ };
203
+
204
+ /**
205
+ * BR-AF-05 as the CII binding of the CEN/TC 434 artefacts 1.3.16 checks it:
206
+ * `ram:RateApplicablePercent > 0`, where the rule text and the UBL binding
207
+ * allow 0. A CII document with IGIC at 0 fails the official check, so it is
208
+ * not written.
209
+ */
210
+ const CII_IGIC_RATE_RULE = {
211
+ ruleId: 'BR-AF-05',
212
+ allows: (rate: number) => rate > 0,
213
+ expected: 'greater than zero in CII, whose binding of the CEN artefacts checks BR-AF-05 so; UBL allows 0',
214
+ };
215
+
216
+ /** The rule a VAT breakdown of each category breaks by stating, or by lacking, an exemption reason */
217
+ const EXEMPTION_RULES: Record<TVatCategoryCode, string> = {
218
+ S: 'BR-S-10',
219
+ Z: 'BR-Z-10',
220
+ E: 'BR-E-10',
221
+ AE: 'BR-AE-10',
222
+ K: 'BR-IC-10',
223
+ G: 'BR-G-10',
224
+ O: 'BR-O-10',
225
+ L: 'BR-AF-10',
226
+ M: 'BR-AG-10',
73
227
  };
74
228
 
75
229
  /**
76
- * The refusal or finding for such a line
77
- * @param line The line
78
- */
79
- export const standardRatedLineWithoutRateMessage = (line: IStandardRatedLineWithoutRate): string =>
80
- `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; items[${line.index}].vatPercentage is ${String(line.vatPercentage)}. A line outside reverse charge is written as standard rated (S); its category, zero rated, exempt, intra-community supply or export (Z, E, K, G or O), cannot be stated until an item carries a VAT category of its own`;
81
-
82
- /**
83
- * Refuses a document whose lines cannot be written with a valid VAT category,
84
- * naming the rule: a line outside reverse charge whose rate is not greater than
85
- * zero (BR-S-05), and a reverse charge document that lacks what EN 16931
86
- * requires of one; no value is put in place of a missing one.
87
- * - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
88
- * recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
89
- * UStG; a stated amount would be owed under § 14c Abs. 1 UStG).
90
- * - BR-AE-02: the seller states a VAT identifier (BT-31) or a tax
91
- * registration identifier (BT-32, `registrationDetails.taxNumber`, the
92
- * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows instead), and the
93
- * buyer a VAT identifier (BT-48) or a legal registration identifier (BT-47).
94
- * The rule also accepts a tax representative's VAT identifier (BT-63) for
95
- * the seller; the envelope cannot state one, so a seller with neither BT-31
96
- * nor BT-32 is refused.
97
- * The envelope has no place of supply, so whether a document is one for a
98
- * supply in another member state, whose invoice states the VAT identification
99
- * numbers of both parties (§ 14a Abs. 1 Satz 3 UStG), cannot be told here: a
100
- * domestic § 13b supply to a buyer based in another member state is taxed in
101
- * Germany and may state a tax number. Only what BR-AE-02 requires is checked;
102
- * the issuing application enforces § 14a Abs. 1.
230
+ * Every rule of EN 16931 on the VAT categories of a document's items that the
231
+ * envelope can break, in item order and then per document; an empty list when
232
+ * the document can be written. Amounts that are no number are not checked
233
+ * here (`findInvalidItemAmounts`).
234
+ * - The rate each category allows (BR-S-05, BR-Z-05, BR-E-05, BR-AE-05,
235
+ * BR-IC-05, BR-G-05, BR-O-05, BR-AF-05, BR-AG-05).
236
+ * - The exemption reason: stated for E, AE, K, G and O (a default for AE, K,
237
+ * G and O, `getDefaultVatExemption`), not stated for S, Z, L and M (the
238
+ * BR-x-10 rules); a code on the VATEX list (BR-CL-22); one reason per VAT
239
+ * breakdown, as a breakdown states one (`EINVOICE-VAT-EXEMPTION-DIFFERS`).
240
+ * - The parties' identifiers: for Z, E, L and M the seller VAT identifier
241
+ * (BT-31) or tax registration identifier (BT-32) (BR-Z-02, BR-E-02,
242
+ * BR-AF-02, BR-AG-02); for AE that and the buyer VAT identifier (BT-48) or
243
+ * legal registration identifier (BT-47) (BR-AE-02); for K the seller and the
244
+ * buyer VAT identifier (BR-IC-02); for G the seller VAT identifier
245
+ * (BR-G-02); for O neither VAT identifier (BR-O-02). The rules also accept
246
+ * a seller tax representative (BT-63), which the envelope cannot state.
247
+ * - An intra-community supply states the delivery date (BT-72) or the
248
+ * invoicing period (BG-14) (BR-IC-11) and the deliver to country (BT-80,
249
+ * `metadata.deliveryAddress.countryCode`) (BR-IC-12).
250
+ * - A document not subject to VAT has no item of another category (BR-O-11,
251
+ * BR-O-12).
252
+ * The rule for a standard rated item holds whether it states S or is S
253
+ * because it states no category, so an item at the rate 0 outside reverse
254
+ * charge that states no category breaks BR-S-05. Whether a document in
255
+ * reverse charge meets § 14a Abs. 1 UStG (both VAT identification numbers for
256
+ * a supply in another member state) cannot be told from the envelope, which
257
+ * has no place of supply; the issuing application enforces it.
103
258
  * @param accountingDoc The document
104
- * @param targetFormat The format being written
105
- */
106
- export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetFormat: string): void => {
107
- const refuse = (rule: string, message: string): never => {
108
- throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
109
- };
110
- if (!accountingDoc.reverseCharge) {
111
- const line = findStandardRatedLineWithoutRate(accountingDoc.items);
112
- if (line) {
113
- refuse('BR-S-05', standardRatedLineWithoutRateMessage(line));
259
+ */
260
+ export const findVatCategoryViolations = (
261
+ accountingDoc: Pick<TAccountingDoc, 'items' | 'from' | 'to' | 'deliveryDate' | 'periodOfPerformance'> & {
262
+ reverseCharge?: boolean;
263
+ language?: string;
264
+ metadata?: { deliveryAddress?: { countryCode?: string } };
265
+ },
266
+ syntax?: 'ubl' | 'cii',
267
+ ): IVatCategoryViolation[] => {
268
+ const violations: IVatCategoryViolation[] = [];
269
+ const items = accountingDoc.items ?? [];
270
+ const categories = items.map((item) => getItemVatCategory(accountingDoc, item));
271
+
272
+ items.forEach((item, index) => {
273
+ const category = categories[index];
274
+ const rule = syntax === 'cii' && category === 'L' ? CII_IGIC_RATE_RULE : RATE_RULES[category];
275
+ if (typeof item.vatPercentage === 'number' && Number.isFinite(item.vatPercentage) && !rule.allows(item.vatPercentage)) {
276
+ violations.push({
277
+ ruleId: rule.ruleId,
278
+ field: `items[${index}].vatPercentage`,
279
+ message:
280
+ category === 'S'
281
+ ? standardRatedLineWithoutRateMessage({ index, vatPercentage: item.vatPercentage }, item.vatCategory === 'S')
282
+ : `an item of the VAT category ${category} has the VAT rate ${rule.expected}; items[${index}].vatPercentage is ${String(item.vatPercentage)}`,
283
+ });
114
284
  }
115
- return;
116
- }
117
- for (const [index, item] of (accountingDoc.items ?? []).entries()) {
118
- if (item.vatPercentage !== 0) {
119
- refuse('BR-AE-05', `a reverse charge line has the VAT rate 0, items[${index}].vatPercentage is ${String(item.vatPercentage)}`);
285
+ const code = item.vatExemptionReasonCode?.trim();
286
+ if (code && !isVatexCode(code)) {
287
+ violations.push({
288
+ ruleId: 'BR-CL-22',
289
+ field: `items[${index}].vatExemptionReasonCode`,
290
+ message: `the VAT exemption reason code (BT-121) belongs to the CEF VATEX code list; items[${index}].vatExemptionReasonCode is ${code}`,
291
+ });
292
+ }
293
+ });
294
+
295
+ // one VAT breakdown per category and rate, as the encoders write them (computeDocumentTotals)
296
+ const groups = new Map<string, { category: TVatCategoryCode; indexes: number[] }>();
297
+ items.forEach((item, index) => {
298
+ const key = `${categories[index]}|${item.vatPercentage}`;
299
+ const group = groups.get(key) ?? { category: categories[index], indexes: [] };
300
+ group.indexes.push(index);
301
+ groups.set(key, group);
302
+ });
303
+ for (const { category, indexes } of groups.values()) {
304
+ const stated = indexes
305
+ .map((index) => ({ index, exemption: getItemVatExemption(items[index]) }))
306
+ .filter((entry): entry is { index: number; exemption: IVatExemption } => entry.exemption !== undefined);
307
+ const distinct = new Set(stated.map((entry) => `${entry.exemption.code ?? ''}|${entry.exemption.reason ?? ''}`));
308
+ const fields = indexes.map((index) => `items[${index}]`).join(', ');
309
+ if (distinct.size > 1) {
310
+ violations.push({
311
+ ruleId: VAT_EXEMPTION_DIFFERS,
312
+ field: stated.map((entry) => `items[${entry.index}]`).join(', '),
313
+ message: `the items of one VAT breakdown (category ${category}) state one exemption reason, as the breakdown states one (BT-120, BT-121); ${stated
314
+ .map((entry) => `items[${entry.index}] states ${[entry.exemption.code, entry.exemption.reason].filter(Boolean).join(' ')}`)
315
+ .join(', ')}`,
316
+ });
317
+ }
318
+ const withExemption = VAT_CATEGORIES_WITH_EXEMPTION.includes(category);
319
+ if (withExemption && stated.length === 0 && !getDefaultVatExemption(category, accountingDoc.language)) {
320
+ violations.push({
321
+ ruleId: EXEMPTION_RULES[category],
322
+ field: fields,
323
+ message: `a VAT breakdown of the category ${category} states a VAT exemption reason code (BT-121) or text (BT-120); ${fields} state neither vatExemptionReasonCode nor vatExemptionReason`,
324
+ });
325
+ }
326
+ if (!withExemption && stated.length > 0) {
327
+ violations.push({
328
+ ruleId: EXEMPTION_RULES[category],
329
+ field: stated.map((entry) => `items[${entry.index}]`).join(', '),
330
+ message: `a VAT breakdown of the category ${category} states no VAT exemption reason (BT-120, BT-121); ${stated
331
+ .map((entry) => `items[${entry.index}]`)
332
+ .join(', ')} state one`,
333
+ });
120
334
  }
121
335
  }
336
+
337
+ const has = (category: TVatCategoryCode) => categories.includes(category);
122
338
  const seller = accountingDoc.from?.registrationDetails;
123
- if (!seller?.vatId && !seller?.taxNumber) {
124
- refuse(
125
- 'BR-AE-02',
126
- 'a reverse charge document states the seller VAT identifier (BT-31) or tax registration identifier (BT-32), from.registrationDetails.vatId and .taxNumber are missing; the rule would also accept a tax representative (BT-63), which the envelope cannot state',
127
- );
128
- }
129
339
  const buyer = accountingDoc.to?.registrationDetails;
130
- if (!buyer?.vatId && !buyer?.registrationId) {
131
- refuse(
132
- 'BR-AE-02',
133
- 'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing',
134
- );
340
+ const sellerVatId = !!seller?.vatId?.trim();
341
+ const sellerTaxNumber = !!seller?.taxNumber?.trim();
342
+ const buyerVatId = !!buyer?.vatId?.trim();
343
+ const sellerNeither = (rule: string, category: string) =>
344
+ violations.push({
345
+ ruleId: rule,
346
+ field: 'from.registrationDetails',
347
+ message: `a document with an item of the VAT category ${category} states the seller VAT identifier (BT-31) or tax registration identifier (BT-32); from.registrationDetails.vatId and .taxNumber are missing. The rule would also accept a tax representative (BT-63), which the envelope cannot state`,
348
+ });
349
+ for (const [category, rule] of [['Z', 'BR-Z-02'], ['E', 'BR-E-02'], ['L', 'BR-AF-02'], ['M', 'BR-AG-02'], ['AE', 'BR-AE-02']] as const) {
350
+ if (has(category) && !sellerVatId && !sellerTaxNumber) {
351
+ sellerNeither(rule, category);
352
+ }
353
+ }
354
+ if (has('AE') && !buyerVatId && !buyer?.registrationId?.trim()) {
355
+ violations.push({
356
+ ruleId: 'BR-AE-02',
357
+ field: 'to.registrationDetails',
358
+ message:
359
+ 'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing',
360
+ });
361
+ }
362
+ if (has('K') && (!sellerVatId || !buyerVatId)) {
363
+ violations.push({
364
+ ruleId: 'BR-IC-02',
365
+ field: !sellerVatId ? 'from.registrationDetails.vatId' : 'to.registrationDetails.vatId',
366
+ message:
367
+ 'a document with an intra-community supply (K) states the seller VAT identifier (BT-31) and the buyer VAT identifier (BT-48) (§ 14a Abs. 3 Satz 2 UStG); ' +
368
+ [!sellerVatId ? 'from.registrationDetails.vatId' : '', !buyerVatId ? 'to.registrationDetails.vatId' : ''].filter(Boolean).join(' and ') +
369
+ ' missing. The rule would also accept a seller tax representative (BT-63), which the envelope cannot state',
370
+ });
371
+ }
372
+ if (has('K') && accountingDoc.deliveryDate === undefined && accountingDoc.periodOfPerformance === undefined) {
373
+ violations.push({
374
+ ruleId: 'BR-IC-11',
375
+ field: 'deliveryDate',
376
+ message: 'a document with an intra-community supply (K) states the actual delivery date (BT-72) or the invoicing period (BG-14); deliveryDate and periodOfPerformance are missing',
377
+ });
378
+ }
379
+ if (has('K') && !accountingDoc.metadata?.deliveryAddress?.countryCode?.trim()) {
380
+ violations.push({
381
+ ruleId: 'BR-IC-12',
382
+ field: 'metadata.deliveryAddress.countryCode',
383
+ message: 'a document with an intra-community supply (K) states the deliver to country code (BT-80); metadata.deliveryAddress.countryCode is missing',
384
+ });
385
+ }
386
+ if (has('G') && !sellerVatId) {
387
+ violations.push({
388
+ ruleId: 'BR-G-02',
389
+ field: 'from.registrationDetails.vatId',
390
+ message:
391
+ 'a document with an export outside the EU (G) states the seller VAT identifier (BT-31); from.registrationDetails.vatId is missing. The rule would also accept a seller tax representative (BT-63), which the envelope cannot state',
392
+ });
135
393
  }
394
+ if (has('O') && (sellerVatId || buyerVatId)) {
395
+ violations.push({
396
+ ruleId: 'BR-O-02',
397
+ field: sellerVatId ? 'from.registrationDetails.vatId' : 'to.registrationDetails.vatId',
398
+ message: `a document with an item not subject to VAT (O) states neither the seller VAT identifier (BT-31) nor the buyer VAT identifier (BT-48); ${[
399
+ sellerVatId ? 'from.registrationDetails.vatId' : '',
400
+ buyerVatId ? 'to.registrationDetails.vatId' : '',
401
+ ]
402
+ .filter(Boolean)
403
+ .join(' and ')} stated`,
404
+ });
405
+ }
406
+ if (has('O') && categories.some((category) => category !== 'O')) {
407
+ violations.push({
408
+ ruleId: 'BR-O-11',
409
+ field: categories.map((category, index) => (category === 'O' ? '' : `items[${index}].vatCategory`)).filter(Boolean).join(', '),
410
+ message: `a document with an item not subject to VAT (O) has no item of another VAT category, as its VAT breakdown is the one of O; ${categories
411
+ .map((category, index) => (category === 'O' ? '' : `items[${index}] is ${category}`))
412
+ .filter(Boolean)
413
+ .join(', ')}`,
414
+ });
415
+ }
416
+ return violations;
417
+ };
418
+
419
+ /**
420
+ * Refuses a document whose items cannot be written with a valid VAT category
421
+ * (`findVatCategoryViolations`), naming the first rule it breaks; no value is
422
+ * put in place of a missing one.
423
+ * @param accountingDoc The document
424
+ * @param targetFormat The syntax being written
425
+ */
426
+ export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetFormat: 'ubl' | 'cii'): void => {
427
+ const [violation] = findVatCategoryViolations(accountingDoc, targetFormat);
428
+ if (violation) {
429
+ throw new EInvoiceFormatError(`${violation.ruleId}: ${violation.message}`, {
430
+ targetFormat,
431
+ unsupportedFeatures: [violation.ruleId],
432
+ });
433
+ }
434
+ };
435
+
436
+ /**
437
+ * Gives the items of a decoded document the VAT category their lines state
438
+ * (BT-151) and the exemption reason (BT-120) and code (BT-121) of the VAT
439
+ * breakdown they belong to: the one of their category and rate, or of their
440
+ * category for an item not subject to VAT, which states no rate. A line that
441
+ * states no category, or a code EN 16931 does not allow, keeps none; the item
442
+ * is then read as AE or S (`getItemVatCategory`) and the code list rules
443
+ * report the code.
444
+ * @param items The decoded items, in line order
445
+ * @param statedValues What the document states; undefined for a ZUGFeRD 1.0 document
446
+ */
447
+ export const applyStatedVatCategories = (items: TAccountingDocItem[], statedValues: IEInvoiceStatedValues | undefined): void => {
448
+ if (!statedValues) {
449
+ return;
450
+ }
451
+ items.forEach((item, index) => {
452
+ const category = statedValues.lines[index]?.vatCategoryCode;
453
+ if (!isVatCategoryCode(category)) {
454
+ return;
455
+ }
456
+ item.vatCategory = category;
457
+ const breakdown = statedValues.vatBreakdown.find(
458
+ (group) => group.categoryCode === category && (category === 'O' || group.rate === item.vatPercentage),
459
+ );
460
+ if (breakdown?.exemptionReason) {
461
+ item.vatExemptionReason = breakdown.exemptionReason;
462
+ }
463
+ if (breakdown?.exemptionReasonCode) {
464
+ item.vatExemptionReasonCode = breakdown.exemptionReasonCode;
465
+ }
466
+ });
136
467
  };