@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
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The amounts, VAT breakdown, line amounts and document level allowances and
3
+ * charges a received document states, read as they are written, with their
4
+ * EN 16931 business terms. They are not computed and not updated when the
5
+ * envelope changes, and no encoder writes them: the envelope computes its
6
+ * totals from quantity × net price (`computeDocumentTotals`), and cannot yet
7
+ * express document or line allowances and charges or a price base quantity.
8
+ * Every field is undefined when the document does not state it.
9
+ */
10
+ export interface IEInvoiceStatedValues {
11
+ /** Document totals (BG-22) */
12
+ totals: IStatedDocumentTotals;
13
+ /** VAT breakdown (BG-23), in document order */
14
+ vatBreakdown: IStatedVatBreakdown[];
15
+ /** Invoice lines (BG-25), in document order */
16
+ lines: IStatedLine[];
17
+ /** Document level allowances (BG-20), in document order */
18
+ allowances: IStatedAllowanceCharge[];
19
+ /** Document level charges (BG-21), in document order */
20
+ charges: IStatedAllowanceCharge[];
21
+ }
22
+ /** Document totals (BG-22) as a document states them */
23
+ export interface IStatedDocumentTotals {
24
+ /** Sum of Invoice line net amount (BT-106) */
25
+ lineTotal?: number;
26
+ /** Sum of allowances on document level (BT-107) */
27
+ allowanceTotal?: number;
28
+ /** Sum of charges on document level (BT-108) */
29
+ chargeTotal?: number;
30
+ /** Invoice total amount without VAT (BT-109) */
31
+ taxExclusiveTotal?: number;
32
+ /** Invoice total VAT amount (BT-110), in the document currency */
33
+ taxTotal?: number;
34
+ /** Invoice total VAT amount in accounting currency (BT-111), with the VAT accounting currency code (BT-6) */
35
+ taxTotalInAccountingCurrency?: {
36
+ amount: number;
37
+ currency: string;
38
+ };
39
+ /** Invoice total amount with VAT (BT-112) */
40
+ taxInclusiveTotal?: number;
41
+ /** Paid amount (BT-113) */
42
+ paidAmount?: number;
43
+ /** Rounding amount (BT-114) */
44
+ roundingAmount?: number;
45
+ /** Amount due for payment (BT-115) */
46
+ amountDue?: number;
47
+ }
48
+ /** One VAT breakdown (BG-23) as a document states it */
49
+ export interface IStatedVatBreakdown {
50
+ /** VAT category taxable amount (BT-116) */
51
+ taxableAmount?: number;
52
+ /** VAT category tax amount (BT-117) */
53
+ taxAmount?: number;
54
+ /** VAT category code (BT-118), UNTDID 5305 */
55
+ categoryCode?: string;
56
+ /** VAT category rate (BT-119); a category not subject to VAT states none */
57
+ rate?: number;
58
+ /** VAT exemption reason text (BT-120) */
59
+ exemptionReason?: string;
60
+ /** VAT exemption reason code (BT-121) */
61
+ exemptionReasonCode?: string;
62
+ }
63
+ /** The amounts and the VAT category of one invoice line (BG-25) as a document states them */
64
+ export interface IStatedLine {
65
+ /** Invoice line identifier (BT-126) */
66
+ lineId?: string;
67
+ /** Invoice line net amount (BT-131) */
68
+ netAmount?: number;
69
+ /** Invoiced item VAT category code (BT-151) */
70
+ vatCategoryCode?: string;
71
+ /** Invoiced item VAT rate (BT-152) */
72
+ vatRate?: number;
73
+ }
74
+ /**
75
+ * One document level allowance (BG-20: BT-92 to BT-98) or charge (BG-21:
76
+ * BT-99 to BT-105) as a document states it
77
+ */
78
+ export interface IStatedAllowanceCharge {
79
+ /** Amount (BT-92, BT-99) */
80
+ amount?: number;
81
+ /** Base amount (BT-93, BT-100) */
82
+ baseAmount?: number;
83
+ /** Percentage (BT-94, BT-101) */
84
+ percentage?: number;
85
+ /** VAT category code (BT-95, BT-102) */
86
+ vatCategoryCode?: string;
87
+ /** VAT rate (BT-96, BT-103) */
88
+ vatRate?: number;
89
+ /** Reason (BT-97, BT-104) */
90
+ reason?: string;
91
+ /** Reason code (BT-98, BT-105) */
92
+ reasonCode?: string;
93
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhdGVkLnZhbHVlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2ludGVyZmFjZXMvc3RhdGVkLnZhbHVlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/einvoice",
3
- "version": "9.0.0",
3
+ "version": "10.0.1",
4
4
  "private": false,
5
5
  "description": "A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.",
6
6
  "main": "dist_ts/index.js",
@@ -16,9 +16,9 @@
16
16
  "@git.zone/tsbuild": "^5.0.0",
17
17
  "@git.zone/tsbundle": "^2.15.0",
18
18
  "@git.zone/tsrun": "^3.0.0",
19
- "@git.zone/tstest": "^6.2.0",
19
+ "@git.zone/tstest": "^6.3.1",
20
20
  "@push.rocks/smartfile": "^13.1.3",
21
- "@types/node": "^26.6.1"
21
+ "@types/node": "^26.6.2"
22
22
  },
23
23
  "dependencies": {
24
24
  "@push.rocks/smartxml": "^2.1.0",
@@ -67,7 +67,7 @@
67
67
  ],
68
68
  "scripts": {
69
69
  "test": "pnpm run test:unit",
70
- "test:unit": "tstest test/test.cii-xrechnung.node.ts --verbose --logfile --timeout 60 && tstest test/test.einvoice.ts --verbose --logfile --timeout 60 && tstest test/test.decimal.ts --verbose --logfile --timeout 60 && tstest test/test.decimal-currency-calculator.ts --verbose --logfile --timeout 60 && tstest test/test.currency-utils.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-validators.ts --verbose --logfile --timeout 60 && tstest test/test.schematron-validator.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-creditnote.node.ts --verbose --logfile --timeout 60 && tstest test/test.accountingdoc-types.node.ts --verbose --logfile --timeout 60 && tstest test/test.corrected-invoice.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-3-due-date.node.ts --verbose --logfile --timeout 60 && tstest test/test.calendar-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.document-totals.node.ts --verbose --logfile --timeout 60 && tstest test/test.reverse-charge.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-schema-order.node.ts --verbose --logfile --timeout 60 && tstest test/test.tax-number.node.ts --verbose --logfile --timeout 60 && tstest test/test.country-codes.node.ts --verbose --logfile --timeout 60 && tstest test/test.paid-amount.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-unitcodes.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-lineunit.node.ts --verbose --logfile --timeout 60",
70
+ "test:unit": "tstest test/test.cii-xrechnung.node.ts --verbose --logfile --timeout 60 && tstest test/test.einvoice.ts --verbose --logfile --timeout 60 && tstest test/test.decimal.ts --verbose --logfile --timeout 60 && tstest test/test.decimal-currency-calculator.ts --verbose --logfile --timeout 60 && tstest test/test.currency-utils.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-validators.ts --verbose --logfile --timeout 60 && tstest test/test.schematron-validator.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-creditnote.node.ts --verbose --logfile --timeout 60 && tstest test/test.accountingdoc-types.node.ts --verbose --logfile --timeout 60 && tstest test/test.corrected-invoice.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-3-due-date.node.ts --verbose --logfile --timeout 60 && tstest test/test.calendar-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.document-totals.node.ts --verbose --logfile --timeout 60 && tstest test/test.reverse-charge.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-schema-order.node.ts --verbose --logfile --timeout 60 && tstest test/test.tax-number.node.ts --verbose --logfile --timeout 60 && tstest test/test.country-codes.node.ts --verbose --logfile --timeout 60 && tstest test/test.paid-amount.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-unitcodes.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-lineunit.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-profile.node.ts --verbose --logfile --timeout 60 && tstest test/test.validator-rule-ids.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-period-address.node.ts --verbose --logfile --timeout 60 && tstest test/test.amount-due.node.ts --verbose --logfile --timeout 60 && tstest test/test.ubl-zoned-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.vat-categories.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-buyer-reference.node.ts --verbose --logfile --timeout 60 && tstest test/test.specification-identifier.node.ts --verbose --logfile --timeout 60 && tstest test/test.seller-identifier.node.ts --verbose --logfile --timeout 60 && tstest test/test.stated-values.node.ts --verbose --logfile --timeout 60 && tstest test/test.vat-category-limits.node.ts --verbose --logfile --timeout 60 && tstest test/test.received-conformance.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-stated-dates.node.ts --verbose --logfile --timeout 60",
71
71
  "test:format": "tstest test/test.format-detection.ts --verbose --logfile --timeout 60",
72
72
  "test:corpus": "tstest test/suite/einvoice_corpus-validation/ --verbose --logfile --timeout 60",
73
73
  "test:performance": "NODE_OPTIONS=--expose-gc tstest test/suite/einvoice_performance/ --verbose --logfile --timeout 60",
package/readme.md CHANGED
@@ -222,8 +222,8 @@ The encoders write only what the document states:
222
222
  to enforce.
223
223
  - `corrections` has no element in EN 16931 and is not written. State what is
224
224
  corrected as a note (`notes`, BT-22); the encoders add no text of their own.
225
- - The Factur-X MINIMUM profile has no BG-3. A document with a preceding invoice
226
- reference is refused there with an `EInvoiceFormatError`.
225
+ - Factur-X and ZUGFeRD write BG-3 in the EN 16931 profile. The MINIMUM profile,
226
+ which has no BG-3, is not written at all (see [CII profiles](#cii-profiles)).
227
227
 
228
228
  With the reference stated, the XRechnung output of a corrected invoice passes
229
229
  the official EN 16931 1.3.16 and KoSIT XRechnung Schematron 2.6.0 rules without
@@ -267,6 +267,35 @@ console.log(business.errors);
267
267
  console.log(business.warnings ?? []);
268
268
  ```
269
269
 
270
+ At the business level, an XRechnung document in UBL syntax that was loaded from
271
+ XML is checked by the package's XRechnung validator. Its findings carry the IDs
272
+ of the official rule sets, the KoSIT XRechnung Schematron 2.6.0 and the CEN
273
+ EN 16931 validation artefacts 1.3.16, and check what those rules check, in their
274
+ context:
275
+
276
+ | Rule | What is missing |
277
+ | --- | --- |
278
+ | BR-DE-1 | Payment instructions (BG-16) |
279
+ | BR-49 | The payment means type code (BT-81) of a payment instruction |
280
+ | BR-DE-15 | The buyer reference (BT-10) |
281
+ | BR-DE-2 | The seller contact (BG-6) |
282
+ | BR-DE-6, BR-DE-7 | The telephone number (BT-42) or email address (BT-43) of the seller contact |
283
+ | BR-DE-3, BR-DE-4 | The seller city (BT-37) or post code (BT-38) |
284
+ | BR-DE-8, BR-DE-9 | The buyer city (BT-52) or post code (BT-53) |
285
+ | BR-DE-16 | The seller VAT identifier (BT-31), tax registration identifier (BT-32) or tax representative (BG-11), when a VAT category S, Z, E, AE, K, G, L or M is used |
286
+ | PEPPOL-EN16931-R010 | The buyer electronic address (BT-49), a rule the XRechnung Schematron includes |
287
+ | BR-21 | The identifier (BT-126) of an invoice line |
288
+ | BR-CO-25 | The payment due date (BT-9) or payment terms (BT-20), while an amount is due (BT-115). EN 16931 states this rule; the CEN artefacts for UBL and CII do not check it |
289
+
290
+ One check of its own has a code no official rule has: `EINVOICE-DE-VATID-FORMAT`,
291
+ a seller VAT identifier (BT-31) that begins with DE but is not DE followed by nine
292
+ digits, the format the Bundeszentralamt für Steuern issues. The official rules
293
+ do not check that format, and its finding has the source `EINVOICE`, not
294
+ `XRECHNUNG`.
295
+
296
+ These checks are a subset. The official Schematron remains the reference for
297
+ whether a document passes.
298
+
270
299
  ### Unit codes (BR-CL-23)
271
300
 
272
301
  A line's `unitType` is written as the unit code of the invoiced quantity (BT-130), so it has to
@@ -375,6 +404,22 @@ The envelope states reverse charge for the whole document; an item has no VAT
375
404
  category of its own. A document mixing reverse charge lines with standard rated
376
405
  ones cannot be expressed until `@tsclass/tsclass` gives items a category.
377
406
 
407
+ Outside reverse charge every line is written as standard rated (`S`), whose
408
+ rate BR-S-05 asks to be greater than zero. Export refuses a document with a
409
+ line at the rate 0 outside reverse charge with an `EInvoiceFormatError` naming
410
+ BR-S-05 and the line (`unsupportedFeatures` `['BR-S-05']`), and `validate()`
411
+ reports it at the business level for a document built in code. The envelope cannot state
412
+ the other categories: zero rated (`Z`), exempt (`E`), intra-community supply
413
+ (`K`), export (`G`) or not subject to VAT (`O`), until `@tsclass/tsclass`
414
+ gives an item a VAT category, so a received document with such lines, which is
415
+ read as 0 % lines, cannot be written back or converted. The VAT category rules the
416
+ conformance harness checks on an envelope (BR-S-01, -05, -08 to -10, BR-AE-01,
417
+ -02, -05, -08 to -10 and BR-CO-17) take the categories and the VAT breakdown
418
+ from the same computation the encoders write, and report them under their
419
+ EN 16931 IDs and texts. A decoded line keeps the category its document states
420
+ (`statedValues.lines[].vatCategoryCode`); one that is not S is not reported as
421
+ BR-S-05, since the envelope reads it as S.
422
+
378
423
  ## Paid amount and advance payments
379
424
 
380
425
  `paidAmount` is the gross sum received before the document was issued, the
@@ -383,7 +428,10 @@ paid amount (BT-113). Every format writes it (`cbc:PrepaidAmount`,
383
428
  total less it (BR-CO-16), zero when paid in full and negative when more was
384
429
  paid; a paid amount of zero is nothing paid and is not written. The decoders
385
430
  read it back. A paid amount with more decimals than the currency has (two at
386
- most, BR-DEC-16) is refused, not rounded.
431
+ most, BR-DEC-16) is refused, not rounded. The amount due is always derived, so
432
+ the envelope has no field for it and the business rule check of the `EInvoice`
433
+ has nothing to compare it with; the official rules check BR-CO-16 on the
434
+ written XML.
387
435
 
388
436
  A final invoice (an invoice, a corrected invoice or a self-billed invoice) may
389
437
  list the `advancePayments` it deducts (`@tsclass/tsclass` 9.9.0 on).
@@ -411,6 +459,45 @@ net and the tax of every payment. A corrected invoice's advance invoices read
411
459
  back as related documents it corrects, since BG-3 does not tell them apart,
412
460
  and an invoice's are not read.
413
461
 
462
+ ## Received documents: what they state
463
+
464
+ A document read from XML carries what it states in `statedValues`, with the
465
+ EN 16931 business terms, as written: the document totals (BG-22: BT-106 to
466
+ BT-115, and BT-111 with its currency BT-6), the VAT breakdown (BG-23: BT-116
467
+ to BT-121), each line's net amount, VAT category and rate (BT-126, BT-131,
468
+ BT-151, BT-152) and the document level allowances and charges (BG-20, BG-21:
469
+ BT-92 to BT-105). A field is undefined when the document does not state it.
470
+ UBL, XRechnung, Factur-X and ZUGFeRD 2.x documents are read; ZUGFeRD 1.0
471
+ predates EN 16931 and has none, nor has a document built in code.
472
+
473
+ ```ts
474
+ const invoice = await EInvoice.fromXml(receivedXml);
475
+ const { totals, vatBreakdown } = invoice.statedValues!;
476
+ console.log(totals.taxExclusiveTotal, totals.taxTotal, totals.amountDue);
477
+ ```
478
+
479
+ Book what the document states. The envelope's totals (`totalNet`, `totalVat`,
480
+ `totalGross`, `taxBreakdown`) are computed from quantity × net price, and the
481
+ envelope cannot express document or line allowances and charges, a price base
482
+ quantity (BT-149) or a VAT category other than standard rated and reverse
483
+ charge, so for such a document they differ from what it states. Neither value
484
+ is changed: `validate()` reports each difference at the business level as a
485
+ warning `EINVOICE-STATED-DIFFERS`, the package's own code, naming the
486
+ business term and both values. `statedValues` is not updated when the envelope
487
+ changes, and no encoder writes it: an export writes the envelope's
488
+ computation, so a received document with allowances, charges or base
489
+ quantities is not written back with the amounts it states, and a document with
490
+ lines of a category other than S and AE is refused on export (BR-S-05). The
491
+ `EINVOICE-STATED-DIFFERS` warnings are the signal that an export would not
492
+ state what was received; a faithful round trip needs `@tsclass/tsclass` to
493
+ express allowances, charges, base quantities and line categories.
494
+
495
+ The stated values are checked against each other at the business level as the
496
+ CEN artefacts 1.3.16 check them, under the official rule IDs BR-CO-10 to
497
+ BR-CO-17, in UBL and CII; these checks replace the approximate ones of earlier
498
+ releases. A stated amount that is no decimal number is refused on read with an
499
+ `EInvoiceParsingError` that names the business term.
500
+
414
501
  ## Calendar dates
415
502
 
416
503
  A calendar date of the invoice (issue date, due date, delivery date, invoicing
@@ -419,7 +506,15 @@ zone: UBL writes it as `xsd:date` (`2026-09-20`), CII in format 102
419
506
  (`20260920`). In the accounting document envelope such a day is a timestamp:
420
507
  the UTC midnight of that day, `Date.UTC(2026, 8, 20)`.
421
508
 
422
- - The decoders return that timestamp.
509
+ - The decoders return that timestamp. A UBL date may carry a time zone
510
+ (`2026-09-20+02:00`, `2026-09-20Z`); the zone does not move the day, so the
511
+ decoders return the UTC midnight of the stated day, and every format writes
512
+ that day. A CII date stated with a time of day, in format 203
513
+ (`202609202330`) or 204 (`20260920233000`), or without a format code as an
514
+ ISO 8601 date or date and time (`2026-09-20T00:30:00+02:00`), is read the
515
+ same way: the day it states, whatever the time of day and the zone. A CII
516
+ date in another form, or in a format that names no day, is refused with an
517
+ `EInvoiceParsingError`; no day is guessed.
423
518
  - The encoders write the day of that timestamp in UTC and count the payment
424
519
  term in UTC days, so what they write does not depend on the time zone of the
425
520
  machine that writes it.
@@ -436,7 +531,7 @@ the UTC midnight of that day, `Date.UTC(2026, 8, 20)`.
436
531
  | `xrechnung` | Yes | Yes | Yes | Yes | UBL export; UBL and CII import |
437
532
  | `cii` | Yes | Yes | Yes | Partial | Generic CII export currently uses the Factur-X encoder path |
438
533
  | `facturx` | Yes | Yes | Yes | Yes | Main CII-based generation path |
439
- | `zugferd` | Yes | Yes | Yes | Partial | Input supports v1 and v2+, export focuses on the current encoder |
534
+ | `zugferd` | Yes | Yes | Yes | Partial | Input supports v1 and v2+; export writes the EN 16931 profile, and v2 reads back as `facturx`, as XML and from a PDF (see [CII profiles](#cii-profiles)) |
440
535
  | `fatturapa` | Yes | No | No | Basic placeholder | Detection exists, decoder/encoder do not |
441
536
 
442
537
  ### Important format notes
@@ -444,13 +539,63 @@ the UTC midnight of that day, `Date.UTC(2026, 8, 20)`.
444
539
  - There is no dedicated root-level `peppol` export format. PEPPOL-specific checks exist, but the XML export targets are still `ubl` / `xrechnung`.
445
540
  - XRechnung import routes UBL and CII by the namespace-qualified XML root.
446
541
  Missing or invalid document-level invoice IDs and issue dates are rejected.
542
+ - The buyer reference (BT-10): every format writes `buyerReference`
543
+ (`cbc:BuyerReference`, `ram:BuyerReference`) and reads it back, ZUGFeRD 1.0
544
+ input as well. XRechnung requires it (BR-DE-15), and it is the
545
+ buyer's: the identifier the buyer gives the seller for routing the invoice, a
546
+ Leitweg-ID for a public buyer, any identifier of the buyer's choice in B2B.
547
+ `exportXml('xrechnung')` writes `buyerReference` (or the reference a decoded
548
+ UBL document keeps) and refuses a document without one with an
549
+ `EInvoiceFormatError` whose `unsupportedFeatures` is `['BR-DE-15']`; nothing,
550
+ not the invoice number, is put in its place. A seller whose buyer gave no
551
+ reference exports `ubl` (EN 16931), where BT-10 is optional. The metadata has
552
+ no buyer reference of its own: `buyerReference` is the one field.
447
553
  - `FatturaPA` should be documented as detection-only right now.
448
- - The day of supply: UBL and XRechnung write `deliveryDate` as `cac:Delivery/cbc:ActualDeliveryDate` (BT-72) and `periodOfPerformance` as `cac:InvoicePeriod` (BG-14), and read both back. Factur-X and ZUGFeRD write `deliveryDate` as `ram:ActualDeliverySupplyChainEvent` and read it back only when the document states it.
449
- - Country codes: every format writes the seller and buyer country (BT-40, BT-55) as an ISO 3166-1 alpha-2 code EN 16931 accepts (BR-CL-14), taken from `address.countryCode`, or from `address.country` when that is already such a code (letter case does not matter). A code is never derived from a country name: a name without a `countryCode`, a code EN 16931 does not know, or a missing country is refused with an `EInvoiceFormatError` naming the field (BR-CL-14, BR-09, BR-11).
554
+ - The day of supply: UBL and XRechnung write `deliveryDate` as `cac:Delivery/cbc:ActualDeliveryDate` (BT-72) and `periodOfPerformance` as `cac:InvoicePeriod` (BG-14), and read both back. Factur-X and ZUGFeRD write `deliveryDate` as `ram:ActualDeliverySupplyChainEvent` and `periodOfPerformance` as `ram:BillingSpecifiedPeriod` of the header settlement, and read each back only when the document states it; ZUGFeRD 1.0 input has its period read as well. `periodOfPerformance` has a start and an end date, so a document that states only one of them (EN 16931 allows that, BR-CO-19) reads back without `periodOfPerformance`, in every format; the date it states is kept in `metadata.extensions.dateInformation.periodStart` or `.periodEnd` (YYYY-MM-DD), and every encoder writes it back as the one date of the period, so it survives a round trip and a conversion between UBL and CII.
555
+ - The seller electronic address (BT-34): Factur-X and ZUGFeRD write `electronicAddress` as `ram:URIUniversalCommunication/ram:URIID` of the seller, with its scheme (BR-62), for a company or a person, and read it back. A ZUGFeRD 1.0 trade party has no electronic address.
556
+ - Country codes: every format writes the seller and buyer country (BT-40, BT-55) as an ISO 3166-1 alpha-2 code EN 16931 accepts (BR-CL-14), taken from `address.countryCode`, or from `address.country` when that is already such a code (letter case does not matter). A code is never derived from a country name: a name without a `countryCode`, a code EN 16931 does not know, or a missing country is refused with an `EInvoiceFormatError` naming the field (BR-CL-14, BR-09, BR-11). The list of codes is the UBL one of the CEN EN 16931 validation artefacts 1.3.16, for every format. Their CII list differs in two codes: it has AN (Netherlands Antilles, withdrawn from ISO 3166-1) and lacks SS (South Sudan). So a Factur-X or ZUGFeRD document with a party in South Sudan is written, and the official CII rules report BR-CL-14 for it; AN is refused in every format.
450
557
  - Party identifiers: `registrationDetails.vatId` is the VAT identifier (BT-31 seller, BT-48 buyer), `registrationId` the legal registration identifier such as the commercial register number (BT-30, BT-47), and `taxNumber` the seller's tax registration identifier, the Steuernummer (BT-32), which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows in place of the VAT identifier. UBL and XRechnung write the tax number as a second `cac:PartyTaxScheme` with the tax scheme `FC`, after the VAT one; Factur-X and ZUGFeRD as `ram:SpecifiedTaxRegistration/ram:ID` with the scheme `FC`. The decoders read each by its scheme, whatever order the document lists them in. EN 16931 has no tax number for the buyer, so a buyer's `taxNumber` is not written.
558
+ - The seller's identification (BR-CO-26): EN 16931 asks the seller to state a seller identifier (BT-29), a legal registration identifier (BT-30, `registrationDetails.registrationId`) or a VAT identifier (BT-31, `registrationDetails.vatId`); a tax number (BT-32) alone does not identify it. Every export whose seller states none of them is refused with an `EInvoiceFormatError` whose `unsupportedFeatures` is `['BR-CO-26']`, and `validate()` reports BR-CO-26 at the business level, for a loaded document and for one built in code. The party identifiers, the seller identifier (BT-29) and the buyer identifier (BT-46), have no typed field in the envelope: the decoders keep them in the party's `additionalIdentifiers` (`{ value, scheme }`), and every format writes them from there, UBL as `cac:PartyIdentification/cbc:ID` with the scheme as `schemeID`, CII as `ram:GlobalID` with its scheme or `ram:ID` without one. A UBL seller identification with the scheme SEPA is the bank assigned creditor identifier (BT-90) and is not written to CII. A buyer identifier with the scheme SEPA is refused (BR-CL-10): UBL admits the scheme for the seller and the payee only, CII for no party.
559
+ - The specification identifier (BT-24): the decoders set `metadata.customizationId` to the identifier the document states (UBL `cbc:CustomizationID`, CII `ram:GuidelineSpecifiedDocumentContextParameter/ram:ID`), so `MainValidator` runs the XRechnung, PEPPOL or Factur-X checks on a decoded document of that kind. The PEPPOL checks run for the PEPPOL BIS Billing 3.0 identifier only, `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0`, not for PINT, A-NZ or another derivative that names PEPPOL. `metadata.profileId` is the business process type (BT-23, UBL `cbc:ProfileID`). The encoders write the identifier of the format they write, not `customizationId`.
451
560
  - Credit notes, debit notes, corrected invoices and self-billed invoices share the `EInvoice` class; see the type code table above. Credit and debit amounts are written as given, the encoders do not change signs.
452
561
  - ZUGFeRD v1 input is detected and routed, but the v1 decoder does not read the v1 header, so such a document always arrives as an `invoice`. Known defect, to be fixed separately.
453
562
 
563
+ ### CII profiles
564
+
565
+ The Factur-X and ZUGFeRD encoders, and `cii`, which uses the Factur-X one, write
566
+ the full EN 16931 content, so they state the EN 16931 profile as the
567
+ specification identifier (BT-24): `urn:cen.eu:en16931:2017`, the identifier the
568
+ CEN validation artefacts use and the EN 16931 profile of Factur-X 1.0 and
569
+ ZUGFeRD 2.x, which ZUGFeRD also calls COMFORT.
570
+
571
+ `FacturXEncoder.setProfile` and `ZUGFeRDEncoder.setProfile` accept `EN16931` and
572
+ `COMFORT`, both written as that identifier. The other choices are refused with an
573
+ `EInvoiceFormatError` whose message names the profile and why, and whose
574
+ `unsupportedFeatures` is `['profile MINIMUM']` and so on; nothing is downgraded
575
+ to EN 16931 silently:
576
+
577
+ | Choice | Why it is refused |
578
+ | --- | --- |
579
+ | `MINIMUM` | The content written is EN 16931, which MINIMUM does not allow (no lines, no VAT breakdown, no street, postcode or city). A MINIMUM document is not an e-invoice (BMF letter of 15 October 2024, Rn. 25 and 30). |
580
+ | `BASIC` | The content written is EN 16931, which neither BASIC nor BASIC WL allows; BASIC WL has no lines and is not an e-invoice either (same letter, Rn. 25 and 30). |
581
+ | `EXTENDED` | The output has not been checked against the EXTENDED schema and rules. |
582
+
583
+ Factur-X 1.0 and ZUGFeRD 2.x are one standard with one XML, and their EN 16931
584
+ profile has one identifier. A ZUGFeRD 2.x export therefore reads back as
585
+ Factur-X, as XML and from a PDF alike, because a PDF's format is detected from
586
+ its embedded XML: `FormatDetector.detectFormat` and `getFormat()` return
587
+ `facturx`, and `FacturXDecoder` reads it. A ZUGFeRD PDF is still written with the
588
+ `zugferd-invoice.xml` attachment.
589
+
590
+ Decoding does not check the profile. A document that states another profile's
591
+ identifier, such as Factur-X MINIMUM or BASIC WL, a ZUGFeRD 2.0 identifier, the
592
+ `urn:zugferd:` identifiers earlier releases of this library wrote, or ZUGFeRD v1,
593
+ is detected and read as before. An identifier that names Factur-X reads as
594
+ `facturx`, one that names ZUGFeRD as `zugferd`: ZUGFeRD 2.0 has identifiers of
595
+ its own (`urn:zugferd.de:2p0:minimum`, and for BASIC and EXTENDED the EN 16931
596
+ identifier extended by `urn:zugferd.de:2p0:basic` or `urn:zugferd.de:2p0:extended`),
597
+ where ZUGFeRD 2.1 and Factur-X 1.0 share theirs.
598
+
454
599
  ## Public API overview
455
600
 
456
601
  Top-level exports from `@fin.cx/einvoice` include:
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/einvoice',
6
- version: '9.0.0',
6
+ version: '10.0.1',
7
7
  description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.'
8
8
  }
package/ts/einvoice.ts CHANGED
@@ -46,7 +46,11 @@ import {
46
46
  // Import enhanced validators
47
47
  import { EN16931Validator } from './formats/validation/en16931.validator.js';
48
48
  import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
49
- import { computeDocumentTotals } from './formats/utils/document.totals.js';
49
+ import { isRateNotAboveZero, standardRatedLineWithoutRateMessage } from './formats/utils/vat.category.js';
50
+ import { BR_CO_26_TEXT, envelopeSellerMeetsBrCo26 } from './formats/utils/seller.identifier.js';
51
+ import { findEnvelopeDifferences } from './formats/utils/stated.values.js';
52
+ import type { IEInvoiceStatedValues } from './interfaces/stated.values.js';
53
+ import { computeDocumentTotals, findInvalidItemAmounts } from './formats/utils/document.totals.js';
50
54
  import { CodeListValidator } from './formats/validation/codelist.validator.js';
51
55
  import type { ValidationOptions } from './formats/validation/validation.types.js';
52
56
 
@@ -176,6 +180,18 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
176
180
  */
177
181
  public paidAmount?: number;
178
182
 
183
+ /**
184
+ * What a loaded document states: its totals (BG-22), VAT breakdown (BG-23),
185
+ * line net amounts and VAT categories (BT-131, BT-151) and document level
186
+ * allowances and charges (BG-20, BG-21), with their EN 16931 business terms,
187
+ * as written. Undefined for a document built in code and for ZUGFeRD 1.0.
188
+ * The envelope's totals (`totalNet`, `totalVat`, `totalGross`) are computed
189
+ * from quantity × net price and may differ; `validate()` reports each
190
+ * difference at the business level as `EINVOICE-STATED-DIFFERS`. No encoder
191
+ * writes these values, and they are not updated when the envelope changes.
192
+ */
193
+ public statedValues?: IEInvoiceStatedValues;
194
+
179
195
  /**
180
196
  * On a final invoice (an invoice, a corrected invoice or a self-billed
181
197
  * invoice): the advance payments it deducts; `paidAmount` equals their gross
@@ -354,6 +370,8 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
354
370
 
355
371
  // Map the decoded invoice to our properties
356
372
  this.mapFromTInvoice(invoice);
373
+ // what the document states, as it states it; undefined for a ZUGFeRD 1.0 document
374
+ this.statedValues = decoder.getStatedValues();
357
375
 
358
376
  // Validate if requested
359
377
  if (this.options.validateOnLoad) {
@@ -513,7 +531,14 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
513
531
  return await encoder.encode(invoice);
514
532
  } catch (error) {
515
533
  const errorMessage = error instanceof Error ? error.message : String(error);
516
- throw new EInvoiceFormatError(`Failed to encode to ${format}: ${errorMessage}`, { targetFormat: format });
534
+ // a refusal keeps the rules it names, so a caller can tell why without reading the message
535
+ throw new EInvoiceFormatError(
536
+ `Failed to encode to ${format}: ${errorMessage}`,
537
+ {
538
+ targetFormat: format,
539
+ ...(error instanceof EInvoiceFormatError && error.unsupportedFeatures ? { unsupportedFeatures: error.unsupportedFeatures } : {}),
540
+ },
541
+ );
517
542
  }
518
543
  }
519
544
 
@@ -554,6 +579,15 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
554
579
  result = this.validateDecodedInvoice(level);
555
580
  }
556
581
 
582
+ // What a loaded document states and the envelope computes differently, reported and not
583
+ // changed: the envelope cannot express allowances, charges or a price base quantity
584
+ if (level === ValidationLevel.BUSINESS && this.statedValues && findInvalidItemAmounts(this.items).length === 0) {
585
+ const differences = findEnvelopeDifferences(this.statedValues, computeDocumentTotals(this), this.paidAmount);
586
+ result.warnings = (result.warnings ?? []).concat(
587
+ differences.map((difference) => ({ code: difference.ruleId, message: difference.message, location: difference.location })),
588
+ );
589
+ }
590
+
557
591
  // At the business level the document check covers the unit rules (BR-23, BR-CL-23) on every
558
592
  // line. The feature flag validators below check the lines again on the decoded model, which
559
593
  // cannot tell an absent unit from an empty unitCode="" attribute, so there they would report
@@ -624,6 +658,22 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
624
658
  );
625
659
 
626
660
  if (level === ValidationLevel.BUSINESS) {
661
+ // BR-S-05: a line outside reverse charge is written as standard rated, whose rate is greater than zero
662
+ if (!invoice.reverseCharge) {
663
+ (invoice.items ?? []).forEach((item, index) => {
664
+ if (isRateNotAboveZero(item.vatPercentage)) {
665
+ errors.push({
666
+ code: 'BR-S-05',
667
+ message: standardRatedLineWithoutRateMessage({ index, vatPercentage: item.vatPercentage }),
668
+ location: `items[${index}].vatPercentage`,
669
+ });
670
+ }
671
+ });
672
+ }
673
+ // BR-CO-26: the seller states a seller, legal registration or VAT identifier; a tax number alone does not identify it
674
+ if (!envelopeSellerMeetsBrCo26(invoice)) {
675
+ errors.push({ code: 'BR-CO-26', message: BR_CO_26_TEXT, location: 'from.registrationDetails' });
676
+ }
627
677
  // BR-23 and BR-CL-23: every line has a unit code, and it is a code of UN/ECE Recommendation 20
628
678
  // with the Recommendation 21 extension, as the XML validators check it on an exported document
629
679
  (invoice.items ?? []).forEach((item, index) => {
@@ -2,6 +2,9 @@ import type { TAccountingDoc } from '../../interfaces/common.js';
2
2
  import { ValidationLevel } from '../../interfaces/common.js';
3
3
  import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { EInvoiceParsingError } from '../../errors.js';
5
+ import { parseCiiDateDay, parseIsoDateTimeDay, parseXsdDateDay } from '../utils/date.value.js';
6
+ import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
7
+ import { statedValueProblemMessage, type IStatedValuesRead } from '../utils/stated.values.js';
5
8
 
6
9
  /**
7
10
  * Base decoder class that defines common decoding functionality
@@ -45,6 +48,29 @@ export abstract class BaseDecoder {
45
48
  return undefined;
46
49
  }
47
50
 
51
+ /**
52
+ * The amounts, VAT breakdown, line amounts and document level allowances
53
+ * and charges the decoded document states, as it states them; undefined for
54
+ * a format that has no EN 16931 business terms to read them by
55
+ */
56
+ public getStatedValues(): IEInvoiceStatedValues | undefined {
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Refuses a document with a stated amount that is no decimal number, naming
62
+ * the business term; nothing is put in its place
63
+ * @param read The stated values read
64
+ * @param format The format read
65
+ */
66
+ protected assertStatedValuesReadable(read: IStatedValuesRead, format: string): IEInvoiceStatedValues {
67
+ const [problem] = read.problems;
68
+ if (problem) {
69
+ throw new EInvoiceParsingError(statedValueProblemMessage(problem), { format });
70
+ }
71
+ return read.values;
72
+ }
73
+
48
74
  /**
49
75
  * Parses a CII date string based on format code
50
76
  * @param dateStr Date string
@@ -76,7 +102,15 @@ export abstract class BaseDecoder {
76
102
  }
77
103
 
78
104
  /**
79
- * Parses a required CII date without inventing a fallback value.
105
+ * Parses a required CII date without inventing a fallback value: the
106
+ * calendar day it states, as the UTC midnight of that day. Format 102
107
+ * (`YYYYMMDD`) is the one EN 16931 asks for; a date stated with a time of
108
+ * day in format 203 (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`), or without a
109
+ * format code as an ISO 8601 date or date and time (`parseIsoDateTimeDay`),
110
+ * is read as the day it states, whatever the time of day and the zone.
111
+ * Format 610 (`YYYYMM`) is read as the 1st of the month. Any other value is
112
+ * refused with an `EInvoiceParsingError`: no day is guessed, and none is read
113
+ * in the zone of the machine.
80
114
  */
81
115
  protected parseRequiredCIIDate(dateStr: string, format?: string): number {
82
116
  const value = dateStr.trim();
@@ -85,22 +119,17 @@ export abstract class BaseDecoder {
85
119
  format: 'cii',
86
120
  });
87
121
  }
122
+ const formatCode = (format ?? '').trim();
88
123
 
89
- if (format === '102') {
90
- const match = /^(\d{4})(\d{2})(\d{2})$/.exec(value);
91
- if (!match) {
92
- throw this.createInvalidDateError(value, 'CII format 102');
124
+ if (formatCode === '102' || formatCode === '203' || formatCode === '204') {
125
+ const day = parseCiiDateDay(value, formatCode);
126
+ if (day === undefined) {
127
+ throw this.createInvalidDateError(value, `CII format ${formatCode}`);
93
128
  }
94
- return this.createStrictUtcTimestamp(
95
- Number(match[1]),
96
- Number(match[2]),
97
- Number(match[3]),
98
- value,
99
- 'CII format 102',
100
- );
129
+ return day;
101
130
  }
102
131
 
103
- if (format === '610') {
132
+ if (formatCode === '610') {
104
133
  const match = /^(\d{4})(\d{2})$/.exec(value);
105
134
  if (!match) {
106
135
  throw this.createInvalidDateError(value, 'CII format 610');
@@ -114,48 +143,29 @@ export abstract class BaseDecoder {
114
143
  );
115
144
  }
116
145
 
117
- const parsed = Date.parse(value);
118
- if (Number.isNaN(parsed)) {
146
+ if (formatCode) {
147
+ throw this.createInvalidDateError(value, `CII format ${formatCode}`);
148
+ }
149
+
150
+ const day = parseIsoDateTimeDay(value);
151
+ if (day === undefined) {
119
152
  throw this.createInvalidDateError(value, 'CII');
120
153
  }
121
- return parsed;
154
+ return day;
122
155
  }
123
156
 
124
157
  /**
125
- * Parses a required UBL xsd:date value strictly and deterministically.
158
+ * Parses a required UBL xsd:date value strictly: the calendar day it states,
159
+ * as the UTC midnight of that day. A time zone does not move the day
160
+ * (`parseXsdDateDay`).
126
161
  */
127
162
  protected parseRequiredUblDate(dateStr: string): number {
128
163
  const value = dateStr.trim();
129
- const match =
130
- /^(\d{4})-(\d{2})-(\d{2})(?:(Z)|([+-])(\d{2}):(\d{2}))?$/.exec(value);
131
- if (!match) {
132
- throw this.createInvalidDateError(value, 'UBL xsd:date');
133
- }
134
-
135
- const timestamp = this.createStrictUtcTimestamp(
136
- Number(match[1]),
137
- Number(match[2]),
138
- Number(match[3]),
139
- value,
140
- 'UBL xsd:date',
141
- );
142
-
143
- if (!match[5]) {
144
- return timestamp;
145
- }
146
-
147
- const offsetHours = Number(match[6]);
148
- const offsetMinutes = Number(match[7]);
149
- if (
150
- offsetHours > 14 ||
151
- offsetMinutes > 59 ||
152
- (offsetHours === 14 && offsetMinutes !== 0)
153
- ) {
164
+ const day = parseXsdDateDay(value);
165
+ if (day === undefined) {
154
166
  throw this.createInvalidDateError(value, 'UBL xsd:date');
155
167
  }
156
-
157
- const direction = match[5] === '+' ? 1 : -1;
158
- return timestamp - direction * (offsetHours * 60 + offsetMinutes) * 60_000;
168
+ return day;
159
169
  }
160
170
 
161
171
  private createStrictUtcTimestamp(