@fin.cx/einvoice 8.2.4 → 8.3.1

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 (41) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/formats/cii/facturx/facturx.decoder.js +7 -8
  3. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +1 -0
  4. package/dist_ts/formats/cii/facturx/facturx.encoder.js +19 -6
  5. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +7 -8
  6. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -0
  7. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +21 -10
  8. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +8 -8
  9. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +0 -7
  10. package/dist_ts/formats/ubl/generic/ubl.encoder.js +20 -23
  11. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +19 -5
  12. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -5
  13. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +1 -23
  14. package/dist_ts/formats/ubl/xrechnung.validator.js +14 -11
  15. package/dist_ts/formats/utils/country.code.d.ts +15 -0
  16. package/dist_ts/formats/utils/country.code.js +41 -0
  17. package/dist_ts/formats/utils/eu.memberstates.d.ts +11 -0
  18. package/dist_ts/formats/utils/eu.memberstates.js +16 -0
  19. package/dist_ts/formats/utils/vat.category.d.ts +13 -8
  20. package/dist_ts/formats/utils/vat.category.js +16 -11
  21. package/dist_ts/formats/validation/codelist.validator.js +2 -2
  22. package/dist_ts/formats/validation/validation.types.js +23 -6
  23. package/dist_ts/formats/validation/vat-categories.validator.js +3 -7
  24. package/package.json +3 -3
  25. package/readme.md +15 -6
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/formats/cii/facturx/facturx.decoder.ts +7 -8
  28. package/ts/formats/cii/facturx/facturx.encoder.ts +19 -5
  29. package/ts/formats/cii/zugferd/zugferd.decoder.ts +7 -8
  30. package/ts/formats/cii/zugferd/zugferd.encoder.ts +21 -9
  31. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +7 -8
  32. package/ts/formats/ubl/generic/ubl.encoder.ts +27 -24
  33. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +17 -4
  34. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +0 -23
  35. package/ts/formats/ubl/xrechnung.validator.ts +21 -17
  36. package/ts/formats/utils/country.code.ts +53 -0
  37. package/ts/formats/utils/eu.memberstates.ts +16 -0
  38. package/ts/formats/utils/vat.category.ts +15 -10
  39. package/ts/formats/validation/codelist.validator.ts +2 -2
  40. package/ts/formats/validation/validation.types.ts +22 -5
  41. package/ts/formats/validation/vat-categories.validator.ts +2 -6
@@ -5,6 +5,7 @@ import { DOMParser, XMLSerializer } from '../../../plugins.js';
5
5
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
6
6
  import { computeDocumentTotals } from '../../utils/document.totals.js';
7
7
  import { toPlainDecimalString } from '../../utils/number.text.js';
8
+ import { resolveCountryCode } from '../../utils/country.code.js';
8
9
  import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
9
10
 
10
11
  /**
@@ -209,12 +210,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
209
210
 
210
211
  // Add seller
211
212
  const sellerElement = doc.createElement('ram:SellerTradeParty');
212
- this.addPartyInfo(doc, sellerElement, invoice.from);
213
+ this.addPartyInfo(doc, sellerElement, invoice.from, 'seller');
213
214
  agreementElement.appendChild(sellerElement);
214
215
 
215
216
  // Add buyer
216
217
  const buyerElement = doc.createElement('ram:BuyerTradeParty');
217
- this.addPartyInfo(doc, buyerElement, invoice.to);
218
+ this.addPartyInfo(doc, buyerElement, invoice.to, 'buyer');
218
219
  agreementElement.appendChild(buyerElement);
219
220
  }
220
221
 
@@ -223,8 +224,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
223
224
  * @param doc XML document
224
225
  * @param partyElement Party element
225
226
  * @param party Party data
227
+ * @param partyRole Whether the party is the seller or the buyer
226
228
  */
227
- private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
229
+ private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
228
230
  // Add name
229
231
  const nameElement = doc.createElement('ram:Name');
230
232
  nameElement.textContent = party.name;
@@ -264,9 +266,10 @@ export class FacturXEncoder extends CIIBaseEncoder {
264
266
  cityElement.textContent = party.address.city;
265
267
  addressElement.appendChild(cityElement);
266
268
 
267
- // Add country
269
+ // Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
270
+ // derived from a name (BR-CL-14)
268
271
  const countryElement = doc.createElement('ram:CountryID');
269
- countryElement.textContent = party.address.countryCode || party.address.country;
272
+ countryElement.textContent = resolveCountryCode(party.address, partyRole, 'cii');
270
273
  addressElement.appendChild(countryElement);
271
274
 
272
275
  partyElement.appendChild(addressElement);
@@ -281,6 +284,17 @@ export class FacturXEncoder extends CIIBaseEncoder {
281
284
  partyElement.appendChild(taxRegistrationElement);
282
285
  }
283
286
 
287
+ // Seller tax registration identifier (BT-32), the Steuernummer: scheme FC, after the VAT
288
+ // identifier. EN 16931 has no such element for the buyer, so a buyer's tax number is not written.
289
+ if (partyRole === 'seller' && party.registrationDetails?.taxNumber) {
290
+ const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
291
+ const taxIdElement = doc.createElement('ram:ID');
292
+ taxIdElement.setAttribute('schemeID', 'FC');
293
+ taxIdElement.textContent = party.registrationDetails.taxNumber;
294
+ taxRegistrationElement.appendChild(taxIdElement);
295
+ partyElement.appendChild(taxRegistrationElement);
296
+ }
297
+
284
298
  }
285
299
 
286
300
  /**
@@ -160,13 +160,11 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
160
160
  // Extract VAT ID
161
161
  const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
162
162
 
163
- // Extract registration ID
164
- // the legal registration identifier (BT-30, BT-47); a document that states only a tax
165
- // number (scheme FC, BT-32) keeps it here as before, the envelope has no field of its own for it
166
- const registrationId =
167
- this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
168
- this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
169
- '';
163
+ // the legal registration identifier (BT-30, BT-47)
164
+ const registrationId = this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) || '';
165
+
166
+ // the tax registration identifier (BT-32 for the seller), the Steuernummer: scheme FC
167
+ const taxNumber = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
170
168
 
171
169
  // Create contact object
172
170
  return {
@@ -179,7 +177,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
179
177
  registrationDetails: {
180
178
  vatId: vatId,
181
179
  registrationId: registrationId,
182
- registrationName: ''
180
+ registrationName: '',
181
+ ...(taxNumber ? { taxNumber } : {})
183
182
  }
184
183
  } as business.TContact;
185
184
  }
@@ -8,6 +8,7 @@ import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
8
8
  import { getPaymentTermsNote } from '../../utils/payment.terms.js';
9
9
  import { computeDocumentTotals } from '../../utils/document.totals.js';
10
10
  import { toPlainDecimalString } from '../../utils/number.text.js';
11
+ import { resolveCountryCode } from '../../utils/country.code.js';
11
12
 
12
13
  /**
13
14
  * Encoder for ZUGFeRD invoice format
@@ -224,7 +225,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
224
225
 
225
226
  // Add seller
226
227
  const sellerElement = doc.createElement('ram:SellerTradeParty');
227
- this.addPartyInfo(doc, sellerElement, invoice.from);
228
+ this.addPartyInfo(doc, sellerElement, invoice.from, 'seller');
228
229
 
229
230
  // Seller electronic address (BT-34): ram:URIUniversalCommunication/ram:URIID of the party; a
230
231
  // trade contact (ram:DefinedTradeContact) has no URIID
@@ -241,7 +242,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
241
242
 
242
243
  // Add buyer
243
244
  const buyerElement = doc.createElement('ram:BuyerTradeParty');
244
- this.addPartyInfo(doc, buyerElement, invoice.to);
245
+ this.addPartyInfo(doc, buyerElement, invoice.to, 'buyer');
245
246
  agreementElement.appendChild(buyerElement);
246
247
  }
247
248
 
@@ -250,8 +251,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
250
251
  * @param doc XML document
251
252
  * @param partyElement Party element
252
253
  * @param party Party data
254
+ * @param partyRole Whether the party is the seller or the buyer
253
255
  */
254
- private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
256
+ private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
255
257
  // Add name
256
258
  const nameElement = doc.createElement('ram:Name');
257
259
  nameElement.textContent = party.name;
@@ -299,12 +301,11 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
299
301
  addressElement.appendChild(cityElement);
300
302
  }
301
303
 
302
- // Add country
303
- if (party.address.country || party.address.countryCode) {
304
- const countryElement = doc.createElement('ram:CountryID');
305
- countryElement.textContent = party.address.countryCode || party.address.country;
306
- addressElement.appendChild(countryElement);
307
- }
304
+ // Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
305
+ // derived from a name (BR-CL-14)
306
+ const countryElement = doc.createElement('ram:CountryID');
307
+ countryElement.textContent = resolveCountryCode(party.address, partyRole, 'cii');
308
+ addressElement.appendChild(countryElement);
308
309
 
309
310
  partyElement.appendChild(addressElement);
310
311
 
@@ -318,6 +319,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
318
319
  partyElement.appendChild(taxRegistrationElement);
319
320
  }
320
321
 
322
+ // Seller tax registration identifier (BT-32), the Steuernummer: scheme FC, after the VAT
323
+ // identifier. EN 16931 has no such element for the buyer, so a buyer's tax number is not written.
324
+ if (partyRole === 'seller' && party.registrationDetails?.taxNumber) {
325
+ const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
326
+ const taxIdElement = doc.createElement('ram:ID');
327
+ taxIdElement.setAttribute('schemeID', 'FC');
328
+ taxIdElement.textContent = party.registrationDetails.taxNumber;
329
+ taxRegistrationElement.appendChild(taxIdElement);
330
+ partyElement.appendChild(taxRegistrationElement);
331
+ }
332
+
321
333
  }
322
334
 
323
335
  /**
@@ -230,13 +230,11 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
230
230
  // Extract VAT ID
231
231
  const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
232
232
 
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
- '';
233
+ // the legal registration identifier and the tax number (scheme FC); the party is still read
234
+ // with the ZUGFeRD 2 namespaces (the known v1 header defect), so for a v1 document neither is
235
+ // found yet
236
+ const registrationId = this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) || '';
237
+ const taxNumber = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
240
238
 
241
239
  // Create contact object
242
240
  return {
@@ -249,7 +247,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
249
247
  registrationDetails: {
250
248
  vatId: vatId,
251
249
  registrationId: registrationId,
252
- registrationName: ''
250
+ registrationName: '',
251
+ ...(taxNumber ? { taxNumber } : {})
253
252
  }
254
253
  } as business.TContact;
255
254
  }
@@ -8,6 +8,7 @@ import { getWritableDueDate } from '../../utils/date.value.js';
8
8
  import { getPaymentTermsNote } from '../../utils/payment.terms.js';
9
9
  import { computeDocumentTotals } from '../../utils/document.totals.js';
10
10
  import { toPlainDecimalString } from '../../utils/number.text.js';
11
+ import { resolveCountryCode } from '../../utils/country.code.js';
11
12
 
12
13
  /**
13
14
  * UBL Encoder implementation
@@ -253,17 +254,18 @@ export class UBLEncoder extends UBLBaseEncoder {
253
254
  this.appendElement(doc, postalAddressNode, 'cbc:PostalZone', party.address.postalCode);
254
255
  }
255
256
 
256
- // Country
257
- if (party.address.country || party.address.countryCode) {
258
- const countryNode = doc.createElement('cac:Country');
259
- postalAddressNode.appendChild(countryNode);
260
-
261
- const countryCode = party.address.countryCode || this.getCountryCode(party.address.country);
262
- this.appendElement(doc, countryNode, 'cbc:IdentificationCode', countryCode);
263
-
264
- if (party.address.country) {
265
- this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
266
- }
257
+ // Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
258
+ // derived from a name (BR-CL-14)
259
+ const countryNode = doc.createElement('cac:Country');
260
+ postalAddressNode.appendChild(countryNode);
261
+ this.appendElement(
262
+ doc,
263
+ countryNode,
264
+ 'cbc:IdentificationCode',
265
+ resolveCountryCode(party.address, elementName === 'cac:AccountingSupplierParty' ? 'seller' : 'buyer', 'ubl'),
266
+ );
267
+ if (party.address.country) {
268
+ this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
267
269
  }
268
270
 
269
271
  // Party tax scheme (VAT ID)
@@ -277,6 +279,20 @@ export class UBLEncoder extends UBLBaseEncoder {
277
279
  partyTaxSchemeNode.appendChild(taxSchemeNode);
278
280
  this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT');
279
281
  }
282
+
283
+ // Seller tax registration identifier (BT-32), the Steuernummer: a party tax scheme other than
284
+ // VAT, after the VAT one; FC as in CII and the German reference examples. EN 16931 has no
285
+ // such element for the buyer, so a buyer's tax number is not written.
286
+ if (elementName === 'cac:AccountingSupplierParty' && party.registrationDetails?.taxNumber) {
287
+ const partyTaxSchemeNode = doc.createElement('cac:PartyTaxScheme');
288
+ partyNode.appendChild(partyTaxSchemeNode);
289
+
290
+ this.appendElement(doc, partyTaxSchemeNode, 'cbc:CompanyID', party.registrationDetails.taxNumber);
291
+
292
+ const taxSchemeNode = doc.createElement('cac:TaxScheme');
293
+ partyTaxSchemeNode.appendChild(taxSchemeNode);
294
+ this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'FC');
295
+ }
280
296
 
281
297
  // Party legal entity (registration information)
282
298
  if (party.registrationDetails) {
@@ -562,19 +578,6 @@ export class UBLEncoder extends UBLBaseEncoder {
562
578
  parentElement.appendChild(element);
563
579
  }
564
580
 
565
- /**
566
- * Helper method to get country code from country name
567
- * Simple implementation that assumes the country name is already a code
568
- * @param countryName Country name
569
- * @returns Country code (2-letter ISO code)
570
- */
571
- private getCountryCode(countryName: string): string {
572
- // In a real implementation, this would map country names to ISO codes
573
- // For now, just return the first 2 characters or "XX" as fallback
574
- if (!countryName) return 'XX';
575
- return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
576
- }
577
-
578
581
  /**
579
582
  * Preserves metadata from invoice to enhance UBL XML output
580
583
  * @param doc XML document
@@ -354,6 +354,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
354
354
  let country = '';
355
355
  let countryCode = '';
356
356
  let vatId = '';
357
+ let taxNumber = '';
357
358
  let registrationId = '';
358
359
  let registrationName = '';
359
360
  let contactPhone = '';
@@ -411,10 +412,21 @@ export class XRechnungDecoder extends UBLBaseDecoder {
411
412
  }
412
413
  }
413
414
 
414
- // Extract tax information
415
+ // Extract tax information by scheme, as the EN 16931 UBL binding selects it: the VAT
416
+ // identifier (BT-31, BT-48) under the tax scheme VAT, the tax registration identifier
417
+ // (BT-32, the Steuernummer) under any other, whichever order the document lists them in;
418
+ // an entry without a tax scheme identifier is neither
415
419
  const taxSchemeNodes = this.select('./cac:PartyTaxScheme', party);
416
- if (taxSchemeNodes && Array.isArray(taxSchemeNodes) && taxSchemeNodes.length > 0) {
417
- vatId = this.getText('./cbc:CompanyID', taxSchemeNodes[0]) || '';
420
+ if (taxSchemeNodes && Array.isArray(taxSchemeNodes)) {
421
+ for (const taxSchemeNode of taxSchemeNodes) {
422
+ const companyId = this.getText('./cbc:CompanyID', taxSchemeNode);
423
+ const taxSchemeId = this.getText('./cac:TaxScheme/cbc:ID', taxSchemeNode).trim();
424
+ if (taxSchemeId === 'VAT') {
425
+ vatId = vatId || companyId;
426
+ } else if (taxSchemeId) {
427
+ taxNumber = taxNumber || companyId;
428
+ }
429
+ }
418
430
  }
419
431
 
420
432
  // Extract registration information
@@ -456,7 +468,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
456
468
  registrationDetails: {
457
469
  vatId: vatId,
458
470
  registrationId: registrationId,
459
- registrationName: registrationName
471
+ registrationName: registrationName,
472
+ ...(taxNumber ? { taxNumber } : {})
460
473
  }
461
474
  };
462
475
 
@@ -89,9 +89,6 @@ export class XRechnungEncoder extends UBLEncoder {
89
89
  }
90
90
  }
91
91
 
92
- // Add country code handling for German addresses
93
- this.fixGermanCountryCodes(doc);
94
-
95
92
  // Preserve business references from metadata
96
93
  this.addBusinessReferences(doc, metadata?.businessReferences);
97
94
 
@@ -207,26 +204,6 @@ export class XRechnungEncoder extends UBLEncoder {
207
204
  }
208
205
  }
209
206
 
210
- /**
211
- * Fixes German country codes in the document
212
- * @param doc XML document
213
- */
214
- private fixGermanCountryCodes(doc: Document): void {
215
- const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
216
- for (let i = 0; i < countryNodes.length; i++) {
217
- const node = countryNodes[i];
218
- if (node.textContent) {
219
- const text = node.textContent.toLowerCase();
220
- if (text === 'germany' || text === 'deutschland' || text === 'de') {
221
- node.textContent = 'DE';
222
- } else if (text.length > 2) {
223
- // Try to use first 2 characters as country code
224
- node.textContent = text.substring(0, 2).toUpperCase();
225
- }
226
- }
227
- }
228
- }
229
-
230
207
  /**
231
208
  * Adds business references from metadata to the document
232
209
  * @param doc XML document
@@ -53,24 +53,28 @@ export class XRechnungValidator extends EN16931UBLValidator {
53
53
  valid = false;
54
54
  }
55
55
 
56
- // BR-DE-5: In Germany, the element "Seller VAT identifier" (BT-31) shall be provided.
57
- const sellerCountry = this.getText('//cac:AccountingSupplierParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
58
- if (sellerCountry === 'DE' && !this.exists('//cac:AccountingSupplierParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
56
+ // BR-DE-16 (XRechnung): when a VAT category code S, Z, E, AE, K, G, L or M is used (BT-95,
57
+ // BT-102, BT-151), the seller states a VAT identifier (BT-31) or a tax registration
58
+ // identifier (BT-32), which is any party tax scheme of the seller, or a tax representative
59
+ // (BG-11). A German seller may state its tax number instead of its VAT identification number
60
+ // (§ 14 Abs. 4 Satz 1 Nr. 2 UStG). No XRechnung rule asks for the buyer's VAT identifier.
61
+ const vatCategoryCodes = ['S', 'Z', 'E', 'AE', 'K', 'G', 'L', 'M'];
62
+ const categoryNodes = this.select(
63
+ '/*/cac:AllowanceCharge/cac:TaxCategory/cbc:ID | /*/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID | /*/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID',
64
+ this.doc
65
+ );
66
+ const usesVatCategory =
67
+ Array.isArray(categoryNodes) &&
68
+ categoryNodes.some((node) => vatCategoryCodes.includes(((node as Node).textContent ?? '').trim()));
69
+ if (
70
+ usesVatCategory &&
71
+ !this.exists('/*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID[normalize-space(.)]') &&
72
+ !this.exists('/*/cac:TaxRepresentativeParty')
73
+ ) {
59
74
  this.addError(
60
- 'BR-DE-5',
61
- 'Seller VAT identifier is required for German sellers',
62
- '//cac:AccountingSupplierParty//cac:PartyTaxScheme'
63
- );
64
- valid = false;
65
- }
66
-
67
- // BR-DE-6: In Germany, the element "Buyer VAT identifier" (BT-48) shall be provided.
68
- const buyerCountry = this.getText('//cac:AccountingCustomerParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
69
- if (buyerCountry === 'DE' && !this.exists('//cac:AccountingCustomerParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
70
- this.addError(
71
- 'BR-DE-6',
72
- 'Buyer VAT identifier is required for German buyers',
73
- '//cac:AccountingCustomerParty//cac:PartyTaxScheme'
75
+ 'BR-DE-16',
76
+ 'A document with the VAT category code S, Z, E, AE, K, G, L or M states the seller VAT identifier (BT-31), the seller tax registration identifier (BT-32) or a seller tax representative (BG-11)',
77
+ '//cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme'
74
78
  );
75
79
  valid = false;
76
80
  }
@@ -0,0 +1,53 @@
1
+ import type { business } from '@tsclass/tsclass';
2
+ import { EInvoiceFormatError } from '../../errors.js';
3
+ import { CodeLists } from '../validation/validation.types.js';
4
+
5
+ /** The party whose address a country code is written for */
6
+ export type TCountryCodeParty = 'seller' | 'buyer';
7
+
8
+ const partyFields: Record<TCountryCodeParty, { path: string; term: string; presenceRule: string }> = {
9
+ // BR-09: the seller postal address (BG-5) states the seller country code (BT-40)
10
+ seller: { path: 'from.address', term: 'BT-40', presenceRule: 'BR-09' },
11
+ // BR-11: the buyer postal address (BG-8) states the buyer country code (BT-55)
12
+ buyer: { path: 'to.address', term: 'BT-55', presenceRule: 'BR-11' },
13
+ };
14
+
15
+ /**
16
+ * The country code an encoder writes for a party's address: an ISO 3166-1
17
+ * alpha-2 code EN 16931 accepts (BR-CL-14, `CodeLists.ISO3166`), taken from
18
+ * `countryCode`, or from `country` when that is already such a code; letter
19
+ * case does not matter. A code is never derived from a country name, so a
20
+ * name, an unknown code or a missing country is refused with an
21
+ * `EInvoiceFormatError` naming the field and the business term.
22
+ * @param address The party's address
23
+ * @param party Whether it is the seller's or the buyer's
24
+ * @param targetFormat The format being written
25
+ */
26
+ export const resolveCountryCode = (
27
+ address: Partial<business.IAddress> | undefined,
28
+ party: TCountryCodeParty,
29
+ targetFormat: string,
30
+ ): string => {
31
+ const { path, term, presenceRule } = partyFields[party];
32
+ const refuse = (rule: string, message: string): never => {
33
+ throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
34
+ };
35
+ const countryCode = (address?.countryCode ?? '').trim();
36
+ if (countryCode) {
37
+ if (!CodeLists.ISO3166.codes.has(countryCode.toUpperCase())) {
38
+ refuse('BR-CL-14', `the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is "${countryCode}"`);
39
+ }
40
+ return countryCode.toUpperCase();
41
+ }
42
+ const country = (address?.country ?? '').trim();
43
+ if (!country) {
44
+ refuse(presenceRule, `the ${party} postal address states a country code (${term}), ${path}.countryCode and ${path}.country are missing`);
45
+ }
46
+ if (!CodeLists.ISO3166.codes.has(country.toUpperCase())) {
47
+ refuse(
48
+ 'BR-CL-14',
49
+ `the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is missing and ${path}.country is "${country}", which is not one; a code is not derived from a country name`,
50
+ );
51
+ }
52
+ return country.toUpperCase();
53
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The member states of the European Union, by ISO 3166-1 alpha-2 code (27
3
+ * since 1 February 2020). Greece is `GR` here, as in an address; its VAT
4
+ * identification numbers carry the prefix `EL` instead.
5
+ */
6
+ export const EU_MEMBER_STATES: ReadonlySet<string> = new Set([
7
+ 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
8
+ 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
9
+ 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
10
+ ]);
11
+
12
+ /**
13
+ * Whether an ISO 3166-1 alpha-2 country code is that of an EU member state
14
+ * @param countryCode The country code, in upper case
15
+ */
16
+ export const isEuMemberState = (countryCode: string): boolean => EU_MEMBER_STATES.has(countryCode);
@@ -49,14 +49,19 @@ export const getVatExemption = (category: TVatCategoryCode, language: string | u
49
49
  * - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
50
50
  * recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
51
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.
52
+ * - BR-AE-02: the seller states a VAT identifier (BT-31) or a tax
53
+ * registration identifier (BT-32, `registrationDetails.taxNumber`, the
54
+ * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows instead), and the
55
+ * buyer a VAT identifier (BT-48) or a legal registration identifier (BT-47).
56
+ * The rule also accepts a tax representative's VAT identifier (BT-63) for
57
+ * the seller; the envelope cannot state one, so a seller with neither BT-31
58
+ * nor BT-32 is refused.
59
+ * The envelope has no place of supply, so whether a document is one for a
60
+ * supply in another member state, whose invoice states the VAT identification
61
+ * numbers of both parties (§ 14a Abs. 1 Satz 3 UStG), cannot be told here: a
62
+ * domestic § 13b supply to a buyer based in another member state is taxed in
63
+ * Germany and may state a tax number. Only what BR-AE-02 requires is checked;
64
+ * the issuing application enforces § 14a Abs. 1.
60
65
  * @param accountingDoc The document
61
66
  * @param targetFormat The format being written
62
67
  */
@@ -73,10 +78,10 @@ export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetF
73
78
  }
74
79
  }
75
80
  const seller = accountingDoc.from?.registrationDetails;
76
- if (!seller?.vatId) {
81
+ if (!seller?.vatId && !seller?.taxNumber) {
77
82
  refuse(
78
83
  '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',
84
+ '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',
80
85
  );
81
86
  }
82
87
  const buyer = accountingDoc.to?.registrationDetails;
@@ -96,7 +96,7 @@ export class CodeListValidator {
96
96
  const buyerCountry = invoice.to?.address?.countryCode;
97
97
  if (buyerCountry && !CodeLists.ISO3166.codes.has(buyerCountry.toUpperCase())) {
98
98
  this.addError(
99
- 'BR-CL-15',
99
+ 'BR-CL-14',
100
100
  `Invalid buyer country code: ${buyerCountry}. Must be ISO 3166-1 alpha-2`,
101
101
  'EN16931',
102
102
  'to.address.countryCode',
@@ -110,7 +110,7 @@ export class CodeListValidator {
110
110
  const deliveryCountry = invoice.metadata?.deliveryAddress?.countryCode;
111
111
  if (deliveryCountry && !CodeLists.ISO3166.codes.has(deliveryCountry.toUpperCase())) {
112
112
  this.addError(
113
- 'BR-CL-16',
113
+ 'BR-CL-14',
114
114
  `Invalid delivery country code: ${deliveryCountry}. Must be ISO 3166-1 alpha-2`,
115
115
  'EN16931',
116
116
  'metadata.deliveryAddress.countryCode',
@@ -96,13 +96,30 @@ export const CodeLists = {
96
96
  ])
97
97
  },
98
98
 
99
- // ISO 3166-1 alpha-2 Country codes
99
+ // ISO 3166-1 alpha-2 country codes, as EN 16931 accepts them (BR-CL-14, BR-CL-15): the code list
100
+ // of the CEN/TC 434 validation artefacts 1.3.16 for UBL, which adds 1A (Kosovo) and XI (Northern
101
+ // Ireland) to the codes ISO 3166-1 assigns
100
102
  ISO3166: {
101
- version: '2020',
103
+ version: 'EN 16931 validation artefacts 1.3.16',
102
104
  codes: new Set([
103
- 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH', 'GB', 'IE', 'PT', 'GR',
104
- 'SE', 'NO', 'DK', 'FI', 'PL', 'CZ', 'HU', 'RO', 'BG', 'HR', 'SI', 'SK',
105
- 'LT', 'LV', 'EE', 'LU', 'MT', 'CY', 'US', 'CA', 'AU', 'NZ', 'JP', 'CN'
105
+ '1A', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU',
106
+ 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM',
107
+ 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF',
108
+ 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY',
109
+ 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET',
110
+ 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI',
111
+ 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN',
112
+ 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
113
+ 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ',
114
+ 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD',
115
+ 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT',
116
+ 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO',
117
+ 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN',
118
+ 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC',
119
+ 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST',
120
+ 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
121
+ 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC',
122
+ 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW',
106
123
  ])
107
124
  },
108
125
 
@@ -3,6 +3,7 @@ import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/documen
3
3
  import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
4
4
  import type { EInvoice } from '../../einvoice.js';
5
5
  import { CurrencyCalculator } from '../utils/currency.utils.js';
6
+ import { isEuMemberState } from '../utils/eu.memberstates.js';
6
7
  import type { ValidationResult } from './validation.types.js';
7
8
 
8
9
  /**
@@ -748,12 +749,7 @@ export class VATCategoriesValidator {
748
749
  }
749
750
 
750
751
  private isEUCountry(countryCode: string): boolean {
751
- const euCountries = [
752
- 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
753
- 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
754
- 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
755
- ];
756
- return euCountries.includes(countryCode);
752
+ return isEuMemberState(countryCode);
757
753
  }
758
754
 
759
755
  private isReverseChargeService(item: TAccountingDocItem): boolean {