@fin.cx/einvoice 9.0.0 → 10.0.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 (126) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/einvoice.d.ts +12 -0
  3. package/dist_ts/einvoice.js +45 -3
  4. package/dist_ts/formats/base/base.decoder.d.ts +27 -2
  5. package/dist_ts/formats/base/base.decoder.js +52 -27
  6. package/dist_ts/formats/cii/cii.decoder.d.ts +89 -6
  7. package/dist_ts/formats/cii/cii.decoder.js +125 -30
  8. package/dist_ts/formats/cii/cii.encoder.d.ts +71 -1
  9. package/dist_ts/formats/cii/cii.encoder.js +191 -12
  10. package/dist_ts/formats/cii/cii.types.d.ts +2 -5
  11. package/dist_ts/formats/cii/cii.types.js +7 -9
  12. package/dist_ts/formats/cii/cii.validator.d.ts +13 -6
  13. package/dist_ts/formats/cii/cii.validator.js +29 -30
  14. package/dist_ts/formats/cii/facturx/facturx.decoder.js +15 -3
  15. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +0 -6
  16. package/dist_ts/formats/cii/facturx/facturx.encoder.js +11 -25
  17. package/dist_ts/formats/cii/facturx/facturx.types.d.ts +0 -5
  18. package/dist_ts/formats/cii/facturx/facturx.types.js +1 -8
  19. package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +0 -5
  20. package/dist_ts/formats/cii/facturx/facturx.validator.js +5 -29
  21. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +15 -2
  22. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +0 -7
  23. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +9 -67
  24. package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +0 -5
  25. package/dist_ts/formats/cii/zugferd/zugferd.types.js +1 -8
  26. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +26 -0
  27. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +43 -1
  28. package/dist_ts/formats/cii/zugferd/zugferd.validator.js +6 -2
  29. package/dist_ts/formats/semantic/semantic.adapter.js +20 -18
  30. package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +0 -4
  31. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -42
  32. package/dist_ts/formats/ubl/generic/ubl.encoder.js +18 -24
  33. package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
  34. package/dist_ts/formats/ubl/ubl.decoder.js +9 -1
  35. package/dist_ts/formats/ubl/ubl.encoder.js +9 -5
  36. package/dist_ts/formats/ubl/ubl.validator.d.ts +13 -0
  37. package/dist_ts/formats/ubl/ubl.validator.js +28 -1
  38. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +10 -1
  39. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -7
  40. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +30 -45
  41. package/dist_ts/formats/ubl/xrechnung.validator.d.ts +5 -4
  42. package/dist_ts/formats/ubl/xrechnung.validator.js +71 -68
  43. package/dist_ts/formats/utils/date.value.d.ts +35 -0
  44. package/dist_ts/formats/utils/date.value.js +98 -1
  45. package/dist_ts/formats/utils/format.detector.js +16 -10
  46. package/dist_ts/formats/utils/party.contact.d.ts +16 -0
  47. package/dist_ts/formats/utils/party.contact.js +16 -0
  48. package/dist_ts/formats/utils/party.identifier.d.ts +28 -0
  49. package/dist_ts/formats/utils/party.identifier.js +49 -0
  50. package/dist_ts/formats/utils/peppol.profile.d.ts +10 -0
  51. package/dist_ts/formats/utils/peppol.profile.js +12 -0
  52. package/dist_ts/formats/utils/seller.identifier.d.ts +46 -0
  53. package/dist_ts/formats/utils/seller.identifier.js +78 -0
  54. package/dist_ts/formats/utils/stated.values.d.ts +80 -0
  55. package/dist_ts/formats/utils/stated.values.js +418 -0
  56. package/dist_ts/formats/utils/vat.category.d.ts +30 -2
  57. package/dist_ts/formats/utils/vat.category.js +36 -6
  58. package/dist_ts/formats/utils/vat.id.d.ts +18 -0
  59. package/dist_ts/formats/utils/vat.id.js +22 -0
  60. package/dist_ts/formats/validation/conformance.harness.js +6 -6
  61. package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -21
  62. package/dist_ts/formats/validation/facturx.validator.js +6 -6
  63. package/dist_ts/formats/validation/integrated.validator.js +4 -13
  64. package/dist_ts/formats/validation/peppol.validator.js +6 -13
  65. package/dist_ts/formats/validation/validation.types.d.ts +5 -0
  66. package/dist_ts/formats/validation/validation.types.js +6 -1
  67. package/dist_ts/formats/validation/vat-categories.validator.d.ts +21 -42
  68. package/dist_ts/formats/validation/vat-categories.validator.js +137 -431
  69. package/dist_ts/formats/validation/xrechnung.validator.d.ts +11 -58
  70. package/dist_ts/formats/validation/xrechnung.validator.js +58 -324
  71. package/dist_ts/index.d.ts +1 -0
  72. package/dist_ts/index.js +1 -1
  73. package/dist_ts/interfaces/en16931-metadata.d.ts +0 -4
  74. package/dist_ts/interfaces/stated.values.d.ts +93 -0
  75. package/dist_ts/interfaces/stated.values.js +2 -0
  76. package/package.json +4 -4
  77. package/readme.md +152 -7
  78. package/ts/00_commitinfo_data.ts +1 -1
  79. package/ts/einvoice.ts +52 -2
  80. package/ts/formats/base/base.decoder.ts +55 -45
  81. package/ts/formats/cii/cii.decoder.ts +159 -32
  82. package/ts/formats/cii/cii.encoder.ts +206 -14
  83. package/ts/formats/cii/cii.types.ts +7 -9
  84. package/ts/formats/cii/cii.validator.ts +30 -32
  85. package/ts/formats/cii/facturx/facturx.decoder.ts +16 -2
  86. package/ts/formats/cii/facturx/facturx.encoder.ts +13 -25
  87. package/ts/formats/cii/facturx/facturx.types.ts +0 -9
  88. package/ts/formats/cii/facturx/facturx.validator.ts +5 -43
  89. package/ts/formats/cii/zugferd/zugferd.decoder.ts +16 -1
  90. package/ts/formats/cii/zugferd/zugferd.encoder.ts +10 -72
  91. package/ts/formats/cii/zugferd/zugferd.types.ts +0 -9
  92. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +56 -0
  93. package/ts/formats/cii/zugferd/zugferd.validator.ts +5 -1
  94. package/ts/formats/semantic/semantic.adapter.ts +19 -17
  95. package/ts/formats/ubl/en16931.ubl.validator.ts +5 -64
  96. package/ts/formats/ubl/generic/ubl.encoder.ts +19 -23
  97. package/ts/formats/ubl/ubl.decoder.ts +12 -0
  98. package/ts/formats/ubl/ubl.encoder.ts +8 -4
  99. package/ts/formats/ubl/ubl.validator.ts +29 -0
  100. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +8 -0
  101. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +33 -45
  102. package/ts/formats/ubl/xrechnung.validator.ts +75 -127
  103. package/ts/formats/utils/date.value.ts +106 -0
  104. package/ts/formats/utils/format.detector.ts +15 -9
  105. package/ts/formats/utils/party.contact.ts +30 -0
  106. package/ts/formats/utils/party.identifier.ts +61 -0
  107. package/ts/formats/utils/peppol.profile.ts +13 -0
  108. package/ts/formats/utils/seller.identifier.ts +102 -0
  109. package/ts/formats/utils/stated.values.ts +499 -0
  110. package/ts/formats/utils/vat.category.ts +47 -5
  111. package/ts/formats/utils/vat.id.ts +24 -0
  112. package/ts/formats/validation/conformance.harness.ts +5 -5
  113. package/ts/formats/validation/en16931.business-rules.validator.ts +3 -28
  114. package/ts/formats/validation/facturx.validator.ts +5 -5
  115. package/ts/formats/validation/integrated.validator.ts +3 -16
  116. package/ts/formats/validation/peppol.validator.ts +5 -16
  117. package/ts/formats/validation/validation.types.ts +7 -1
  118. package/ts/formats/validation/vat-categories.validator.ts +179 -761
  119. package/ts/formats/validation/xrechnung.validator.ts +61 -382
  120. package/ts/index.ts +9 -0
  121. package/ts/interfaces/en16931-metadata.ts +3 -7
  122. package/ts/interfaces/stated.values.ts +94 -0
  123. package/ts/readme.md +1 -1
  124. package/dist_ts/formats/utils/eu.memberstates.d.ts +0 -11
  125. package/dist_ts/formats/utils/eu.memberstates.js +0 -16
  126. package/ts/formats/utils/eu.memberstates.ts +0 -16
@@ -10,6 +10,7 @@ import { computeDocumentTotals } from '../../utils/document.totals.js';
10
10
  import { getPaymentTotals } from '../../utils/paid.amount.js';
11
11
  import { toPlainDecimalString } from '../../utils/number.text.js';
12
12
  import { resolveCountryCode } from '../../utils/country.code.js';
13
+ import { getPartyIdentifiers } from '../../utils/party.identifier.js';
13
14
 
14
15
  /**
15
16
  * UBL Encoder implementation
@@ -114,6 +115,12 @@ export class UBLEncoder extends UBLBaseEncoder {
114
115
  // Document Currency Code
115
116
  this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
116
117
 
118
+ // Buyer reference (BT-10), as the buyer gave it
119
+ const buyerReference = invoice.buyerReference?.trim();
120
+ if (buyerReference) {
121
+ this.appendElement(doc, root, 'cbc:BuyerReference', buyerReference);
122
+ }
123
+
117
124
  // Invoicing period (BG-14), in schema order before the document references
118
125
  this.addInvoicePeriod(doc, root, invoice);
119
126
 
@@ -229,6 +236,18 @@ export class UBLEncoder extends UBLBaseEncoder {
229
236
 
230
237
  const partyNode = doc.createElement('cac:Party');
231
238
  partyElement.appendChild(partyNode);
239
+
240
+ // Party identifiers (BT-29 seller, BT-46 buyer), before the name in the schema order
241
+ for (const identifier of getPartyIdentifiers(party)) {
242
+ const partyIdentificationNode = doc.createElement('cac:PartyIdentification');
243
+ partyNode.appendChild(partyIdentificationNode);
244
+ const idNode = doc.createElement('cbc:ID');
245
+ if (identifier.scheme) {
246
+ idNode.setAttribute('schemeID', identifier.scheme);
247
+ }
248
+ idNode.textContent = identifier.value;
249
+ partyIdentificationNode.appendChild(idNode);
250
+ }
232
251
 
233
252
  // Party name
234
253
  const partyNameNode = doc.createElement('cac:PartyName');
@@ -902,29 +921,6 @@ export class UBLEncoder extends UBLBaseEncoder {
902
921
  }
903
922
  }
904
923
 
905
- // Add additional identifiers
906
- if (partyData.additionalIdentifiers && Array.isArray(partyData.additionalIdentifiers)) {
907
- for (const identifier of partyData.additionalIdentifiers) {
908
- const partyId = doc.createElement('cac:PartyIdentification');
909
- const id = doc.createElement('cbc:ID');
910
- if (identifier.scheme) {
911
- id.setAttribute('schemeID', identifier.scheme);
912
- }
913
- id.textContent = identifier.value;
914
- partyId.appendChild(id);
915
-
916
- // Insert after EndpointID or at beginning
917
- const endpoint = party.getElementsByTagName('cbc:EndpointID')[0];
918
- if (endpoint && endpoint.nextSibling) {
919
- party.insertBefore(partyId, endpoint.nextSibling);
920
- } else if (party.firstChild) {
921
- party.insertBefore(partyId, party.firstChild);
922
- } else {
923
- party.appendChild(partyId);
924
- }
925
- }
926
- }
927
-
928
924
  // Add contact information from metadata if not already present
929
925
  const contactInfo = partyData.metadata?.contactInformation;
930
926
  if (contactInfo) {
@@ -4,6 +4,8 @@ import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
  import { DOMParser, xpath } from '../../plugins.js';
5
5
  import { EInvoiceParsingError } from '../../errors.js';
6
6
  import { getAccountingDocType } from '../utils/document.typecode.js';
7
+ import { readUblStatedValues } from '../utils/stated.values.js';
8
+ import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
7
9
 
8
10
  const ublParser = new DOMParser();
9
11
  const ublNamespaces = {
@@ -37,11 +39,21 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
37
39
  return this.doc;
38
40
  }
39
41
 
42
+ /** The values the document states, read by `decode` */
43
+ private statedValues?: IEInvoiceStatedValues;
44
+
45
+ public override getStatedValues(): IEInvoiceStatedValues | undefined {
46
+ return this.statedValues;
47
+ }
48
+
40
49
  /**
41
50
  * Decodes UBL XML into an accounting document
42
51
  * @returns Promise resolving to the accounting document
43
52
  */
44
53
  public async decode(): Promise<TAccountingDoc> {
54
+ // the values the document states, read first: an amount that is no number is refused
55
+ this.statedValues = this.assertStatedValuesReadable(readUblStatedValues(this.doc), 'ubl');
56
+
45
57
  // a CreditNote root is a credit note whatever its type code says
46
58
  if (this.getDocumentType() === UBLDocumentType.CREDIT_NOTE) {
47
59
  return this.decodeCreditNote();
@@ -3,6 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interf
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
  import { getWritableDate } from '../utils/date.value.js';
5
5
  import { assertVatCategoryWritable } from '../utils/vat.category.js';
6
+ import { assertSellerIdentified } from '../utils/seller.identifier.js';
7
+ import { assertPartyIdentifiersWritable } from '../utils/party.identifier.js';
6
8
  import { computeDocumentTotals } from '../utils/document.totals.js';
7
9
  import { getPaymentTotals } from '../utils/paid.amount.js';
8
10
 
@@ -53,13 +55,15 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
53
55
  public async encode(invoice: TAccountingDoc): Promise<string> {
54
56
  // a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
55
57
  assertVatCategoryWritable(invoice, 'ubl');
58
+ // a buyer identifier with the SEPA scheme is refused (BR-CL-10)
59
+ assertPartyIdentifiersWritable(invoice, 'ubl');
56
60
  // a paid amount the advance payments do not add up to, or with more decimals than the currency has, is refused
57
61
  getPaymentTotals(invoice, computeDocumentTotals(invoice));
58
62
  // 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
59
- if (invoice.accountingDocType === 'creditnote') {
60
- return this.encodeCreditNote(invoice);
61
- }
62
- return this.encodeInvoice(invoice);
63
+ const xml = invoice.accountingDocType === 'creditnote' ? await this.encodeCreditNote(invoice) : await this.encodeInvoice(invoice);
64
+ // a seller the written document does not identify is refused (BR-CO-26), never written silently
65
+ assertSellerIdentified(xml, 'ubl', 'ubl');
66
+ return xml;
63
67
  }
64
68
 
65
69
  /**
@@ -4,6 +4,8 @@ import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
5
5
  import { DOMParser, xpath } from '../../plugins.js';
6
6
  import { meetsUnitCodeRule } from '../utils/unit.codes.js';
7
+ import { BR_CO_26_TEXT, ublSellerMeetsBrCo26 } from '../utils/seller.identifier.js';
8
+ import { findStatedValueDisagreements, readUblStatedValues } from '../utils/stated.values.js';
7
9
 
8
10
  const ublValidatorParser = new DOMParser();
9
11
  const ublValidatorNamespaces = {
@@ -105,6 +107,33 @@ export abstract class UBLBaseValidator extends BaseValidator {
105
107
  */
106
108
  protected abstract validateStructure(): boolean;
107
109
 
110
+ /**
111
+ * BR-CO-10 to BR-CO-17: the document totals, the VAT breakdown, the line
112
+ * amounts and the document level allowances and charges the document states
113
+ * agree with each other, as the CEN artefacts check them
114
+ * @returns True if they do
115
+ */
116
+ protected validateStatedTotals(): boolean {
117
+ const findings = findStatedValueDisagreements(readUblStatedValues(this.doc), 'ubl');
118
+ for (const finding of findings) {
119
+ this.addError(finding.ruleId, finding.message, finding.location);
120
+ }
121
+ return findings.length === 0;
122
+ }
123
+
124
+ /**
125
+ * BR-CO-26: the seller states a seller identifier (BT-29), a legal
126
+ * registration identifier (BT-30) or a VAT identifier (BT-31).
127
+ * @returns True if it does
128
+ */
129
+ protected validateSellerIdentifier(): boolean {
130
+ if (ublSellerMeetsBrCo26(this.doc)) {
131
+ return true;
132
+ }
133
+ this.addError('BR-CO-26', BR_CO_26_TEXT, '/*/cac:AccountingSupplierParty');
134
+ return false;
135
+ }
136
+
108
137
  /**
109
138
  * BR-23: every invoice or credit note line has a unit code on its invoiced or credited quantity.
110
139
  * @returns True if every line has one
@@ -271,6 +271,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
271
271
  currency: currencyCode as finance.TCurrency,
272
272
  notes: notes,
273
273
  objectActions: [],
274
+ // the buyer reference (BT-10), when stated
275
+ ...(buyerReference.trim() ? { buyerReference: buyerReference.trim() } : {}),
274
276
  // the day and the period of supply as the document states them (BT-72, BG-14)
275
277
  ...(deliveryDate ? { deliveryDate: this.parseRequiredUblDate(deliveryDate) } : {}),
276
278
  ...(periodStart && periodEnd
@@ -285,16 +287,22 @@ export class XRechnungDecoder extends UBLBaseDecoder {
285
287
  ...(paidAmount === undefined ? {} : { paidAmount }),
286
288
  };
287
289
 
290
+ // the specification identifier (BT-24), which the validators read to tell an XRechnung, a
291
+ // PEPPOL or a Factur-X document
292
+ const customizationId = this.getText('/*/cbc:CustomizationID', this.doc).trim();
288
293
  if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
289
294
  invoiceData.metadata = {
290
295
  format: 'xrechnung' as any,
291
296
  version: '1.0.0',
297
+ ...(customizationId ? { customizationId } : {}),
292
298
  extensions: {
293
299
  ...(hasBusinessReferences ? { businessReferences } : {}),
294
300
  ...(hasPaymentInformation ? { paymentInformation } : {}),
295
301
  ...(hasDateInformation ? { dateInformation } : {}),
296
302
  }
297
303
  };
304
+ } else if (customizationId) {
305
+ invoiceData.metadata = { customizationId };
298
306
  }
299
307
 
300
308
  // Validate mandatory EN16931 fields unless validation is skipped
@@ -2,6 +2,32 @@ import { UBLEncoder } from '../generic/ubl.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
3
3
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
4
4
  import { UBL_CUSTOMIZATION_IDS } from '../ubl.types.js';
5
+ import { getPartyContact } from '../../utils/party.contact.js';
6
+ import { EInvoiceFormatError } from '../../../errors.js';
7
+
8
+ /**
9
+ * The buyer reference (BT-10) an XRechnung document states: the envelope's
10
+ * `buyerReference`, or the one a decoded UBL document keeps in
11
+ * `metadata.extensions.businessReferences`. XRechnung requires it (BR-DE-15,
12
+ * "Das Element \"Buyer reference\" (BT-10) muss übermittelt werden."), and it is
13
+ * the buyer's: the buyer gives the seller an identifier of its choice, a
14
+ * Leitweg-ID for a public buyer. Without one the export is refused; a document
15
+ * for a buyer that gave none can be written as EN 16931 UBL (`ubl`), where
16
+ * BT-10 is optional.
17
+ * @param invoice The document
18
+ */
19
+ const getXRechnungBuyerReference = (invoice: TAccountingDoc): string => {
20
+ const kept: unknown = (invoice as { metadata?: { extensions?: { businessReferences?: { buyerReference?: unknown } } } }).metadata
21
+ ?.extensions?.businessReferences?.buyerReference;
22
+ const buyerReference = invoice.buyerReference?.trim() || (typeof kept === 'string' ? kept.trim() : '');
23
+ if (!buyerReference) {
24
+ throw new EInvoiceFormatError(
25
+ "BR-DE-15: an XRechnung document states the buyer reference (BT-10), buyerReference is missing. It is the identifier the buyer gave for routing the invoice (a Leitweg-ID for a public buyer) and cannot be put in place by the seller; without one, export the document as EN 16931 UBL ('ubl'), where BT-10 is optional",
26
+ { targetFormat: 'xrechnung', unsupportedFeatures: ['BR-DE-15'] },
27
+ );
28
+ }
29
+ return buyerReference;
30
+ };
5
31
 
6
32
  /**
7
33
  * Encoder for XRechnung (UBL) format
@@ -59,9 +85,11 @@ export class XRechnungEncoder extends UBLEncoder {
59
85
  customizationId.textContent = UBL_CUSTOMIZATION_IDS.XRECHNUNG;
60
86
  }
61
87
 
62
- // Add or update Buyer Reference (required for XRechnung)
88
+ // The buyer reference (BT-10), which XRechnung requires (BR-DE-15): the identifier the buyer
89
+ // gave the seller for routing the invoice, a Leitweg-ID for a public buyer. It comes from the
90
+ // buyer, so there is none to put in place of a missing one.
91
+ const buyerReferenceValue = getXRechnungBuyerReference(invoice);
63
92
  let buyerRef = root.getElementsByTagName('cbc:BuyerReference')[0];
64
- const buyerReferenceValue = (invoice as any).buyerReference || metadata?.businessReferences?.buyerReference || invoice.id;
65
93
  if (!buyerRef) {
66
94
  // Find where to insert it (after DocumentCurrencyCode)
67
95
  const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
@@ -70,7 +98,7 @@ export class XRechnungEncoder extends UBLEncoder {
70
98
  buyerRef.textContent = buyerReferenceValue;
71
99
  currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling);
72
100
  }
73
- } else if (!buyerRef.textContent || buyerRef.textContent.trim() === '') {
101
+ } else {
74
102
  buyerRef.textContent = buyerReferenceValue;
75
103
  }
76
104
 
@@ -151,29 +179,6 @@ export class XRechnungEncoder extends UBLEncoder {
151
179
  }
152
180
  }
153
181
 
154
- // Add PartyIdentification for additional identifiers
155
- if (party.additionalIdentifiers) {
156
- for (const identifier of party.additionalIdentifiers) {
157
- const partyId = doc.createElement('cac:PartyIdentification');
158
- const id = doc.createElement('cbc:ID');
159
- if (identifier.scheme) {
160
- id.setAttribute('schemeID', identifier.scheme);
161
- }
162
- id.textContent = identifier.value;
163
- partyId.appendChild(id);
164
-
165
- // Insert after EndpointID or at beginning
166
- const endpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0];
167
- if (endpoint && endpoint.nextSibling) {
168
- partyElement.insertBefore(partyId, endpoint.nextSibling);
169
- } else if (partyElement.firstChild) {
170
- partyElement.insertBefore(partyId, partyElement.firstChild);
171
- } else {
172
- partyElement.appendChild(partyId);
173
- }
174
- }
175
- }
176
-
177
182
  // Add company registration number to PartyLegalEntity
178
183
  if (party.registrationDetails?.registrationId) {
179
184
  let legalEntity = partyElement.getElementsByTagName('cac:PartyLegalEntity')[0];
@@ -444,27 +449,10 @@ export class XRechnungEncoder extends UBLEncoder {
444
449
  */
445
450
  private enhancePartyInformation(doc: Document, invoice: TAccountingDoc): void {
446
451
  // Enhance supplier party
447
- this.addContactToParty(doc, 'cac:AccountingSupplierParty', this.contactInformationOf(invoice.from));
452
+ this.addContactToParty(doc, 'cac:AccountingSupplierParty', getPartyContact(invoice.from));
448
453
 
449
454
  // Enhance customer party
450
- this.addContactToParty(doc, 'cac:AccountingCustomerParty', this.contactInformationOf(invoice.to));
451
- }
452
-
453
- /**
454
- * The contact block of a party: explicit contact metadata first, otherwise
455
- * the party's own email and phone (BR-DE-11 and BR-DE-12 require both for
456
- * the seller).
457
- * @param party Party data
458
- */
459
- private contactInformationOf(party: any): { name?: string; phone?: string; email?: string } | undefined {
460
- const explicit = party?.metadata?.contactInformation;
461
- if (explicit && (explicit.name || explicit.phone || explicit.email)) {
462
- return explicit;
463
- }
464
- if (party?.email || party?.phone) {
465
- return { name: party.name, phone: party.phone, email: party.email };
466
- }
467
- return undefined;
455
+ this.addContactToParty(doc, 'cac:AccountingCustomerParty', getPartyContact(invoice.to));
468
456
  }
469
457
 
470
458
  /**
@@ -1,31 +1,11 @@
1
1
  import { EN16931UBLValidator } from './en16931.ubl.validator.js';
2
+ import { GERMAN_VAT_ID_FORMAT_CODE, germanVatIdFormatMessage, isMalformedGermanVatId } from '../utils/vat.id.js';
2
3
 
3
4
  /**
4
5
  * XRechnung-specific validator that extends EN16931 validation
5
6
  * Implements additional German CIUS (Core Invoice Usage Specification) rules
6
7
  */
7
8
  export class XRechnungValidator extends EN16931UBLValidator {
8
- /**
9
- * Validates XRechnung-specific structure requirements
10
- */
11
- protected validateStructure(): boolean {
12
- // First validate EN16931 structure
13
- let valid = super.validateStructure();
14
-
15
- // XRechnung-specific: Check for proper customization ID
16
- const customizationID = this.getText('//cbc:CustomizationID');
17
- if (!customizationID || !customizationID.includes('xrechnung')) {
18
- this.addError(
19
- 'XRECH-STRUCT-1',
20
- 'XRechnung customization ID is missing or invalid',
21
- '//cbc:CustomizationID'
22
- );
23
- valid = false;
24
- }
25
-
26
- return valid;
27
- }
28
-
29
9
  /**
30
10
  * Validates XRechnung-specific business rules
31
11
  */
@@ -33,26 +13,6 @@ export class XRechnungValidator extends EN16931UBLValidator {
33
13
  // First validate EN16931 business rules
34
14
  let valid = super.validateBusinessRules();
35
15
 
36
- // BR-DE-1: Payment terms (BT-20) or Payment due date (BT-9) shall be provided.
37
- if (!this.exists('//cbc:PaymentDueDate') && !this.exists('//cac:PaymentTerms/cbc:Note')) {
38
- this.addError(
39
- 'BR-DE-1',
40
- 'Payment terms or Payment due date shall be provided',
41
- '//cac:PaymentTerms'
42
- );
43
- valid = false;
44
- }
45
-
46
- // BR-DE-2: The element "Buyer reference" (BT-10) shall be provided.
47
- if (!this.exists('//cbc:BuyerReference')) {
48
- this.addError(
49
- 'BR-DE-2',
50
- 'Buyer reference is required in XRechnung',
51
- '//cbc:BuyerReference'
52
- );
53
- valid = false;
54
- }
55
-
56
16
  // BR-DE-16 (XRechnung): when a VAT category code S, Z, E, AE, K, G, L or M is used (BT-95,
57
17
  // BT-102, BT-151), the seller states a VAT identifier (BT-31) or a tax registration
58
18
  // identifier (BT-32), which is any party tax scheme of the seller, or a tax representative
@@ -79,111 +39,99 @@ export class XRechnungValidator extends EN16931UBLValidator {
79
39
  valid = false;
80
40
  }
81
41
 
82
- // BR-DE-7: The element "Seller city" (BT-37) shall be provided.
83
- if (!this.exists('//cac:AccountingSupplierParty//cac:PostalAddress/cbc:CityName')) {
84
- this.addError(
85
- 'BR-DE-7',
86
- 'Seller city is required',
87
- '//cac:AccountingSupplierParty//cac:PostalAddress'
88
- );
89
- valid = false;
90
- }
42
+ // The rules below carry the IDs of the official rule sets and check what they check, in
43
+ // their context: KoSIT XRechnung Schematron 2.6.0 (BR-DE-*, and PEPPOL-EN16931-R010, which it
44
+ // includes) and the CEN EN 16931 validation artefacts 1.3.16 (BR-21, BR-49).
45
+ const supplierParty = '/*/cac:AccountingSupplierParty/cac:Party';
46
+ const customerParty = '/*/cac:AccountingCustomerParty/cac:Party';
47
+ const requireElement = (context: string, element: string, ruleId: string, message: string): void => {
48
+ for (const node of this.nodes(context)) {
49
+ if (!this.exists(`${element}[normalize-space(.)]`, node)) {
50
+ this.addError(ruleId, message, `${context}/${element}`);
51
+ valid = false;
52
+ }
53
+ }
54
+ };
91
55
 
92
- // BR-DE-8: The element "Seller post code" (BT-38) shall be provided.
93
- if (!this.exists('//cac:AccountingSupplierParty//cac:PostalAddress/cbc:PostalZone')) {
94
- this.addError(
95
- 'BR-DE-8',
96
- 'Seller post code is required',
97
- '//cac:AccountingSupplierParty//cac:PostalAddress'
98
- );
56
+ // BR-DE-1: an invoice states PAYMENT INSTRUCTIONS (BG-16)
57
+ if (!this.exists('/*/cac:PaymentMeans')) {
58
+ this.addError('BR-DE-1', 'An invoice states the payment instructions (BG-16)', '/*/cac:PaymentMeans');
99
59
  valid = false;
100
60
  }
101
-
102
- // BR-DE-9: The element "Buyer city" (BT-52) shall be provided.
103
- if (!this.exists('//cac:AccountingCustomerParty//cac:PostalAddress/cbc:CityName')) {
104
- this.addError(
105
- 'BR-DE-9',
106
- 'Buyer city is required',
107
- '//cac:AccountingCustomerParty//cac:PostalAddress'
108
- );
109
- valid = false;
61
+ // BR-49 (EN 16931): a payment instruction (BG-16) states the payment means type code (BT-81)
62
+ for (const [index, paymentMeans] of this.nodes('/*/cac:PaymentMeans').entries()) {
63
+ if (!this.exists('cbc:PaymentMeansCode', paymentMeans)) {
64
+ this.addError('BR-49', 'A payment instruction (BG-16) states the payment means type code (BT-81)', `/*/cac:PaymentMeans[${index + 1}]/cbc:PaymentMeansCode`);
65
+ valid = false;
66
+ }
110
67
  }
111
68
 
112
- // BR-DE-10: The element "Buyer post code" (BT-53) shall be provided.
113
- if (!this.exists('//cac:AccountingCustomerParty//cac:PostalAddress/cbc:PostalZone')) {
114
- this.addError(
115
- 'BR-DE-10',
116
- 'Buyer post code is required',
117
- '//cac:AccountingCustomerParty//cac:PostalAddress'
118
- );
119
- valid = false;
120
- }
69
+ // BR-DE-15: the buyer reference (BT-10) is stated
70
+ requireElement('/*', 'cbc:BuyerReference', 'BR-DE-15', 'The buyer reference (BT-10) is stated');
121
71
 
122
- // BR-DE-11: The element "Seller contact telephone number" (BT-42) shall be provided.
123
- if (!this.exists('//cac:AccountingSupplierParty//cac:Contact/cbc:Telephone')) {
124
- this.addError(
125
- 'BR-DE-11',
126
- 'Seller contact telephone number is required',
127
- '//cac:AccountingSupplierParty//cac:Contact'
128
- );
72
+ // BR-DE-2: the seller contact (BG-6) is stated; BR-DE-6 and BR-DE-7: with a telephone number
73
+ // (BT-42) and an email address (BT-43)
74
+ if (this.exists(supplierParty) && !this.exists(`${supplierParty}/cac:Contact`)) {
75
+ this.addError('BR-DE-2', 'The seller contact (BG-6) is stated', `${supplierParty}/cac:Contact`);
129
76
  valid = false;
130
77
  }
131
-
132
- // BR-DE-12: The element "Seller contact email address" (BT-43) shall be provided.
133
- if (!this.exists('//cac:AccountingSupplierParty//cac:Contact/cbc:ElectronicMail')) {
134
- this.addError(
135
- 'BR-DE-12',
136
- 'Seller contact email address is required',
137
- '//cac:AccountingSupplierParty//cac:Contact'
138
- );
139
- valid = false;
78
+ requireElement(`${supplierParty}/cac:Contact`, 'cbc:Telephone', 'BR-DE-6', 'The seller contact telephone number (BT-42) is stated');
79
+ requireElement(`${supplierParty}/cac:Contact`, 'cbc:ElectronicMail', 'BR-DE-7', 'The seller contact email address (BT-43) is stated');
80
+
81
+ // BR-DE-3, BR-DE-4: the seller city (BT-37) and post code (BT-38)
82
+ requireElement(`${supplierParty}/cac:PostalAddress`, 'cbc:CityName', 'BR-DE-3', 'The seller city (BT-37) is stated');
83
+ requireElement(`${supplierParty}/cac:PostalAddress`, 'cbc:PostalZone', 'BR-DE-4', 'The seller post code (BT-38) is stated');
84
+ // BR-DE-8, BR-DE-9: the buyer city (BT-52) and post code (BT-53)
85
+ requireElement(`${customerParty}/cac:PostalAddress`, 'cbc:CityName', 'BR-DE-8', 'The buyer city (BT-52) is stated');
86
+ requireElement(`${customerParty}/cac:PostalAddress`, 'cbc:PostalZone', 'BR-DE-9', 'The buyer post code (BT-53) is stated');
87
+
88
+ // PEPPOL-EN16931-R010, part of the XRechnung rules: the buyer electronic address (BT-49)
89
+ for (const party of this.nodes(customerParty)) {
90
+ if (!this.exists('cbc:EndpointID', party)) {
91
+ this.addError('PEPPOL-EN16931-R010', 'The buyer electronic address (BT-49) is stated', `${customerParty}/cbc:EndpointID`);
92
+ valid = false;
93
+ }
140
94
  }
141
95
 
142
- // BR-DE-13: The element "Buyer electronic address" (BT-49) shall be provided.
143
- if (!this.exists('//cac:AccountingCustomerParty//cac:Party/cbc:EndpointID')) {
144
- this.addError(
145
- 'BR-DE-13',
146
- 'Buyer electronic address (EndpointID) is required',
147
- '//cac:AccountingCustomerParty//cac:Party'
148
- );
149
- valid = false;
96
+ // BR-21 (EN 16931): each invoice line (BG-25) has an invoice line identifier (BT-126)
97
+ for (const [index, line] of this.nodes('/*/cac:InvoiceLine | /*/cac:CreditNoteLine').entries()) {
98
+ if (!this.exists('cbc:ID[normalize-space(.)]', line)) {
99
+ this.addError('BR-21', `Invoice line ${index + 1} has an invoice line identifier (BT-126)`, `${this.locationOf(line as Element)}/cbc:ID`);
100
+ valid = false;
101
+ }
150
102
  }
151
103
 
152
- // BR-DE-14: The element "Payment means type code" (BT-81) shall be provided.
153
- if (!this.exists('//cac:PaymentMeans/cbc:PaymentMeansCode')) {
154
- this.addError(
155
- 'BR-DE-14',
156
- 'Payment means type code is required',
157
- '//cac:PaymentMeans'
158
- );
104
+ // BR-CO-25 (EN 16931): while the amount due for payment (BT-115) is positive, the payment due
105
+ // date (BT-9) or the payment terms (BT-20) are stated. The CEN artefacts for UBL and CII do not
106
+ // check this rule; EN 16931 states it.
107
+ const amountDue = Number(this.getText('/*/cac:LegalMonetaryTotal/cbc:PayableAmount').trim());
108
+ if (
109
+ amountDue > 0 &&
110
+ !this.exists('/*/cbc:DueDate[normalize-space(.)] | /*/cac:PaymentMeans/cbc:PaymentDueDate[normalize-space(.)]') &&
111
+ !this.exists('/*/cac:PaymentTerms/cbc:Note[normalize-space(.)]')
112
+ ) {
113
+ this.addError('BR-CO-25', 'While an amount is due (BT-115), the payment due date (BT-9) or the payment terms (BT-20) are stated', '/*/cac:PaymentTerms');
159
114
  valid = false;
160
115
  }
161
116
 
162
- // BR-DE-15: The element "Invoice line identifier" (BT-126) shall be provided.
163
- const invoiceLines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Node[];
164
- for (let i = 0; i < invoiceLines.length; i++) {
165
- const line = invoiceLines[i];
166
- if (!this.exists('./cbc:ID', line)) {
167
- this.addError(
168
- 'BR-DE-15',
169
- `Invoice line ${i + 1} is missing identifier`,
170
- `//cac:InvoiceLine[${i + 1}]`
171
- );
117
+ // The package's own check of the German VAT identification number format (no official rule)
118
+ for (const vatId of this.nodes('/*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
119
+ const value = (vatId.textContent ?? '').trim();
120
+ if (isMalformedGermanVatId(value)) {
121
+ this.addError(GERMAN_VAT_ID_FORMAT_CODE, germanVatIdFormatMessage(value), '/*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID');
172
122
  valid = false;
173
123
  }
174
124
  }
175
125
 
176
- // German VAT ID format validation
177
- const supplierVatId = this.getText('//cac:AccountingSupplierParty//cbc:CompanyID[../cac:TaxScheme/cbc:ID="VAT"]');
178
- if (supplierVatId && supplierVatId.startsWith('DE') && !/^DE[0-9]{9}$/.test(supplierVatId)) {
179
- this.addError(
180
- 'BR-DE-VAT',
181
- 'German VAT ID format is invalid (must be DE followed by 9 digits)',
182
- '//cac:AccountingSupplierParty//cbc:CompanyID'
183
- );
184
- valid = false;
185
- }
186
-
187
126
  return valid;
188
127
  }
128
+
129
+ /**
130
+ * The nodes an XPath expression selects
131
+ * @param xpathExpr XPath expression
132
+ */
133
+ private nodes(xpathExpr: string): Node[] {
134
+ const result = this.select(xpathExpr, this.doc);
135
+ return Array.isArray(result) ? (result as Node[]) : [];
136
+ }
189
137
  }