@fin.cx/einvoice 9.0.0 → 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 (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 +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 +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 +11 -0
  44. package/dist_ts/formats/utils/date.value.js +33 -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 +2 -2
  77. package/readme.md +147 -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 +32 -29
  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 +33 -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
@@ -1,13 +1,12 @@
1
1
  import { CIIBaseEncoder } from '../cii.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
3
- import { FACTURX_PROFILE_IDS } from './facturx.types.js';
4
3
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
5
4
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
6
5
  import { computeDocumentTotals } from '../../utils/document.totals.js';
7
6
  import { getPaymentTotals } from '../../utils/paid.amount.js';
8
7
  import { toPlainDecimalString } from '../../utils/number.text.js';
9
8
  import { resolveCountryCode } from '../../utils/country.code.js';
10
- import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
9
+ import { getWritableDueDate } from '../../utils/date.value.js';
11
10
 
12
11
  /**
13
12
  * Encoder for Factur-X invoice format
@@ -94,15 +93,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
94
93
  // Add ID element with profile
95
94
  const idElement = doc.createElement('ram:ID');
96
95
 
97
- // Set profile based on the selected profile
98
- let profileId = FACTURX_PROFILE_IDS.EN16931;
99
- if (this.profile === 'BASIC') {
100
- profileId = FACTURX_PROFILE_IDS.BASIC;
101
- } else if (this.profile === 'MINIMUM') {
102
- profileId = FACTURX_PROFILE_IDS.MINIMUM;
103
- }
104
-
105
- idElement.textContent = profileId;
96
+ idElement.textContent = this.getProfileId();
106
97
  guidelineElement.appendChild(idElement);
107
98
  }
108
99
 
@@ -209,9 +200,14 @@ export class FacturXEncoder extends CIIBaseEncoder {
209
200
  const agreementElement = doc.createElement('ram:ApplicableHeaderTradeAgreement');
210
201
  transactionElement.appendChild(agreementElement);
211
202
 
203
+ // Buyer reference (BT-10)
204
+ this.addBuyerReference(doc, agreementElement, invoice);
205
+
212
206
  // Add seller
213
207
  const sellerElement = doc.createElement('ram:SellerTradeParty');
214
208
  this.addPartyInfo(doc, sellerElement, invoice.from, 'seller');
209
+ // Seller electronic address (BT-34)
210
+ this.addSellerElectronicAddress(doc, sellerElement, invoice);
215
211
  agreementElement.appendChild(sellerElement);
216
212
 
217
213
  // Add buyer
@@ -228,6 +224,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
228
224
  * @param partyRole Whether the party is the seller or the buyer
229
225
  */
230
226
  private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
227
+ // Party identifiers (BT-29 seller, BT-46 buyer)
228
+ this.addPartyIdentifiers(doc, partyElement, party);
229
+
231
230
  // Add name
232
231
  const nameElement = doc.createElement('ram:Name');
233
232
  nameElement.textContent = party.name;
@@ -381,6 +380,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
381
380
 
382
381
  settlementElement.appendChild(paymentTermsElement);
383
382
 
383
+ // Invoicing period (BG-14)
384
+ this.addInvoicingPeriod(doc, settlementElement, invoice);
385
+
384
386
  // Add totals, in the order of the CII schema
385
387
  const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
386
388
  const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
@@ -505,18 +507,4 @@ export class FacturXEncoder extends CIIBaseEncoder {
505
507
  }
506
508
  }
507
509
  }
508
-
509
- /**
510
- * Formats a date as YYYYMMDD
511
- * @param timestamp Timestamp to format
512
- * @returns Formatted date string
513
- */
514
- private formatDateYYYYMMDD(timestamp: number, field: string): string {
515
- const date = getWritableDate(timestamp, field, 'cii');
516
- // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
517
- const year = date.getUTCFullYear();
518
- const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
519
- const day = date.getUTCDate().toString().padStart(2, '0');
520
- return `${year}${month}${day}`;
521
- }
522
510
  }
@@ -1,16 +1,7 @@
1
- import { CIIProfile, CII_PROFILE_IDS } from '../cii.types.js';
2
-
3
1
  /**
4
2
  * Factur-X specific constants and types
5
3
  */
6
4
 
7
- // Factur-X profile IDs
8
- export const FACTURX_PROFILE_IDS = {
9
- MINIMUM: CII_PROFILE_IDS.FACTURX_MINIMUM,
10
- BASIC: CII_PROFILE_IDS.FACTURX_BASIC,
11
- EN16931: CII_PROFILE_IDS.FACTURX_EN16931
12
- };
13
-
14
5
  // Factur-X PDF attachment filename
15
6
  export const FACTURX_ATTACHMENT_FILENAME = 'factur-x.xml';
16
7
 
@@ -59,8 +59,8 @@ export class FacturXValidator extends CIIBaseValidator {
59
59
 
60
60
  let valid = true;
61
61
 
62
- // BR-16: Amount due for payment (BT-115) = Invoice total amount with VAT (BT-112) - Paid amount (BT-113)
63
- valid = this.validateAmounts() && valid;
62
+ // BR-CO-10 to BR-CO-17: the stated totals, VAT breakdown and line amounts agree with each other
63
+ valid = this.validateStatedTotals() && valid;
64
64
 
65
65
  // BR-CO-3: Value added tax point date (BT-7) and Value added tax point date code (BT-8) are mutually exclusive
66
66
  valid = this.validateMutuallyExclusiveFields() && valid;
@@ -70,6 +70,9 @@ export class FacturXValidator extends CIIBaseValidator {
70
70
  // and/or the Seller tax representative VAT identifier (BT-63).
71
71
  valid = this.validateSellerVatIdentifier() && valid;
72
72
 
73
+ // BR-CO-26: the seller states a seller, legal registration or VAT identifier
74
+ valid = this.validateSellerIdentifier() && valid;
75
+
73
76
  // BR-23: every line has a unit code
74
77
  valid = this.validateLineUnitCodePresence() && valid;
75
78
 
@@ -79,47 +82,6 @@ export class FacturXValidator extends CIIBaseValidator {
79
82
  return valid;
80
83
  }
81
84
 
82
- /**
83
- * Validates amount calculations in the invoice
84
- * @returns True if amount validation passed
85
- */
86
- private validateAmounts(): boolean {
87
- if (!this.doc) return false;
88
-
89
- try {
90
- // Extract amounts
91
- const totalAmount = this.getNumber(
92
- '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount'
93
- );
94
-
95
- const paidAmount = this.getNumber(
96
- '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount'
97
- ) || 0;
98
-
99
- const dueAmount = this.getNumber(
100
- '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount'
101
- );
102
-
103
- // Calculate expected due amount
104
- const expectedDueAmount = totalAmount - paidAmount;
105
-
106
- // Compare with a small tolerance for rounding errors
107
- if (Math.abs(dueAmount - expectedDueAmount) > 0.01) {
108
- this.addError(
109
- 'BR-16',
110
- `Amount due for payment (${dueAmount}) must equal Invoice total amount with VAT (${totalAmount}) - Paid amount (${paidAmount})`,
111
- '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation'
112
- );
113
- return false;
114
- }
115
-
116
- return true;
117
- } catch (error) {
118
- this.addError('FX-AMOUNT', `Error validating amounts: ${error}`, '/');
119
- return false;
120
- }
121
- }
122
-
123
85
  /**
124
86
  * Validates mutually exclusive fields
125
87
  * @returns True if validation passed
@@ -88,6 +88,12 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
88
88
  // Reverse charge: every line has the VAT category AE
89
89
  const reverseCharge = this.isReverseChargeDocument();
90
90
 
91
+ // the invoicing period (BG-14) and the seller electronic address (BT-34), when stated
92
+ const invoicingPeriod = this.invoicingPeriodFields(this.extractInvoicingPeriod());
93
+ // the buyer reference (BT-10), when stated
94
+ const buyerReference = this.extractBuyerReference();
95
+ const electronicAddress = this.extractSellerElectronicAddress();
96
+
91
97
  const paidAmount = readPaidAmount(this.getText('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount'), 'zugferd');
92
98
 
93
99
  // Create the common invoice data
@@ -116,6 +122,11 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
116
122
  notes: notes,
117
123
  // the day of supply as the document states it (BT-72)
118
124
  ...(deliveryDate === undefined ? {} : { deliveryDate }),
125
+ ...invoicingPeriod,
126
+ // the specification identifier (BT-24), with the metadata of a one-date period
127
+ ...this.metadataFields(invoicingPeriod),
128
+ ...(buyerReference === undefined ? {} : { buyerReference }),
129
+ ...(electronicAddress === undefined ? {} : { electronicAddress }),
119
130
  objectActions: [],
120
131
  ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
121
132
  // the paid amount (BT-113); the amount due (BT-115) follows from it and the totals
@@ -171,6 +182,9 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
171
182
  // the tax registration identifier (BT-32 for the seller), the Steuernummer: scheme FC
172
183
  const taxNumber = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
173
184
 
185
+ // the party identifiers (BT-29 seller, BT-46 buyer)
186
+ const additionalIdentifiers = this.extractPartyIdentifiers(partyXPath);
187
+
174
188
  // Create contact object
175
189
  return {
176
190
  type: 'company',
@@ -184,7 +198,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
184
198
  registrationId: registrationId,
185
199
  registrationName: '',
186
200
  ...(taxNumber ? { taxNumber } : {})
187
- }
201
+ },
202
+ ...(additionalIdentifiers.length > 0 ? { additionalIdentifiers } : {})
188
203
  } as business.TContact;
189
204
  }
190
205
 
@@ -1,10 +1,8 @@
1
1
  import { CIIBaseEncoder } from '../cii.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
3
- import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
4
- import { CIIProfile } from '../cii.types.js';
5
3
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
6
4
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
7
- import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
5
+ import { getWritableDueDate } from '../../utils/date.value.js';
8
6
  import { getPaymentTermsNote } from '../../utils/payment.terms.js';
9
7
  import { computeDocumentTotals } from '../../utils/document.totals.js';
10
8
  import { getPaymentTotals } from '../../utils/paid.amount.js';
@@ -15,12 +13,6 @@ import { resolveCountryCode } from '../../utils/country.code.js';
15
13
  * Encoder for ZUGFeRD invoice format
16
14
  */
17
15
  export class ZUGFeRDEncoder extends CIIBaseEncoder {
18
- constructor() {
19
- super();
20
- // Set default profile to BASIC
21
- this.profile = CIIProfile.BASIC;
22
- }
23
-
24
16
  /**
25
17
  * Encodes a credit note into ZUGFeRD XML
26
18
  * @param creditNote Credit note to encode
@@ -102,15 +94,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
102
94
  // Add ID element with profile
103
95
  const idElement = doc.createElement('ram:ID');
104
96
 
105
- // Set profile based on the selected profile
106
- let profileId = ZUGFERD_PROFILE_IDS.BASIC;
107
- if (this.profile === CIIProfile.COMFORT) {
108
- profileId = ZUGFERD_PROFILE_IDS.COMFORT;
109
- } else if (this.profile === CIIProfile.EXTENDED) {
110
- profileId = ZUGFERD_PROFILE_IDS.EXTENDED;
111
- }
112
-
113
- idElement.textContent = profileId;
97
+ idElement.textContent = this.getProfileId();
114
98
  guidelineElement.appendChild(idElement);
115
99
  }
116
100
 
@@ -217,12 +201,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
217
201
  const agreementElement = doc.createElement('ram:ApplicableHeaderTradeAgreement');
218
202
  transactionElement.appendChild(agreementElement);
219
203
 
220
- // Add buyer reference if available
221
- if (invoice.buyerReference) {
222
- const buyerRefElement = doc.createElement('ram:BuyerReference');
223
- buyerRefElement.textContent = invoice.buyerReference;
224
- agreementElement.appendChild(buyerRefElement);
225
- }
204
+ // Buyer reference (BT-10)
205
+ this.addBuyerReference(doc, agreementElement, invoice);
226
206
 
227
207
  // Add seller
228
208
  const sellerElement = doc.createElement('ram:SellerTradeParty');
@@ -230,14 +210,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
230
210
 
231
211
  // Seller electronic address (BT-34): ram:URIUniversalCommunication/ram:URIID of the party; a
232
212
  // trade contact (ram:DefinedTradeContact) has no URIID
233
- if (invoice.electronicAddress && invoice.from.type === 'company') {
234
- const communicationElement = doc.createElement('ram:URIUniversalCommunication');
235
- const uriElement = doc.createElement('ram:URIID');
236
- uriElement.setAttribute('schemeID', invoice.electronicAddress.scheme);
237
- uriElement.textContent = invoice.electronicAddress.value;
238
- communicationElement.appendChild(uriElement);
239
- sellerElement.appendChild(communicationElement);
240
- }
213
+ this.addSellerElectronicAddress(doc, sellerElement, invoice);
241
214
 
242
215
  agreementElement.appendChild(sellerElement);
243
216
 
@@ -255,6 +228,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
255
228
  * @param partyRole Whether the party is the seller or the buyer
256
229
  */
257
230
  private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
231
+ // Party identifiers (BT-29 seller, BT-46 buyer)
232
+ this.addPartyIdentifiers(doc, partyElement, party);
233
+
258
234
  // Add name
259
235
  const nameElement = doc.createElement('ram:Name');
260
236
  nameElement.textContent = party.name;
@@ -443,31 +419,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
443
419
  this.addTaxDetails(doc, settlementElement, invoice);
444
420
 
445
421
  // Invoicing period (BG-14): in the header settlement, where the CII schema puts it
446
- if (invoice.periodOfPerformance) {
447
- const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
448
-
449
- // Start date
450
- if (invoice.periodOfPerformance.from !== undefined) {
451
- const startDateElement = doc.createElement('ram:StartDateTime');
452
- const startDateStringElement = doc.createElement('udt:DateTimeString');
453
- startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
454
- startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
455
- startDateElement.appendChild(startDateStringElement);
456
- periodElement.appendChild(startDateElement);
457
- }
458
-
459
- // End date
460
- if (invoice.periodOfPerformance.to !== undefined) {
461
- const endDateElement = doc.createElement('ram:EndDateTime');
462
- const endDateStringElement = doc.createElement('udt:DateTimeString');
463
- endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
464
- endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
465
- endDateElement.appendChild(endDateStringElement);
466
- periodElement.appendChild(endDateElement);
467
- }
468
-
469
- settlementElement.appendChild(periodElement);
470
- }
422
+ this.addInvoicingPeriod(doc, settlementElement, invoice);
471
423
 
472
424
  // Add totals
473
425
  this.addMonetarySummation(doc, settlementElement, invoice);
@@ -663,18 +615,4 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
663
615
  }
664
616
  }
665
617
  }
666
-
667
- /**
668
- * Formats a date as YYYYMMDD
669
- * @param timestamp Timestamp to format
670
- * @returns Formatted date string
671
- */
672
- private formatDateYYYYMMDD(timestamp: number, field: string): string {
673
- const date = getWritableDate(timestamp, field, 'cii');
674
- // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
675
- const year = date.getUTCFullYear();
676
- const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
677
- const day = date.getUTCDate().toString().padStart(2, '0');
678
- return `${year}${month}${day}`;
679
- }
680
- }
618
+ }
@@ -1,16 +1,7 @@
1
- import { CIIProfile, CII_PROFILE_IDS } from '../cii.types.js';
2
-
3
1
  /**
4
2
  * ZUGFeRD specific constants and types
5
3
  */
6
4
 
7
- // ZUGFeRD profile IDs
8
- export const ZUGFERD_PROFILE_IDS = {
9
- BASIC: CII_PROFILE_IDS.ZUGFERD_BASIC,
10
- COMFORT: CII_PROFILE_IDS.ZUGFERD_COMFORT,
11
- EXTENDED: CII_PROFILE_IDS.ZUGFERD_EXTENDED
12
- };
13
-
14
5
  // ZUGFeRD PDF attachment filename
15
6
  export const ZUGFERD_ATTACHMENT_FILENAME = 'zugferd-invoice.xml';
16
7
 
@@ -1,4 +1,5 @@
1
1
  import { CIIBaseDecoder } from '../cii.decoder.js';
2
+ import type { ICIIInvoicingPeriod } from '../cii.decoder.js';
2
3
  import type {
3
4
  TAccountingDocEnvelope,
4
5
  TAccountingDocType,
@@ -9,6 +10,7 @@ import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
9
10
  import { business, finance, xpath } from '../../../plugins.js';
10
11
  import { EN16931Validator } from '../../validation/en16931.validator.js';
11
12
  import { readPaidAmount } from '../../utils/paid.amount.js';
13
+ import type { IEInvoiceStatedValues } from '../../../interfaces/stated.values.js';
12
14
 
13
15
  /** XPath selection with the ZUGFeRD v1 namespaces */
14
16
  const zugferdV1Select = xpath.useNamespaces({
@@ -69,6 +71,38 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
69
71
  return dueDate ? this.parseRequiredCIIDate(dueDate, this.v1Text(`${dueDatePath}/@format`)) : undefined;
70
72
  }
71
73
 
74
+ /**
75
+ * Reads the buyer reference of the v1 header trade agreement
76
+ * (`ram:ApplicableSupplyChainTradeAgreement/ram:BuyerReference`); undefined
77
+ * when the document states none.
78
+ */
79
+ protected override extractBuyerReference(): string | undefined {
80
+ return (
81
+ this.v1Text('/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeAgreement/ram:BuyerReference') ||
82
+ undefined
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Reads the identifier of the v1 document context
88
+ * (`rsm:SpecifiedExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID`);
89
+ * undefined when the document states none.
90
+ */
91
+ protected override extractSpecificationIdentifier(): string | undefined {
92
+ return (
93
+ this.v1Text('/rsm:CrossIndustryDocument/rsm:SpecifiedExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID') ||
94
+ undefined
95
+ );
96
+ }
97
+
98
+ /**
99
+ * ZUGFeRD 1.0 predates EN 16931 and has no business terms to read the stated
100
+ * values by; a v1 document has none
101
+ */
102
+ protected override readStatedValues(): IEInvoiceStatedValues | undefined {
103
+ return undefined;
104
+ }
105
+
72
106
  /** The trimmed text of the first node a v1 path selects, or '' */
73
107
  private v1Text(path: string): string {
74
108
  return String(zugferdV1Select(`string(${path})`, this.doc)).trim();
@@ -90,6 +124,19 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
90
124
  return this.parseRequiredCIIDate(deliveryDate, format);
91
125
  }
92
126
 
127
+ /**
128
+ * Reads the invoicing period of the v1 header settlement
129
+ * (`ram:ApplicableSupplyChainTradeSettlement/ram:BillingSpecifiedPeriod`).
130
+ * A ZUGFeRD 1.0 trade party has no electronic address, so a v1 document has
131
+ * no seller electronic address (BT-34).
132
+ */
133
+ protected override extractInvoicingPeriod(): ICIIInvoicingPeriod {
134
+ return this.readInvoicingPeriod(
135
+ '/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeSettlement/ram:BillingSpecifiedPeriod',
136
+ (path) => this.v1Text(path),
137
+ );
138
+ }
139
+
93
140
  /**
94
141
  * Decodes a ZUGFeRD v1 credit note
95
142
  * @returns Promise resolving to a TCreditNote object
@@ -164,6 +211,11 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
164
211
  // Reverse charge: every line has the VAT category AE
165
212
  const reverseCharge = this.isReverseChargeDocument();
166
213
 
214
+ // the invoicing period (BG-14), when stated
215
+ const invoicingPeriod = this.invoicingPeriodFields(this.extractInvoicingPeriod());
216
+ // the buyer reference (BT-10), when stated
217
+ const buyerReference = this.extractBuyerReference();
218
+
167
219
  // the paid amount (BT-113) of the v1 header settlement, read with the v1 namespaces
168
220
  const paidAmount = readPaidAmount(
169
221
  this.v1Text(
@@ -198,6 +250,10 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
198
250
  notes: notes,
199
251
  // the day of supply as the document states it (BT-72), never the issue date in its place
200
252
  ...(deliveryDate === undefined ? {} : { deliveryDate }),
253
+ ...invoicingPeriod,
254
+ // the specification identifier (BT-24), with the metadata of a one-date period
255
+ ...this.metadataFields(invoicingPeriod),
256
+ ...(buyerReference === undefined ? {} : { buyerReference }),
201
257
  ...(paidAmount === undefined ? {} : { paidAmount }),
202
258
  objectActions: []
203
259
  };
@@ -24,9 +24,13 @@ export class ZUGFeRDValidator extends CIIBaseValidator {
24
24
  * @returns True if business validation passed
25
25
  */
26
26
  protected validateBusinessRules(): boolean {
27
+ // BR-CO-10 to BR-CO-17: the stated totals, VAT breakdown and line amounts agree with each other
28
+ const totalsAgree = this.validateStatedTotals();
29
+ // BR-CO-26: the seller states a seller, legal registration or VAT identifier
30
+ const sellerIdentified = this.validateSellerIdentifier();
27
31
  // BR-23: every line has a unit code
28
32
  const linesHaveUnits = this.validateLineUnitCodePresence();
29
33
  // BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
30
- return this.validateUnitCodes() && linesHaveUnits;
34
+ return this.validateUnitCodes() && linesHaveUnits && sellerIdentified && totalsAgree;
31
35
  }
32
36
  }
@@ -45,14 +45,15 @@ export class SemanticModelAdapter {
45
45
  },
46
46
 
47
47
  // Process metadata
48
- processControl: invoice.metadata?.profileId ? {
49
- businessProcessType: invoice.metadata?.extensions?.businessProcessId,
50
- specificationIdentifier: invoice.metadata.profileId
48
+ // the specification identifier (BT-24) is customizationId, the business process type (BT-23) profileId
49
+ processControl: invoice.metadata?.customizationId ? {
50
+ ...(invoice.metadata.profileId ? { businessProcessType: invoice.metadata.profileId } : {}),
51
+ specificationIdentifier: invoice.metadata.customizationId
51
52
  } : undefined,
52
53
 
53
54
  // References
54
55
  references: {
55
- buyerReference: invoice.metadata?.buyerReference,
56
+ buyerReference: invoice.buyerReference,
56
57
  projectReference: invoice.metadata?.extensions?.projectReference,
57
58
  contractReference: invoice.metadata?.extensions?.contractReference,
58
59
  purchaseOrderReference: invoice.metadata?.extensions?.purchaseOrderReference,
@@ -143,19 +144,18 @@ export class SemanticModelAdapter {
143
144
  if (model.processControl) {
144
145
  invoice.metadata = {
145
146
  ...invoice.metadata,
146
- profileId: model.processControl.specificationIdentifier,
147
- extensions: {
148
- ...invoice.metadata?.extensions,
149
- businessProcessId: model.processControl.businessProcessType
150
- }
147
+ customizationId: model.processControl.specificationIdentifier,
148
+ ...(model.processControl.businessProcessType ? { profileId: model.processControl.businessProcessType } : {}),
151
149
  };
152
150
  }
153
151
 
154
- // Set references
152
+ // Set references; the buyer reference (BT-10) is the envelope's
155
153
  if (model.references) {
154
+ if (model.references.buyerReference) {
155
+ invoice.buyerReference = model.references.buyerReference;
156
+ }
156
157
  invoice.metadata = {
157
158
  ...invoice.metadata,
158
- buyerReference: model.references.buyerReference,
159
159
  extensions: {
160
160
  ...invoice.metadata?.extensions,
161
161
  contractReference: model.references.contractReference,
@@ -257,8 +257,8 @@ export class SemanticModelAdapter {
257
257
  tradingName: contact.tradingName,
258
258
  identifier: contact.registrationDetails?.registrationId,
259
259
  legalRegistrationIdentifier: contact.registrationDetails?.registrationId,
260
- vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
261
- taxRegistrationIdentifier: contact.taxId,
260
+ vatIdentifier: contact.registrationDetails?.vatId || undefined,
261
+ taxRegistrationIdentifier: contact.registrationDetails?.taxNumber || undefined,
262
262
  additionalLegalInfo: contact.description,
263
263
  electronicAddress: contact.email || contact.contact?.email
264
264
  };
@@ -266,7 +266,8 @@ export class SemanticModelAdapter {
266
266
  return {
267
267
  name: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
268
268
  identifier: contact.registrationDetails?.registrationId,
269
- vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
269
+ vatIdentifier: contact.registrationDetails?.vatId || undefined,
270
+ taxRegistrationIdentifier: contact.registrationDetails?.taxNumber || undefined,
270
271
  electronicAddress: contact.email
271
272
  };
272
273
  }
@@ -283,14 +284,14 @@ export class SemanticModelAdapter {
283
284
  tradingName: contact.tradingName,
284
285
  identifier: contact.registrationDetails?.registrationId,
285
286
  legalRegistrationIdentifier: contact.registrationDetails?.registrationId,
286
- vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
287
+ vatIdentifier: contact.registrationDetails?.vatId || undefined,
287
288
  electronicAddress: contact.email || contact.contact?.email
288
289
  };
289
290
  } else {
290
291
  return {
291
292
  name: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
292
293
  identifier: contact.registrationDetails?.registrationId,
293
- vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
294
+ vatIdentifier: contact.registrationDetails?.vatId || undefined,
294
295
  electronicAddress: contact.email
295
296
  };
296
297
  }
@@ -509,7 +510,8 @@ export class SemanticModelAdapter {
509
510
  registrationDetails: {
510
511
  vatId: seller.vatIdentifier || '',
511
512
  registrationId: seller.identifier || seller.legalRegistrationIdentifier || '',
512
- registrationName: seller.name
513
+ registrationName: seller.name,
514
+ ...(seller.taxRegistrationIdentifier ? { taxNumber: seller.taxRegistrationIdentifier } : {})
513
515
  },
514
516
  status: 'active',
515
517
  foundedDate: {
@@ -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
  }