@fin.cx/einvoice 8.3.1 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/einvoice.d.ts +26 -0
  3. package/dist_ts/einvoice.js +61 -3
  4. package/dist_ts/formats/base/base.decoder.d.ts +18 -1
  5. package/dist_ts/formats/base/base.decoder.js +30 -17
  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 +195 -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 +19 -3
  15. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +0 -6
  16. package/dist_ts/formats/cii/facturx/facturx.encoder.js +17 -26
  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 +19 -2
  22. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +0 -7
  23. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +15 -68
  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 +47 -1
  28. package/dist_ts/formats/cii/zugferd/zugferd.validator.js +6 -2
  29. package/dist_ts/formats/semantic/semantic.adapter.js +29 -20
  30. package/dist_ts/formats/semantic/semantic.validator.js +17 -2
  31. package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +0 -4
  32. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -42
  33. package/dist_ts/formats/ubl/generic/ubl.encoder.js +29 -26
  34. package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
  35. package/dist_ts/formats/ubl/ubl.decoder.js +9 -1
  36. package/dist_ts/formats/ubl/ubl.encoder.js +13 -5
  37. package/dist_ts/formats/ubl/ubl.validator.d.ts +13 -0
  38. package/dist_ts/formats/ubl/ubl.validator.js +28 -1
  39. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +14 -1
  40. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -7
  41. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +30 -45
  42. package/dist_ts/formats/ubl/xrechnung.validator.d.ts +5 -4
  43. package/dist_ts/formats/ubl/xrechnung.validator.js +71 -68
  44. package/dist_ts/formats/utils/date.value.d.ts +11 -0
  45. package/dist_ts/formats/utils/date.value.js +33 -1
  46. package/dist_ts/formats/utils/document.totals.d.ts +1 -3
  47. package/dist_ts/formats/utils/document.totals.js +1 -2
  48. package/dist_ts/formats/utils/format.detector.js +16 -10
  49. package/dist_ts/formats/utils/paid.amount.d.ts +44 -0
  50. package/dist_ts/formats/utils/paid.amount.js +124 -0
  51. package/dist_ts/formats/utils/party.contact.d.ts +16 -0
  52. package/dist_ts/formats/utils/party.contact.js +16 -0
  53. package/dist_ts/formats/utils/party.identifier.d.ts +28 -0
  54. package/dist_ts/formats/utils/party.identifier.js +49 -0
  55. package/dist_ts/formats/utils/peppol.profile.d.ts +10 -0
  56. package/dist_ts/formats/utils/peppol.profile.js +12 -0
  57. package/dist_ts/formats/utils/preceding.invoice.d.ts +12 -2
  58. package/dist_ts/formats/utils/preceding.invoice.js +22 -3
  59. package/dist_ts/formats/utils/seller.identifier.d.ts +46 -0
  60. package/dist_ts/formats/utils/seller.identifier.js +78 -0
  61. package/dist_ts/formats/utils/stated.values.d.ts +80 -0
  62. package/dist_ts/formats/utils/stated.values.js +418 -0
  63. package/dist_ts/formats/utils/vat.category.d.ts +30 -2
  64. package/dist_ts/formats/utils/vat.category.js +36 -6
  65. package/dist_ts/formats/utils/vat.id.d.ts +18 -0
  66. package/dist_ts/formats/utils/vat.id.js +22 -0
  67. package/dist_ts/formats/validation/conformance.harness.js +6 -6
  68. package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -19
  69. package/dist_ts/formats/validation/facturx.validator.js +6 -6
  70. package/dist_ts/formats/validation/integrated.validator.js +4 -13
  71. package/dist_ts/formats/validation/peppol.validator.js +6 -13
  72. package/dist_ts/formats/validation/validation.types.d.ts +5 -0
  73. package/dist_ts/formats/validation/validation.types.js +6 -1
  74. package/dist_ts/formats/validation/vat-categories.validator.d.ts +21 -42
  75. package/dist_ts/formats/validation/vat-categories.validator.js +137 -431
  76. package/dist_ts/formats/validation/xrechnung.validator.d.ts +11 -58
  77. package/dist_ts/formats/validation/xrechnung.validator.js +58 -324
  78. package/dist_ts/index.d.ts +1 -0
  79. package/dist_ts/index.js +1 -1
  80. package/dist_ts/interfaces/common.d.ts +1 -0
  81. package/dist_ts/interfaces/en16931-metadata.d.ts +0 -5
  82. package/dist_ts/interfaces/stated.values.d.ts +93 -0
  83. package/dist_ts/interfaces/stated.values.js +2 -0
  84. package/package.json +2 -2
  85. package/readme.md +182 -6
  86. package/ts/00_commitinfo_data.ts +1 -1
  87. package/ts/einvoice.ts +70 -2
  88. package/ts/formats/base/base.decoder.ts +32 -29
  89. package/ts/formats/cii/cii.decoder.ts +159 -32
  90. package/ts/formats/cii/cii.encoder.ts +210 -14
  91. package/ts/formats/cii/cii.types.ts +7 -9
  92. package/ts/formats/cii/cii.validator.ts +30 -32
  93. package/ts/formats/cii/facturx/facturx.decoder.ts +21 -2
  94. package/ts/formats/cii/facturx/facturx.encoder.ts +19 -26
  95. package/ts/formats/cii/facturx/facturx.types.ts +0 -9
  96. package/ts/formats/cii/facturx/facturx.validator.ts +5 -43
  97. package/ts/formats/cii/zugferd/zugferd.decoder.ts +21 -1
  98. package/ts/formats/cii/zugferd/zugferd.encoder.ts +16 -73
  99. package/ts/formats/cii/zugferd/zugferd.types.ts +0 -9
  100. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +66 -0
  101. package/ts/formats/cii/zugferd/zugferd.validator.ts +5 -1
  102. package/ts/formats/semantic/semantic.adapter.ts +28 -19
  103. package/ts/formats/semantic/semantic.validator.ts +17 -1
  104. package/ts/formats/ubl/en16931.ubl.validator.ts +5 -64
  105. package/ts/formats/ubl/generic/ubl.encoder.ts +31 -25
  106. package/ts/formats/ubl/ubl.decoder.ts +12 -0
  107. package/ts/formats/ubl/ubl.encoder.ts +12 -4
  108. package/ts/formats/ubl/ubl.validator.ts +29 -0
  109. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +13 -0
  110. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +33 -45
  111. package/ts/formats/ubl/xrechnung.validator.ts +75 -127
  112. package/ts/formats/utils/date.value.ts +33 -0
  113. package/ts/formats/utils/document.totals.ts +1 -4
  114. package/ts/formats/utils/format.detector.ts +15 -9
  115. package/ts/formats/utils/paid.amount.ts +160 -0
  116. package/ts/formats/utils/party.contact.ts +30 -0
  117. package/ts/formats/utils/party.identifier.ts +61 -0
  118. package/ts/formats/utils/peppol.profile.ts +13 -0
  119. package/ts/formats/utils/preceding.invoice.ts +23 -2
  120. package/ts/formats/utils/seller.identifier.ts +102 -0
  121. package/ts/formats/utils/stated.values.ts +499 -0
  122. package/ts/formats/utils/vat.category.ts +47 -5
  123. package/ts/formats/utils/vat.id.ts +24 -0
  124. package/ts/formats/validation/conformance.harness.ts +5 -5
  125. package/ts/formats/validation/en16931.business-rules.validator.ts +3 -26
  126. package/ts/formats/validation/facturx.validator.ts +5 -5
  127. package/ts/formats/validation/integrated.validator.ts +3 -16
  128. package/ts/formats/validation/peppol.validator.ts +5 -16
  129. package/ts/formats/validation/validation.types.ts +7 -1
  130. package/ts/formats/validation/vat-categories.validator.ts +179 -761
  131. package/ts/formats/validation/xrechnung.validator.ts +61 -382
  132. package/ts/index.ts +9 -0
  133. package/ts/interfaces/common.ts +1 -0
  134. package/ts/interfaces/en16931-metadata.ts +3 -8
  135. package/ts/interfaces/stated.values.ts +94 -0
  136. package/ts/readme.md +1 -1
  137. package/dist_ts/formats/utils/eu.memberstates.d.ts +0 -11
  138. package/dist_ts/formats/utils/eu.memberstates.js +0 -16
  139. package/ts/formats/utils/eu.memberstates.ts +0 -16
@@ -3,7 +3,8 @@
3
3
  * Validates invoices against EN16931 Business Terms and Business Groups
4
4
  */
5
5
 
6
- import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
6
+ import { computeDocumentTotals, findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
7
+ import { findPaymentProblem } from '../utils/paid.amount.js';
7
8
  import type { ValidationResult } from '../validation/validation.types.js';
8
9
  import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
9
10
  import type { EInvoice } from '../../einvoice.js';
@@ -57,6 +58,21 @@ export class SemanticModelValidator {
57
58
  return results;
58
59
  }
59
60
 
61
+ // The amount due (BT-115) follows from the paid amount; a paid amount that cannot be written
62
+ // (more decimals than the currency has, or advance payments that do not add up to it) is
63
+ // reported and the model rules are skipped, as for a line amount that is no number
64
+ const paymentProblem = findPaymentProblem(invoice, computeDocumentTotals(invoice));
65
+ if (paymentProblem) {
66
+ results.push({
67
+ ruleId: paymentProblem.ruleId,
68
+ source: 'SEMANTIC',
69
+ severity: 'error',
70
+ message: paymentProblem.message,
71
+ field: paymentProblem.field,
72
+ });
73
+ return results;
74
+ }
75
+
60
76
  // Convert to semantic model
61
77
  const model = this.adapter.toSemanticModel(invoice);
62
78
 
@@ -148,76 +148,17 @@ export class EN16931UBLValidator extends UBLBaseValidator {
148
148
  valid = false;
149
149
  }
150
150
 
151
+ // BR-CO-26: the seller states a seller, legal registration or VAT identifier
152
+ valid = this.validateSellerIdentifier() && valid;
153
+
151
154
  // BR-23: every line has a unit code
152
155
  valid = this.validateLineUnitCodePresence() && valid;
153
156
 
154
157
  // BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
155
158
  valid = this.validateUnitCodes() && valid;
156
159
 
157
- // Validate calculation rules if we have the necessary data
158
- if (this.exists('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount')) {
159
- valid = this.validateCalculationRules() && valid;
160
- }
161
-
162
- return valid;
163
- }
164
-
165
- /**
166
- * Validates calculation rules (BR-CO-*)
167
- */
168
- private validateCalculationRules(): boolean {
169
- let valid = true;
170
-
171
- // BR-CO-10: Sum of Invoice line net amount = Σ Invoice line net amount.
172
- const lineExtensionAmount = this.getNumber('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount');
173
- const lines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Node[];
174
-
175
- let calculatedSum = 0;
176
- for (const line of lines) {
177
- const lineAmount = this.getNumber('.//cbc:LineExtensionAmount', line);
178
- calculatedSum += lineAmount;
179
- }
180
-
181
- // Allow for small rounding differences (0.01)
182
- if (Math.abs(lineExtensionAmount - calculatedSum) > 0.01) {
183
- this.addError(
184
- 'BR-CO-10',
185
- `Sum of Invoice line net amount (${lineExtensionAmount}) must equal sum of line amounts (${calculatedSum})`,
186
- '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount'
187
- );
188
- valid = false;
189
- }
190
-
191
- // BR-CO-13: Invoice total amount without VAT = Σ Invoice line net amount - Sum of allowances on document level + Sum of charges on document level.
192
- const taxExclusiveAmount = this.getNumber('//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount');
193
- const allowanceTotal = this.getNumber('//cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount') || 0;
194
- const chargeTotal = this.getNumber('//cac:LegalMonetaryTotal/cbc:ChargeTotalAmount') || 0;
195
-
196
- const calculatedTaxExclusive = lineExtensionAmount - allowanceTotal + chargeTotal;
197
-
198
- if (Math.abs(taxExclusiveAmount - calculatedTaxExclusive) > 0.01) {
199
- this.addError(
200
- 'BR-CO-13',
201
- `Invoice total amount without VAT (${taxExclusiveAmount}) must equal calculated amount (${calculatedTaxExclusive})`,
202
- '//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'
203
- );
204
- valid = false;
205
- }
206
-
207
- // BR-CO-15: Invoice total amount with VAT = Invoice total amount without VAT + Invoice total VAT amount.
208
- const taxInclusiveAmount = this.getNumber('//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount');
209
- const totalTaxAmount = this.getNumber('//cac:TaxTotal/cbc:TaxAmount') || 0;
210
-
211
- const calculatedTaxInclusive = taxExclusiveAmount + totalTaxAmount;
212
-
213
- if (Math.abs(taxInclusiveAmount - calculatedTaxInclusive) > 0.01) {
214
- this.addError(
215
- 'BR-CO-15',
216
- `Invoice total amount with VAT (${taxInclusiveAmount}) must equal calculated amount (${calculatedTaxInclusive})`,
217
- '//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount'
218
- );
219
- valid = false;
220
- }
160
+ // BR-CO-10 to BR-CO-17: the stated totals, VAT breakdown and line amounts agree with each other
161
+ valid = this.validateStatedTotals() && valid;
221
162
 
222
163
  return valid;
223
164
  }
@@ -7,8 +7,10 @@ import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js'
7
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
+ import { getPaymentTotals } from '../../utils/paid.amount.js';
10
11
  import { toPlainDecimalString } from '../../utils/number.text.js';
11
12
  import { resolveCountryCode } from '../../utils/country.code.js';
13
+ import { getPartyIdentifiers } from '../../utils/party.identifier.js';
12
14
 
13
15
  /**
14
16
  * UBL Encoder implementation
@@ -113,6 +115,12 @@ export class UBLEncoder extends UBLBaseEncoder {
113
115
  // Document Currency Code
114
116
  this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
115
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
+
116
124
  // Invoicing period (BG-14), in schema order before the document references
117
125
  this.addInvoicePeriod(doc, root, invoice);
118
126
 
@@ -228,6 +236,18 @@ export class UBLEncoder extends UBLBaseEncoder {
228
236
 
229
237
  const partyNode = doc.createElement('cac:Party');
230
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
+ }
231
251
 
232
252
  // Party name
233
253
  const partyNameNode = doc.createElement('cac:PartyName');
@@ -481,10 +501,19 @@ export class UBLEncoder extends UBLBaseEncoder {
481
501
  taxInclusiveAmountElement.textContent = amount(totals.grandTotal);
482
502
  legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement);
483
503
 
484
- // Payable amount
504
+ // Paid amount (BT-113), before the payable amount in the schema order
505
+ const payment = getPaymentTotals(invoice, totals);
506
+ if (payment.paidAmount) {
507
+ const prepaidAmountElement = doc.createElement('cbc:PrepaidAmount');
508
+ prepaidAmountElement.setAttribute('currencyID', invoice.currency);
509
+ prepaidAmountElement.textContent = amount(payment.paidAmount);
510
+ legalMonetaryTotalNode.appendChild(prepaidAmountElement);
511
+ }
512
+
513
+ // Payable amount (BT-115)
485
514
  const payableAmountElement = doc.createElement('cbc:PayableAmount');
486
515
  payableAmountElement.setAttribute('currencyID', invoice.currency);
487
- payableAmountElement.textContent = amount(totals.duePayable);
516
+ payableAmountElement.textContent = amount(payment.duePayable);
488
517
  legalMonetaryTotalNode.appendChild(payableAmountElement);
489
518
  }
490
519
 
@@ -892,29 +921,6 @@ export class UBLEncoder extends UBLBaseEncoder {
892
921
  }
893
922
  }
894
923
 
895
- // Add additional identifiers
896
- if (partyData.additionalIdentifiers && Array.isArray(partyData.additionalIdentifiers)) {
897
- for (const identifier of partyData.additionalIdentifiers) {
898
- const partyId = doc.createElement('cac:PartyIdentification');
899
- const id = doc.createElement('cbc:ID');
900
- if (identifier.scheme) {
901
- id.setAttribute('schemeID', identifier.scheme);
902
- }
903
- id.textContent = identifier.value;
904
- partyId.appendChild(id);
905
-
906
- // Insert after EndpointID or at beginning
907
- const endpoint = party.getElementsByTagName('cbc:EndpointID')[0];
908
- if (endpoint && endpoint.nextSibling) {
909
- party.insertBefore(partyId, endpoint.nextSibling);
910
- } else if (party.firstChild) {
911
- party.insertBefore(partyId, party.firstChild);
912
- } else {
913
- party.appendChild(partyId);
914
- }
915
- }
916
- }
917
-
918
924
  // Add contact information from metadata if not already present
919
925
  const contactInfo = partyData.metadata?.contactInformation;
920
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,10 @@ 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';
8
+ import { computeDocumentTotals } from '../utils/document.totals.js';
9
+ import { getPaymentTotals } from '../utils/paid.amount.js';
6
10
 
7
11
  /**
8
12
  * The children of the document root in the order the UBL 2.1 schema requires
@@ -51,11 +55,15 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
51
55
  public async encode(invoice: TAccountingDoc): Promise<string> {
52
56
  // a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
53
57
  assertVatCategoryWritable(invoice, 'ubl');
58
+ // a buyer identifier with the SEPA scheme is refused (BR-CL-10)
59
+ assertPartyIdentifiersWritable(invoice, 'ubl');
60
+ // a paid amount the advance payments do not add up to, or with more decimals than the currency has, is refused
61
+ getPaymentTotals(invoice, computeDocumentTotals(invoice));
54
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
55
- if (invoice.accountingDocType === 'creditnote') {
56
- return this.encodeCreditNote(invoice);
57
- }
58
- 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;
59
67
  }
60
68
 
61
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
@@ -12,6 +12,7 @@ import {
12
12
  getPrecedingInvoiceFields,
13
13
  type TPrecedingInvoiceReference,
14
14
  } from '../../utils/preceding.invoice.js';
15
+ import { readPaidAmount } from '../../utils/paid.amount.js';
15
16
 
16
17
  /**
17
18
  * Decoder for XRechnung (UBL) format
@@ -240,6 +241,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
240
241
  const hasPaymentInformation = Object.values(paymentInformation).some(value => Boolean(value));
241
242
  const hasDateInformation = Object.values(dateInformation).some(value => Boolean(value));
242
243
 
244
+ // the paid amount (BT-113); the amount due (BT-115) follows from it and the totals
245
+ const paidAmount = readPaidAmount(this.getText('/*/cac:LegalMonetaryTotal/cbc:PrepaidAmount'), 'ubl');
246
+
243
247
  // Create the common invoice data with metadata only when the source XML actually contains it.
244
248
  const invoiceData: any = {
245
249
  type: 'accounting-doc' as const,
@@ -267,6 +271,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
267
271
  currency: currencyCode as finance.TCurrency,
268
272
  notes: notes,
269
273
  objectActions: [],
274
+ // the buyer reference (BT-10), when stated
275
+ ...(buyerReference.trim() ? { buyerReference: buyerReference.trim() } : {}),
270
276
  // the day and the period of supply as the document states them (BT-72, BG-14)
271
277
  ...(deliveryDate ? { deliveryDate: this.parseRequiredUblDate(deliveryDate) } : {}),
272
278
  ...(periodStart && periodEnd
@@ -278,18 +284,25 @@ export class XRechnungDecoder extends UBLBaseDecoder {
278
284
  }
279
285
  : {}),
280
286
  ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
287
+ ...(paidAmount === undefined ? {} : { paidAmount }),
281
288
  };
282
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();
283
293
  if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
284
294
  invoiceData.metadata = {
285
295
  format: 'xrechnung' as any,
286
296
  version: '1.0.0',
297
+ ...(customizationId ? { customizationId } : {}),
287
298
  extensions: {
288
299
  ...(hasBusinessReferences ? { businessReferences } : {}),
289
300
  ...(hasPaymentInformation ? { paymentInformation } : {}),
290
301
  ...(hasDateInformation ? { dateInformation } : {}),
291
302
  }
292
303
  };
304
+ } else if (customizationId) {
305
+ invoiceData.metadata = { customizationId };
293
306
  }
294
307
 
295
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
  /**