@fin.cx/einvoice 8.2.2 → 8.2.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.
Files changed (33) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/formats/cii/cii.decoder.d.ts +6 -0
  3. package/dist_ts/formats/cii/cii.decoder.js +11 -1
  4. package/dist_ts/formats/cii/cii.encoder.js +4 -1
  5. package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
  6. package/dist_ts/formats/cii/facturx/facturx.encoder.js +22 -15
  7. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
  8. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +27 -14
  9. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
  10. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
  11. package/dist_ts/formats/ubl/generic/ubl.encoder.js +10 -13
  12. package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
  13. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
  14. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
  15. package/dist_ts/formats/utils/document.totals.d.ts +16 -6
  16. package/dist_ts/formats/utils/document.totals.js +25 -10
  17. package/dist_ts/formats/utils/vat.category.d.ts +50 -0
  18. package/dist_ts/formats/utils/vat.category.js +64 -0
  19. package/package.json +2 -2
  20. package/readme.md +25 -0
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/formats/cii/cii.decoder.ts +14 -0
  23. package/ts/formats/cii/cii.encoder.ts +3 -0
  24. package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
  25. package/ts/formats/cii/facturx/facturx.encoder.ts +22 -14
  26. package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
  27. package/ts/formats/cii/zugferd/zugferd.encoder.ts +29 -13
  28. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
  29. package/ts/formats/ubl/generic/ubl.encoder.ts +9 -13
  30. package/ts/formats/ubl/ubl.encoder.ts +3 -0
  31. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
  32. package/ts/formats/utils/document.totals.ts +36 -13
  33. package/ts/formats/utils/vat.category.ts +89 -0
@@ -249,6 +249,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
249
249
  nameElement.textContent = party.name;
250
250
  partyElement.appendChild(nameElement);
251
251
 
252
+ // Legal registration identifier (BT-30 seller, BT-47 buyer), e.g. the commercial register
253
+ // number; in the schema order it follows the name. It is no tax registration: the scheme
254
+ // FC of ram:SpecifiedTaxRegistration is the seller's tax number (BT-32)
255
+ if (party.registrationDetails && party.registrationDetails.registrationId) {
256
+ const legalOrganizationElement = doc.createElement('ram:SpecifiedLegalOrganization');
257
+ const legalIdElement = doc.createElement('ram:ID');
258
+ legalIdElement.textContent = party.registrationDetails.registrationId;
259
+ legalOrganizationElement.appendChild(legalIdElement);
260
+ partyElement.appendChild(legalOrganizationElement);
261
+ }
262
+
252
263
  // Add postal address
253
264
  const addressElement = doc.createElement('ram:PostalTradeAddress');
254
265
 
@@ -299,15 +310,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
299
310
  partyElement.appendChild(taxRegistrationElement);
300
311
  }
301
312
 
302
- // Add registration ID if available
303
- if (party.registrationDetails && party.registrationDetails.registrationId) {
304
- const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
305
- const regIdElement = doc.createElement('ram:ID');
306
- regIdElement.setAttribute('schemeID', 'FC');
307
- regIdElement.textContent = party.registrationDetails.registrationId;
308
- regRegistrationElement.appendChild(regIdElement);
309
- partyElement.appendChild(regRegistrationElement);
310
- }
311
313
  }
312
314
 
313
315
  /**
@@ -460,8 +462,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
460
462
  // the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
461
463
  const totals = computeDocumentTotals(invoice);
462
464
 
463
- // VAT breakdown (BG-23), one per rate
464
- for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
465
+ // VAT breakdown (BG-23), one per VAT category and rate
466
+ for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
465
467
  const taxElement = doc.createElement('ram:ApplicableTradeTax');
466
468
 
467
469
  // VAT category tax amount (BT-117)
@@ -473,6 +475,13 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
473
475
  const typeCodeElement = doc.createElement('ram:TypeCode');
474
476
  typeCodeElement.textContent = 'VAT';
475
477
  taxElement.appendChild(typeCodeElement);
478
+
479
+ // VAT exemption reason text (BT-120), for reverse charge (BR-AE-10)
480
+ if (exemption) {
481
+ const exemptionReasonElement = doc.createElement('ram:ExemptionReason');
482
+ exemptionReasonElement.textContent = exemption.reason;
483
+ taxElement.appendChild(exemptionReasonElement);
484
+ }
476
485
 
477
486
  // VAT category taxable amount (BT-116)
478
487
  const basisAmountElement = doc.createElement('ram:BasisAmount');
@@ -481,8 +490,15 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
481
490
 
482
491
  // Add category code
483
492
  const categoryCodeElement = doc.createElement('ram:CategoryCode');
484
- categoryCodeElement.textContent = invoice.reverseCharge ? 'AE' : 'S';
493
+ categoryCodeElement.textContent = category;
485
494
  taxElement.appendChild(categoryCodeElement);
495
+
496
+ // VAT exemption reason code (BT-121)
497
+ if (exemption) {
498
+ const exemptionReasonCodeElement = doc.createElement('ram:ExemptionReasonCode');
499
+ exemptionReasonCodeElement.textContent = exemption.code;
500
+ taxElement.appendChild(exemptionReasonCodeElement);
501
+ }
486
502
 
487
503
  // Add rate
488
504
  const rateElement = doc.createElement('ram:RateApplicablePercent');
@@ -594,7 +610,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
594
610
 
595
611
  // Add tax category code
596
612
  const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
597
- taxCategoryCodeElement.textContent = invoice.reverseCharge ? 'AE' : 'S';
613
+ taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
598
614
  taxElement.appendChild(taxCategoryCodeElement);
599
615
 
600
616
  // Add tax rate
@@ -35,6 +35,19 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
35
35
  };
36
36
  }
37
37
 
38
+ /**
39
+ * Whether every line of the v1 document has the VAT category reverse charge (AE), read with
40
+ * the v1 namespaces from the line settlement (`ram:SpecifiedSupplyChainTradeSettlement`)
41
+ */
42
+ protected override isReverseChargeDocument(): boolean {
43
+ const categories = zugferdV1Select(
44
+ '/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode',
45
+ this.doc,
46
+ );
47
+ const codes = (Array.isArray(categories) ? categories : []).map((node) => ((node as Node).textContent ?? '').trim());
48
+ return codes.length > 0 && codes.every((code) => code === 'AE');
49
+ }
50
+
38
51
  /**
39
52
  * Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
40
53
  * missing or invalid one is refused with an `EInvoiceParsingError`.
@@ -147,8 +160,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
147
160
  // Extract the actual delivery date, if stated
148
161
  const deliveryDate = this.extractDeliveryDate();
149
162
 
150
- // Check for reverse charge
151
- const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
163
+ // Reverse charge: every line has the VAT category AE
164
+ const reverseCharge = this.isReverseChargeDocument();
152
165
 
153
166
  // Create the common invoice data
154
167
  const invoiceData = {
@@ -217,8 +230,13 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
217
230
  // Extract VAT ID
218
231
  const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
219
232
 
220
- // Extract registration ID
221
- const registrationId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
233
+ // the legal registration identifier, and a tax number (scheme FC) as before when that is all
234
+ // the document states; the party is still read with the ZUGFeRD 2 namespaces (the known v1
235
+ // header defect), so for a v1 document this finds nothing yet
236
+ const registrationId =
237
+ this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
238
+ this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
239
+ '';
222
240
 
223
241
  // Create contact object
224
242
  return {
@@ -395,7 +395,7 @@ export class UBLEncoder extends UBLBaseEncoder {
395
395
  taxTotalNode.appendChild(taxAmountElement);
396
396
 
397
397
  // VAT breakdown (BG-23), one subtotal per rate
398
- for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
398
+ for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
399
399
  const taxSubtotalNode = doc.createElement('cac:TaxSubtotal');
400
400
  taxTotalNode.appendChild(taxSubtotalNode);
401
401
 
@@ -415,17 +415,14 @@ export class UBLEncoder extends UBLBaseEncoder {
415
415
  const taxCategoryNode = doc.createElement('cac:TaxCategory');
416
416
  taxSubtotalNode.appendChild(taxCategoryNode);
417
417
 
418
- // Determine tax category ID based on reverse charge
419
- const categoryId = invoice.reverseCharge ? 'AE' : 'S';
420
- this.appendElement(doc, taxCategoryNode, 'cbc:ID', categoryId);
421
-
422
- // Add percent with 2 decimal places
418
+ // VAT category code (BT-118) and rate (BT-119)
419
+ this.appendElement(doc, taxCategoryNode, 'cbc:ID', category);
423
420
  this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2));
424
421
 
425
- // Add tax exemption reason if reverse charge
426
- if (invoice.reverseCharge) {
427
- this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', 'VATEX-EU-IC');
428
- this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', 'Reverse charge');
422
+ // VAT exemption reason code (BT-121) and text (BT-120), for reverse charge (BR-AE-10)
423
+ if (exemption) {
424
+ this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', exemption.code);
425
+ this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', exemption.reason);
429
426
  }
430
427
 
431
428
  // Add tax scheme
@@ -527,9 +524,8 @@ export class UBLEncoder extends UBLBaseEncoder {
527
524
  const classifiedTaxCategoryNode = doc.createElement('cac:ClassifiedTaxCategory');
528
525
  itemNode.appendChild(classifiedTaxCategoryNode);
529
526
 
530
- // Determine tax category ID based on reverse charge
531
- const categoryId = invoice.reverseCharge ? 'AE' : 'S';
532
- this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', categoryId);
527
+ // Invoiced item VAT category code (BT-151)
528
+ this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', totals.lineVatCategories[index]);
533
529
 
534
530
  // Tax percent with 2 decimal places
535
531
  this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
@@ -2,6 +2,7 @@ import { BaseEncoder } from '../base/base.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
  import { getWritableDate } from '../utils/date.value.js';
5
+ import { assertVatCategoryWritable } from '../utils/vat.category.js';
5
6
 
6
7
  /**
7
8
  * The children of the document root in the order the UBL 2.1 schema requires
@@ -48,6 +49,8 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
48
49
  * @returns UBL XML string
49
50
  */
50
51
  public async encode(invoice: TAccountingDoc): Promise<string> {
52
+ // a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
53
+ assertVatCategoryWritable(invoice, 'ubl');
51
54
  // a credit note is a CreditNote document; an invoice, a debit note and a self-billed invoice are Invoice documents that differ in their type code
52
55
  if (invoice.accountingDocType === 'creditnote') {
53
56
  return this.encodeCreditNote(invoice);
@@ -261,7 +261,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
261
261
  subject: subject,
262
262
  items: items,
263
263
  dueInDays: dueInDays,
264
- reverseCharge: false,
264
+ // reverse charge when every line is VAT category AE; the envelope states it for the
265
+ // whole document, so a document mixing AE lines with others cannot say it
266
+ reverseCharge: this.isReverseChargeDocument(),
265
267
  currency: currencyCode as finance.TCurrency,
266
268
  notes: notes,
267
269
  objectActions: [],
@@ -301,6 +303,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
301
303
  }
302
304
  }
303
305
 
306
+ /**
307
+ * Whether every line of the document has the VAT category reverse charge (AE, BT-151)
308
+ */
309
+ private isReverseChargeDocument(): boolean {
310
+ const categories = this.select(
311
+ '/*/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID | /*/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID',
312
+ this.doc,
313
+ );
314
+ const codes = (Array.isArray(categories) ? categories : []).map((node) => (node.textContent ?? '').trim());
315
+ return codes.length > 0 && codes.every((code) => code === 'AE');
316
+ }
317
+
304
318
  /**
305
319
  * Reads the preceding invoice references (BG-3): the number (BT-25) and,
306
320
  * when stated, the issue date (BT-26) of each.
@@ -3,6 +3,7 @@ import { Decimal } from './decimal.js';
3
3
  import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
4
4
  import { EInvoiceFormatError } from '../../errors.js';
5
5
  import type { ValidationResult } from '../validation/validation.types.js';
6
+ import { getVatCategory, getVatExemption, type IVatExemption, type TVatCategoryCode } from './vat.category.js';
6
7
 
7
8
  /** An item amount that is no finite number, so no total can be computed from it */
8
9
  export interface IInvalidItemAmount {
@@ -61,13 +62,17 @@ export const getDocumentCalculator = (currency: TAccountingDoc['currency']): Dec
61
62
  new DecimalCurrencyCalculator(currency, 'HALF_UP', { maxDecimals: EN16931_MAX_AMOUNT_DECIMALS });
62
63
 
63
64
  /**
64
- * The VAT breakdown of one rate (BG-23): the taxable amount (BT-116), the tax
65
- * on it (BT-117) and the rate (BT-119).
65
+ * The VAT breakdown of one VAT category and rate (BG-23): the category
66
+ * (BT-118), the rate (BT-119), the taxable amount (BT-116), the tax on it
67
+ * (BT-117) and, for a category that states one, the exemption reason
68
+ * (BT-121, BT-120).
66
69
  */
67
70
  export interface IDocumentVatGroup {
71
+ category: TVatCategoryCode;
68
72
  rate: number;
69
73
  taxableAmount: Decimal;
70
74
  taxAmount: Decimal;
75
+ exemption?: IVatExemption;
71
76
  }
72
77
 
73
78
  /**
@@ -80,11 +85,13 @@ export interface IDocumentTotals {
80
85
  minorUnits: number;
81
86
  /** the Invoice line net amount (BT-131) of each item, in item order: quantity × net price, rounded */
82
87
  lineNetAmounts: Decimal[];
88
+ /** the VAT category (BT-151) of each item, in item order */
89
+ lineVatCategories: TVatCategoryCode[];
83
90
  /** Sum of Invoice line net amount (BT-106) */
84
91
  lineTotal: Decimal;
85
92
  /** Invoice total amount without VAT (BT-109): BT-106, as the envelope has no document level allowances or charges */
86
93
  taxBasisTotal: Decimal;
87
- /** VAT breakdown (BG-23), in the order the rates first appear */
94
+ /** VAT breakdown (BG-23), one per VAT category and rate, in the order they first appear */
88
95
  vatGroups: IDocumentVatGroup[];
89
96
  /** Invoice total VAT amount (BT-110): the sum of the rounded VAT category tax amounts */
90
97
  taxTotal: Decimal;
@@ -100,16 +107,20 @@ export interface IDocumentTotals {
100
107
  * decimals (BR-DEC-*): each line net amount (BT-131) is quantity × net price
101
108
  * rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
102
109
  * each VAT category taxable amount (BT-116) is the sum of the line net amounts
103
- * at that rate, and its tax amount (BT-117) is BT-116 × rate / 100 rounded
104
- * (BR-CO-17); the total VAT (BT-110) is the sum of the category tax amounts
110
+ * of that category and rate, and its tax amount (BT-117) is BT-116 × rate / 100
111
+ * rounded (BR-CO-17), 0 for reverse charge (BR-AE-09); the total VAT (BT-110) is the sum of the category tax amounts
105
112
  * (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
106
113
  * @param accountingDoc The document
107
114
  */
108
- export const computeDocumentTotals = (accountingDoc: Pick<TAccountingDoc, 'currency' | 'items'>): IDocumentTotals => {
115
+ export const computeDocumentTotals = (
116
+ accountingDoc: Pick<TAccountingDoc, 'currency' | 'items'> & { reverseCharge?: boolean; language?: string },
117
+ ): IDocumentTotals => {
109
118
  const calculator = getDocumentCalculator(accountingDoc.currency);
110
119
  const minorUnits = calculator.getCurrencyInfo().minorUnits;
120
+ const category = getVatCategory(accountingDoc);
111
121
  const lineNetAmounts: Decimal[] = [];
112
- const taxableByRate = new Map<number, Decimal>();
122
+ const lineVatCategories: TVatCategoryCode[] = [];
123
+ const taxableByGroup = new Map<string, { category: TVatCategoryCode; rate: number; taxable: Decimal }>();
113
124
  // an amount that is no number cannot be computed with; it is refused, not treated as 0
114
125
  const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
115
126
  if (firstInvalid) {
@@ -121,19 +132,31 @@ export const computeDocumentTotals = (accountingDoc: Pick<TAccountingDoc, 'curre
121
132
  for (const item of accountingDoc.items ?? []) {
122
133
  const lineNet = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
123
134
  lineNetAmounts.push(lineNet);
124
- taxableByRate.set(item.vatPercentage, (taxableByRate.get(item.vatPercentage) ?? Decimal.ZERO).add(lineNet));
135
+ lineVatCategories.push(category);
136
+ // grouped by VAT category and rate: the same rate under two categories is two groups
137
+ const key = `${category}|${item.vatPercentage}`;
138
+ const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO };
139
+ group.taxable = group.taxable.add(lineNet);
140
+ taxableByGroup.set(key, group);
125
141
  }
126
- const vatGroups: IDocumentVatGroup[] = [...taxableByRate.entries()].map(([rate, taxableAmount]) => ({
127
- rate,
128
- taxableAmount: calculator.round(taxableAmount),
129
- taxAmount: calculator.calculateVAT(taxableAmount, rate),
130
- }));
142
+ const vatGroups: IDocumentVatGroup[] = [...taxableByGroup.values()].map((group) => {
143
+ const exemption = getVatExemption(group.category, accountingDoc.language);
144
+ return {
145
+ category: group.category,
146
+ rate: group.rate,
147
+ taxableAmount: calculator.round(group.taxable),
148
+ // reverse charge states no tax: the recipient owes it (BR-AE-09)
149
+ taxAmount: group.category === 'AE' ? calculator.round(Decimal.ZERO) : calculator.calculateVAT(group.taxable, group.rate),
150
+ ...(exemption ? { exemption } : {}),
151
+ };
152
+ });
131
153
  const lineTotal = calculator.round(Decimal.sum(lineNetAmounts));
132
154
  const taxTotal = calculator.round(Decimal.sum(vatGroups.map((group) => group.taxAmount)));
133
155
  const grandTotal = calculator.round(lineTotal.add(taxTotal));
134
156
  return {
135
157
  minorUnits,
136
158
  lineNetAmounts,
159
+ lineVatCategories,
137
160
  lineTotal,
138
161
  taxBasisTotal: lineTotal,
139
162
  vatGroups,
@@ -0,0 +1,89 @@
1
+ import type { TAccountingDoc } from '../../interfaces/common.js';
2
+ import { EInvoiceFormatError } from '../../errors.js';
3
+
4
+ /**
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.
9
+ */
10
+ export type TVatCategoryCode = 'S' | 'AE';
11
+
12
+ /** The VAT exemption reason (BT-121 code, BT-120 text) a VAT category states */
13
+ export interface IVatExemption {
14
+ code: string;
15
+ reason: string;
16
+ }
17
+
18
+ /**
19
+ * The VAT category of every line of a document: reverse charge (`AE`) when the
20
+ * document states it, standard rated (`S`) otherwise.
21
+ * @param accountingDoc The document
22
+ */
23
+ export const getVatCategory = (accountingDoc: { reverseCharge?: boolean }): TVatCategoryCode =>
24
+ accountingDoc.reverseCharge ? 'AE' : 'S';
25
+
26
+ /**
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.
33
+ * @param category The VAT category
34
+ * @param language The document language
35
+ */
36
+ export const getVatExemption = (category: TVatCategoryCode, language: string | undefined): IVatExemption | undefined =>
37
+ category === 'AE'
38
+ ? {
39
+ code: 'VATEX-EU-AE',
40
+ reason: (language ?? '').toLowerCase().startsWith('de')
41
+ ? 'Steuerschuldnerschaft des Leistungsempfängers'
42
+ : 'Reverse charge',
43
+ }
44
+ : undefined;
45
+
46
+ /**
47
+ * Refuses a reverse charge document that lacks what EN 16931 requires of one,
48
+ * naming the rule; no value is put in place of a missing one.
49
+ * - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
50
+ * recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
51
+ * UStG; a stated amount would be owed under § 14c Abs. 1 UStG).
52
+ * - BR-AE-02: the seller states a VAT identifier (BT-31) and the buyer a VAT
53
+ * identifier (BT-48) or a legal registration identifier (BT-47). The rule
54
+ * also accepts the seller's tax registration identifier (BT-32, e.g. the
55
+ * Steuernummer) or a tax representative's VAT identifier (BT-63) instead of
56
+ * BT-31; the envelope can state neither, so a seller without a VAT
57
+ * identifier is refused. A domestic § 13b issuer that states only a
58
+ * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows, is therefore
59
+ * refused until the envelope carries a tax number.
60
+ * @param accountingDoc The document
61
+ * @param targetFormat The format being written
62
+ */
63
+ export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetFormat: string): void => {
64
+ if (!accountingDoc.reverseCharge) {
65
+ return;
66
+ }
67
+ const refuse = (rule: string, message: string): never => {
68
+ throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
69
+ };
70
+ for (const [index, item] of (accountingDoc.items ?? []).entries()) {
71
+ if (item.vatPercentage !== 0) {
72
+ refuse('BR-AE-05', `a reverse charge line has the VAT rate 0, items[${index}].vatPercentage is ${String(item.vatPercentage)}`);
73
+ }
74
+ }
75
+ const seller = accountingDoc.from?.registrationDetails;
76
+ if (!seller?.vatId) {
77
+ refuse(
78
+ 'BR-AE-02',
79
+ 'a reverse charge document states the seller VAT identifier (BT-31), from.registrationDetails.vatId is missing; the rule would also accept a tax registration identifier (BT-32) or a tax representative (BT-63), which the envelope cannot state',
80
+ );
81
+ }
82
+ const buyer = accountingDoc.to?.registrationDetails;
83
+ if (!buyer?.vatId && !buyer?.registrationId) {
84
+ refuse(
85
+ 'BR-AE-02',
86
+ 'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing',
87
+ );
88
+ }
89
+ };