@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
@@ -1,10 +1,22 @@
1
1
  import { BaseDecoder } from '../base/base.decoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
- import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
3
+ import { CII_NAMESPACES } from './cii.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
7
  import type { TPrecedingInvoiceReference } from '../utils/preceding.invoice.js';
8
+ import type { IEInvoiceMetadata } from '../../interfaces/en16931-metadata.js';
9
+ import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
10
+ import { readCiiStatedValues } from '../utils/stated.values.js';
11
+
12
+ /**
13
+ * The invoicing period (BG-14) a CII document states: its start date (BT-73),
14
+ * its end date (BT-74), or both (BR-CO-19), as the UTC timestamps of the days
15
+ */
16
+ export interface ICIIInvoicingPeriod {
17
+ start?: number;
18
+ end?: number;
19
+ }
8
20
 
9
21
  const ciiParser = new DOMParser();
10
22
  const ciiNamespaces = {
@@ -22,7 +34,6 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
22
34
  protected doc: Document;
23
35
  protected namespaces: Record<string, string>;
24
36
  protected select: xpath.XPathSelect;
25
- protected profile: CIIProfile = CIIProfile.EN16931;
26
37
 
27
38
  constructor(xml: string, skipValidation: boolean = false) {
28
39
  super(xml, skipValidation);
@@ -35,20 +46,35 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
35
46
 
36
47
  // Create XPath selector with namespaces
37
48
  this.select = ciiSelect;
38
-
39
- // Detect profile
40
- this.detectProfile();
41
49
  }
42
50
 
43
51
  public override getParsedDocument(): Document {
44
52
  return this.doc;
45
53
  }
46
54
 
55
+ /** The values the document states, read by `decode` */
56
+ private statedValues?: IEInvoiceStatedValues;
57
+
58
+ public override getStatedValues(): IEInvoiceStatedValues | undefined {
59
+ return this.statedValues;
60
+ }
61
+
62
+ /**
63
+ * Reads the values a CII (D16B) document states, by their EN 16931 business
64
+ * terms
65
+ */
66
+ protected readStatedValues(): IEInvoiceStatedValues | undefined {
67
+ return this.assertStatedValuesReadable(readCiiStatedValues(this.doc), 'cii');
68
+ }
69
+
47
70
  /**
48
71
  * Decodes CII XML into an accounting document
49
72
  * @returns Promise resolving to the accounting document
50
73
  */
51
74
  public async decode(): Promise<TAccountingDoc> {
75
+ // the values the document states, read first: an amount that is no number is refused
76
+ this.statedValues = this.readStatedValues();
77
+
52
78
  // CII has one root for every document; the document type code (BT-3) alone tells them apart
53
79
  const accountingDocType = getAccountingDocType(
54
80
  this.getText('/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:TypeCode'),
@@ -59,33 +85,6 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
59
85
  return this.decodeInvoice(accountingDocType);
60
86
  }
61
87
 
62
- /**
63
- * Detects the CII profile from the XML
64
- */
65
- protected detectProfile(): void {
66
- // Look for profile identifier
67
- const profileNode = this.select(
68
- 'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
69
- this.doc
70
- );
71
-
72
- if (profileNode) {
73
- const profileText = profileNode.toString();
74
-
75
- if (profileText.includes('BASIC')) {
76
- this.profile = CIIProfile.BASIC;
77
- } else if (profileText.includes('EN16931')) {
78
- this.profile = CIIProfile.EN16931;
79
- } else if (profileText.includes('EXTENDED')) {
80
- this.profile = CIIProfile.EXTENDED;
81
- } else if (profileText.includes('MINIMUM')) {
82
- this.profile = CIIProfile.MINIMUM;
83
- } else if (profileText.includes('COMFORT')) {
84
- this.profile = CIIProfile.COMFORT;
85
- }
86
- }
87
- }
88
-
89
88
  /**
90
89
  * Decodes a CII credit note
91
90
  * @returns Promise resolving to a TCreditNote object
@@ -186,6 +185,134 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
186
185
  return this.parseRequiredCIIDate(deliveryDate, this.getText(`${deliveryDatePath}/@format`).trim());
187
186
  }
188
187
 
188
+ /**
189
+ * Reads the invoicing period (BG-14) of the header settlement: the start date
190
+ * (BT-73), the end date (BT-74), or both (BR-CO-19). A line's own period
191
+ * (BG-26) is not the document's.
192
+ */
193
+ protected extractInvoicingPeriod(): ICIIInvoicingPeriod {
194
+ return this.readInvoicingPeriod(
195
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:BillingSpecifiedPeriod',
196
+ (path) => this.getText(path).trim(),
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Reads an invoicing period from its start and end date strings; each end is
202
+ * read when the document states it
203
+ * @param periodPath The path of the period element
204
+ * @param text The trimmed text a path selects, or ''
205
+ */
206
+ protected readInvoicingPeriod(periodPath: string, text: (path: string) => string): ICIIInvoicingPeriod {
207
+ const startPath = `${periodPath}/ram:StartDateTime/udt:DateTimeString`;
208
+ const endPath = `${periodPath}/ram:EndDateTime/udt:DateTimeString`;
209
+ const start = text(startPath);
210
+ const end = text(endPath);
211
+ return {
212
+ ...(start ? { start: this.parseRequiredCIIDate(start, text(`${startPath}/@format`)) } : {}),
213
+ ...(end ? { end: this.parseRequiredCIIDate(end, text(`${endPath}/@format`)) } : {}),
214
+ };
215
+ }
216
+
217
+ /**
218
+ * The accounting document fields of a stated invoicing period. With both
219
+ * dates it is `periodOfPerformance`. The envelope's period has both dates, so
220
+ * a period that states only its start or only its end keeps that date where
221
+ * the UBL decoder keeps it, in `metadata.extensions.dateInformation`
222
+ * (`periodStart` or `periodEnd`, the calendar day as YYYY-MM-DD); the CII and
223
+ * UBL encoders write it back from there.
224
+ * @param period The invoicing period the document states
225
+ */
226
+ protected invoicingPeriodFields(
227
+ period: ICIIInvoicingPeriod,
228
+ ): { periodOfPerformance: { from: number; to: number } } | { metadata: IEInvoiceMetadata } | Record<string, never> {
229
+ if (period.start !== undefined && period.end !== undefined) {
230
+ return { periodOfPerformance: { from: period.start, to: period.end } };
231
+ }
232
+ // the calendar day of the UTC timestamp the decoders return for a date
233
+ const dayOf = (timestamp: number) => new Date(timestamp).toISOString().slice(0, 10);
234
+ if (period.start !== undefined) {
235
+ return { metadata: { extensions: { dateInformation: { periodStart: dayOf(period.start) } } } };
236
+ }
237
+ if (period.end !== undefined) {
238
+ return { metadata: { extensions: { dateInformation: { periodEnd: dayOf(period.end) } } } };
239
+ }
240
+ return {};
241
+ }
242
+
243
+ /**
244
+ * Reads the seller electronic address (BT-34),
245
+ * `ram:URIUniversalCommunication/ram:URIID` of the seller, with its scheme;
246
+ * undefined when the document states none.
247
+ */
248
+ protected extractSellerElectronicAddress(): { scheme: string; value: string } | undefined {
249
+ const uriPath =
250
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:URIUniversalCommunication/ram:URIID';
251
+ const value = this.getText(uriPath).trim();
252
+ if (!value) {
253
+ return undefined;
254
+ }
255
+ return { scheme: this.getText(`${uriPath}/@schemeID`).trim(), value };
256
+ }
257
+
258
+ /**
259
+ * Reads the specification identifier (BT-24),
260
+ * `ram:GuidelineSpecifiedDocumentContextParameter/ram:ID` of the document
261
+ * context; undefined when the document states none.
262
+ */
263
+ protected extractSpecificationIdentifier(): string | undefined {
264
+ return (
265
+ this.getText('/rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID').trim() ||
266
+ undefined
267
+ );
268
+ }
269
+
270
+ /**
271
+ * The accounting document's `metadata`: the specification identifier (BT-24)
272
+ * as `customizationId`, which the validators read to tell an XRechnung, a
273
+ * PEPPOL or a Factur-X document, merged with the metadata of the invoicing
274
+ * period fields (`invoicingPeriodFields`), which it takes the place of in
275
+ * the decoded document.
276
+ * @param invoicingPeriod The invoicing period fields of the document
277
+ */
278
+ protected metadataFields(
279
+ invoicingPeriod: ReturnType<CIIBaseDecoder['invoicingPeriodFields']>,
280
+ ): { metadata: IEInvoiceMetadata } | Record<string, never> {
281
+ const customizationId = this.extractSpecificationIdentifier();
282
+ const periodMetadata = 'metadata' in invoicingPeriod ? invoicingPeriod.metadata : undefined;
283
+ if (customizationId === undefined && periodMetadata === undefined) {
284
+ return {};
285
+ }
286
+ return { metadata: { ...periodMetadata, ...(customizationId === undefined ? {} : { customizationId }) } };
287
+ }
288
+
289
+ /**
290
+ * Reads the party identifiers (BT-29 seller, BT-46 buyer) of a trade party:
291
+ * `ram:ID` without a scheme and `ram:GlobalID` with its scheme, kept where the
292
+ * UBL decoder keeps them, as the party's `additionalIdentifiers`
293
+ * @param partyXPath XPath to the trade party
294
+ */
295
+ protected extractPartyIdentifiers(partyXPath: string): Array<{ value: string; scheme: string }> {
296
+ const nodes = this.select(`${partyXPath}/ram:ID | ${partyXPath}/ram:GlobalID`, this.doc);
297
+ return (Array.isArray(nodes) ? nodes : [])
298
+ .map((node) => {
299
+ const element = node as Element;
300
+ return { value: (element.textContent ?? '').trim(), scheme: element.localName === 'GlobalID' ? (element.getAttribute('schemeID') ?? '').trim() : '' };
301
+ })
302
+ .filter((identifier) => identifier.value !== '');
303
+ }
304
+
305
+ /**
306
+ * Reads the buyer reference (BT-10), `ram:BuyerReference` of the header trade
307
+ * agreement; undefined when the document states none.
308
+ */
309
+ protected extractBuyerReference(): string | undefined {
310
+ return (
311
+ this.getText('/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerReference').trim() ||
312
+ undefined
313
+ );
314
+ }
315
+
189
316
  /**
190
317
  * Gets a text value from an XPath expression
191
318
  * @param xpath XPath expression
@@ -2,11 +2,43 @@ import { BaseEncoder } from '../base/base.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
3
  import { EInvoiceFormatError } from '../../errors.js';
4
4
  import { getPrecedingInvoiceReferences } from '../utils/preceding.invoice.js';
5
- import { getWritableDate } from '../utils/date.value.js';
5
+ import { getWritableDate, parseXsdDateDay } from '../utils/date.value.js';
6
6
  import { assertVatCategoryWritable } from '../utils/vat.category.js';
7
+ import { assertSellerIdentified } from '../utils/seller.identifier.js';
8
+ import { assertPartyIdentifiersWritable, getPartyIdentifiers } from '../utils/party.identifier.js';
7
9
  import { computeDocumentTotals } from '../utils/document.totals.js';
8
10
  import { getPaymentTotals } from '../utils/paid.amount.js';
9
- import { CII_CHILD_SEQUENCES, CII_NAMESPACES, CIIProfile } from './cii.types.js';
11
+ import { CII_CHILD_SEQUENCES, CII_NAMESPACES, CII_PROFILE_IDS, CIIProfile } from './cii.types.js';
12
+ import type { IEInvoiceMetadata } from '../../interfaces/en16931-metadata.js';
13
+
14
+ /**
15
+ * Why a CII profile is not written. The CII encoders write the full EN 16931
16
+ * content, so the one profile they state is EN 16931 (`urn:cen.eu:en16931:2017`,
17
+ * named COMFORT in ZUGFeRD 2.x); stating another profile for that content would
18
+ * make a document that does not validate against the profile it names.
19
+ */
20
+ const REFUSED_PROFILES: Partial<Record<CIIProfile, string>> = {
21
+ [CIIProfile.MINIMUM]:
22
+ 'the content written is EN 16931, which the MINIMUM profile does not allow, and a MINIMUM document is not an e-invoice (BMF letter of 15 October 2024, Rn. 25 and 30)',
23
+ [CIIProfile.BASIC]:
24
+ 'the content written is EN 16931, which neither the BASIC nor the BASIC WL profile allows, and a BASIC WL document is not an e-invoice (BMF letter of 15 October 2024, Rn. 25 and 30)',
25
+ [CIIProfile.EXTENDED]:
26
+ 'the output has not been checked against the EXTENDED schema and rules',
27
+ };
28
+
29
+ /**
30
+ * Refuses a profile the CII encoders do not write, naming the profile and why
31
+ * @param profile CII profile
32
+ */
33
+ const assertWritableProfile = (profile: CIIProfile): void => {
34
+ const reason = REFUSED_PROFILES[profile];
35
+ if (reason !== undefined) {
36
+ throw new EInvoiceFormatError(
37
+ `The CII encoders do not write the ${profile} profile: ${reason}; choose ${CIIProfile.EN16931}, the profile whose content they write`,
38
+ { targetFormat: 'cii', unsupportedFeatures: [`profile ${profile}`] },
39
+ );
40
+ }
41
+ };
10
42
 
11
43
  /**
12
44
  * Base encoder for CII-based invoice formats
@@ -15,13 +47,26 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
15
47
  protected profile: CIIProfile = CIIProfile.EN16931;
16
48
 
17
49
  /**
18
- * Sets the CII profile to use for encoding
50
+ * Sets the CII profile to use for encoding. EN16931 and its ZUGFeRD name
51
+ * COMFORT are written; MINIMUM, BASIC (and with it BASIC WL) and EXTENDED are
52
+ * refused with an `EInvoiceFormatError`.
19
53
  * @param profile CII profile
20
54
  */
21
55
  public setProfile(profile: CIIProfile): void {
56
+ assertWritableProfile(profile);
22
57
  this.profile = profile;
23
58
  }
24
59
 
60
+ /**
61
+ * The specification identifier (BT-24) the document states: the EN 16931
62
+ * profile, the one profile whose content the encoders write. A refused
63
+ * profile a subclass assigned directly is refused here too.
64
+ */
65
+ protected getProfileId(): string {
66
+ assertWritableProfile(this.profile);
67
+ return CII_PROFILE_IDS.EN16931;
68
+ }
69
+
25
70
  /**
26
71
  * Encodes an accounting document into CII XML
27
72
  * @param invoice Accounting document to encode
@@ -30,20 +75,15 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
30
75
  public async encode(invoice: TAccountingDoc): Promise<string> {
31
76
  // a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
32
77
  assertVatCategoryWritable(invoice, 'cii');
78
+ // a buyer identifier with the SEPA scheme is refused (BR-CL-10)
79
+ assertPartyIdentifiersWritable(invoice, 'cii');
33
80
  // a paid amount the advance payments do not add up to, or with more decimals than the currency has, is refused
34
81
  getPaymentTotals(invoice, computeDocumentTotals(invoice));
35
- // MINIMUM has no preceding invoice reference (BG-3): a reference the document states cannot be written there
36
- if (this.profile === CIIProfile.MINIMUM && getPrecedingInvoiceReferences(invoice).length > 0) {
37
- throw new EInvoiceFormatError(
38
- 'The MINIMUM profile has no preceding invoice reference (BG-3); a document that refers to a preceding invoice needs a profile that carries it',
39
- { targetFormat: 'cii', unsupportedFeatures: ['BG-3 in profile MINIMUM'] },
40
- );
41
- }
42
82
  // CII has one root for every document; the two paths differ in the type code they write
43
- if (invoice.accountingDocType === 'creditnote') {
44
- return this.encodeCreditNote(invoice);
45
- }
46
- return this.encodeInvoice(invoice);
83
+ const xml = invoice.accountingDocType === 'creditnote' ? await this.encodeCreditNote(invoice) : await this.encodeInvoice(invoice);
84
+ // a seller the written document does not identify is refused (BR-CO-26), never written silently
85
+ assertSellerIdentified(xml, 'cii', 'cii');
86
+ return xml;
47
87
  }
48
88
 
49
89
  /**
@@ -165,4 +205,156 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
165
205
  protected formatDate(timestamp: number, field: string): string {
166
206
  return getWritableDate(timestamp, field, 'cii').toISOString().split('T')[0];
167
207
  }
208
+
209
+ /**
210
+ * Formats a date as YYYYMMDD, the CII date format 102. A value that is no
211
+ * valid timestamp is refused with an `EInvoiceFormatError` naming the field.
212
+ * @param timestamp Timestamp to format
213
+ * @param field The business term the date is written as
214
+ * @returns Formatted date string
215
+ */
216
+ protected formatDateYYYYMMDD(timestamp: number, field: string): string {
217
+ const date = getWritableDate(timestamp, field, 'cii');
218
+ // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
219
+ const year = date.getUTCFullYear();
220
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
221
+ const day = date.getUTCDate().toString().padStart(2, '0');
222
+ return `${year}${month}${day}`;
223
+ }
224
+
225
+ /**
226
+ * Adds the seller electronic address (BT-34), `electronicAddress`, as
227
+ * `ram:URIUniversalCommunication/ram:URIID` of the seller, with its scheme
228
+ * (BR-62). EN 16931 asks nothing of the seller for it, so a person states one
229
+ * as a company does.
230
+ * @param doc XML document
231
+ * @param sellerElement The seller trade party
232
+ * @param invoice Invoice data
233
+ */
234
+ protected addSellerElectronicAddress(doc: Document, sellerElement: Element, invoice: TAccountingDoc): void {
235
+ if (!invoice.electronicAddress) {
236
+ return;
237
+ }
238
+ const communicationElement = doc.createElement('ram:URIUniversalCommunication');
239
+ const uriElement = doc.createElement('ram:URIID');
240
+ uriElement.setAttribute('schemeID', invoice.electronicAddress.scheme);
241
+ uriElement.textContent = invoice.electronicAddress.value;
242
+ communicationElement.appendChild(uriElement);
243
+ sellerElement.appendChild(communicationElement);
244
+ }
245
+
246
+ /**
247
+ * Adds the party identifiers (BT-29 seller, BT-46 buyer) the party carries:
248
+ * one with a scheme as `ram:GlobalID` with its scheme (BT-29-1, an ISO/IEC
249
+ * 6523 ICD code), one without as `ram:ID`, first in the trade party as the
250
+ * CII schema orders it. A SEPA creditor identifier, which UBL states as a
251
+ * seller identification with the scheme SEPA, is the bank assigned creditor
252
+ * identifier (BT-90) and no party identifier in CII; it is not written here.
253
+ * @param doc XML document
254
+ * @param partyElement The seller or buyer trade party
255
+ * @param party The seller or the buyer
256
+ */
257
+ protected addPartyIdentifiers(doc: Document, partyElement: Element, party: unknown): void {
258
+ const identifiers = getPartyIdentifiers(party).filter((identifier) => identifier.scheme?.toUpperCase() !== 'SEPA');
259
+ for (const identifier of identifiers.reverse()) {
260
+ const idElement = doc.createElement(identifier.scheme ? 'ram:GlobalID' : 'ram:ID');
261
+ if (identifier.scheme) {
262
+ idElement.setAttribute('schemeID', identifier.scheme);
263
+ }
264
+ idElement.textContent = identifier.value;
265
+ partyElement.insertBefore(idElement, partyElement.firstChild);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Adds the buyer reference (BT-10) as `ram:BuyerReference` of the header
271
+ * trade agreement, its first child in the CII schema; nothing when the
272
+ * document has none.
273
+ * @param doc XML document
274
+ * @param agreementElement The header trade agreement
275
+ * @param invoice Invoice data
276
+ */
277
+ protected addBuyerReference(doc: Document, agreementElement: Element, invoice: TAccountingDoc): void {
278
+ const buyerReference = invoice.buyerReference?.trim();
279
+ if (!buyerReference) {
280
+ return;
281
+ }
282
+ const buyerReferenceElement = doc.createElement('ram:BuyerReference');
283
+ buyerReferenceElement.textContent = buyerReference;
284
+ agreementElement.appendChild(buyerReferenceElement);
285
+ }
286
+
287
+ /**
288
+ * Adds the invoicing period (BG-14) as `ram:BillingSpecifiedPeriod` of the
289
+ * header settlement, where the CII schema puts it: the start date (BT-73) and
290
+ * the end date (BT-74) of `periodOfPerformance`. Without it, the date or
291
+ * dates a decoded document kept in `metadata.extensions.dateInformation`
292
+ * (`periodStart`, `periodEnd`) are written, as the UBL encoders write them:
293
+ * a period may state only one of its dates (BR-CO-19).
294
+ * @param doc XML document
295
+ * @param settlementElement The header trade settlement
296
+ * @param invoice Invoice data
297
+ */
298
+ protected addInvoicingPeriod(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
299
+ const { start, end } = this.getInvoicingPeriodDays(invoice);
300
+ if (start === undefined && end === undefined) {
301
+ return;
302
+ }
303
+ const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
304
+ const appendDate = (name: string, day: string) => {
305
+ const dateElement = doc.createElement(name);
306
+ const dateStringElement = doc.createElement('udt:DateTimeString');
307
+ dateStringElement.setAttribute('format', '102');
308
+ dateStringElement.textContent = day;
309
+ dateElement.appendChild(dateStringElement);
310
+ periodElement.appendChild(dateElement);
311
+ };
312
+ if (start !== undefined) {
313
+ appendDate('ram:StartDateTime', start);
314
+ }
315
+ if (end !== undefined) {
316
+ appendDate('ram:EndDateTime', end);
317
+ }
318
+ settlementElement.appendChild(periodElement);
319
+ }
320
+
321
+ /**
322
+ * The start (BT-73) and end (BT-74) dates of the invoicing period, in CII
323
+ * format 102: from `periodOfPerformance`, or else from the dates kept in
324
+ * `metadata.extensions.dateInformation`. A kept date is an `xsd:date`, as
325
+ * the UBL decoder keeps it and the CII decoders write it (YYYY-MM-DD); a time
326
+ * zone does not move the day it names (`parseXsdDateDay`), so the stated day
327
+ * is written. '' is no date, as the UBL decoder writes it. Any other value is
328
+ * refused with an `EInvoiceFormatError` naming the date.
329
+ * @param invoice Invoice data
330
+ */
331
+ private getInvoicingPeriodDays(invoice: TAccountingDoc): { start?: string; end?: string } {
332
+ if (invoice.periodOfPerformance) {
333
+ return {
334
+ start: this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date'),
335
+ end: this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date'),
336
+ };
337
+ }
338
+ const dateInformation: unknown = (invoice as TAccountingDoc & { metadata?: IEInvoiceMetadata }).metadata?.extensions?.dateInformation;
339
+ if (typeof dateInformation !== 'object' || dateInformation === null) {
340
+ return {};
341
+ }
342
+ const keptDay = (key: 'periodStart' | 'periodEnd', field: string): string | undefined => {
343
+ const value: unknown = (dateInformation as Record<string, unknown>)[key];
344
+ if (value === undefined || value === '') {
345
+ return undefined;
346
+ }
347
+ const day = typeof value === 'string' ? parseXsdDateDay(value) : undefined;
348
+ if (day === undefined) {
349
+ throw new EInvoiceFormatError(`${field} is no valid date: ${String(value)}`, {
350
+ targetFormat: 'cii',
351
+ unsupportedFeatures: [field],
352
+ });
353
+ }
354
+ return this.formatDateYYYYMMDD(day, field);
355
+ };
356
+ const start = keptDay('periodStart', 'BT-73 invoicing period start date');
357
+ const end = keptDay('periodEnd', 'BT-74 invoicing period end date');
358
+ return { ...(start === undefined ? {} : { start }), ...(end === undefined ? {} : { end }) };
359
+ }
168
360
  }
@@ -26,17 +26,15 @@ export enum CIIProfile {
26
26
  MINIMUM = 'MINIMUM'
27
27
  }
28
28
 
29
- // CII profile IDs for different formats
29
+ // CII profile IDs (BT-24, the specification identifier)
30
30
  export const CII_PROFILE_IDS = {
31
- // Factur-X profiles
32
- FACTURX_MINIMUM: 'urn:factur-x.eu:1p0:minimum',
33
- FACTURX_BASIC: 'urn:factur-x.eu:1p0:basicwl',
34
- FACTURX_EN16931: 'urn:cen.eu:en16931:2017',
31
+ // the EN 16931 profile of Factur-X 1.0 and ZUGFeRD 2.x (named COMFORT in ZUGFeRD): one standard, one
32
+ // identifier, and the only profile the CII encoders write
33
+ EN16931: 'urn:cen.eu:en16931:2017',
35
34
 
36
- // ZUGFeRD v2 profiles
37
- ZUGFERD_BASIC: 'urn:zugferd:basic',
38
- ZUGFERD_COMFORT: 'urn:zugferd:comfort',
39
- ZUGFERD_EXTENDED: 'urn:zugferd:extended',
35
+ // Factur-X and ZUGFeRD 2.1 subset profiles, recognised on decode
36
+ FACTURX_MINIMUM: 'urn:factur-x.eu:1p0:minimum',
37
+ FACTURX_BASIC_WL: 'urn:factur-x.eu:1p0:basicwl',
40
38
 
41
39
  // ZUGFeRD v1 profiles
42
40
  ZUGFERD_V1_BASIC: 'urn:ferd:CrossIndustryDocument:invoice:1p0:basic',
@@ -1,9 +1,11 @@
1
1
  import { BaseValidator } from '../base/base.validator.js';
2
2
  import { ValidationLevel } from '../../interfaces/common.js';
3
3
  import type { ValidationResult } from '../../interfaces/common.js';
4
- import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
4
+ import { CII_NAMESPACES } from './cii.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, ciiSellerMeetsBrCo26 } from '../utils/seller.identifier.js';
8
+ import { findStatedValueDisagreements, readCiiStatedValues } from '../utils/stated.values.js';
7
9
 
8
10
  const ciiValidatorParser = new DOMParser();
9
11
  const ciiValidatorNamespaces = {
@@ -20,7 +22,6 @@ export abstract class CIIBaseValidator extends BaseValidator {
20
22
  protected doc!: Document;
21
23
  protected namespaces!: Record<string, string>;
22
24
  protected select!: xpath.XPathSelect;
23
- protected profile: CIIProfile = CIIProfile.EN16931;
24
25
 
25
26
  constructor(xml: string, doc?: Document) {
26
27
  super(xml);
@@ -34,9 +35,6 @@ export abstract class CIIBaseValidator extends BaseValidator {
34
35
 
35
36
  // Create XPath selector with namespaces
36
37
  this.select = ciiValidatorSelect;
37
-
38
- // Detect profile
39
- this.detectProfile();
40
38
  } catch (error) {
41
39
  this.addError('CII-PARSE', `Failed to parse XML: ${error}`, '/');
42
40
  }
@@ -114,6 +112,33 @@ export abstract class CIIBaseValidator extends BaseValidator {
114
112
  */
115
113
  protected abstract validateStructure(): boolean;
116
114
 
115
+ /**
116
+ * BR-CO-10 to BR-CO-17: the document totals, the VAT breakdown, the line
117
+ * amounts and the document level allowances and charges the document states
118
+ * agree with each other, as the CEN artefacts check them
119
+ * @returns True if they do
120
+ */
121
+ protected validateStatedTotals(): boolean {
122
+ const findings = findStatedValueDisagreements(readCiiStatedValues(this.doc), 'cii');
123
+ for (const finding of findings) {
124
+ this.addError(finding.ruleId, finding.message, finding.location);
125
+ }
126
+ return findings.length === 0;
127
+ }
128
+
129
+ /**
130
+ * BR-CO-26: the seller states a seller identifier (BT-29), a legal
131
+ * registration identifier (BT-30) or a VAT identifier (BT-31).
132
+ * @returns True if it does
133
+ */
134
+ protected validateSellerIdentifier(): boolean {
135
+ if (ciiSellerMeetsBrCo26(this.doc)) {
136
+ return true;
137
+ }
138
+ this.addError('BR-CO-26', BR_CO_26_TEXT, '//ram:SellerTradeParty');
139
+ return false;
140
+ }
141
+
117
142
  /**
118
143
  * BR-23: every invoice line has a unit code on its billed quantity.
119
144
  * @returns True if every line has one
@@ -160,33 +185,6 @@ export abstract class CIIBaseValidator extends BaseValidator {
160
185
  return valid;
161
186
  }
162
187
 
163
- /**
164
- * Detects the CII profile from the XML
165
- */
166
- protected detectProfile(): void {
167
- // Look for profile identifier
168
- const profileNode = this.select(
169
- 'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
170
- this.doc
171
- );
172
-
173
- if (profileNode) {
174
- const profileText = profileNode.toString();
175
-
176
- if (profileText.includes('BASIC')) {
177
- this.profile = CIIProfile.BASIC;
178
- } else if (profileText.includes('EN16931')) {
179
- this.profile = CIIProfile.EN16931;
180
- } else if (profileText.includes('EXTENDED')) {
181
- this.profile = CIIProfile.EXTENDED;
182
- } else if (profileText.includes('MINIMUM')) {
183
- this.profile = CIIProfile.MINIMUM;
184
- } else if (profileText.includes('COMFORT')) {
185
- this.profile = CIIProfile.COMFORT;
186
- }
187
- }
188
- }
189
-
190
188
  /**
191
189
  * Gets a text value from an XPath expression
192
190
  * @param xpath XPath expression
@@ -5,7 +5,6 @@ import type {
5
5
  TCreditNote,
6
6
  TInvoiceDocument,
7
7
  } from '../../../interfaces/common.js';
8
- import { FACTURX_PROFILE_IDS } from './facturx.types.js';
9
8
  import { business, finance, general } from '../../../plugins.js';
10
9
  import { EN16931Validator } from '../../validation/en16931.validator.js';
11
10
  import { getPrecedingInvoiceFields } from '../../utils/preceding.invoice.js';
@@ -98,6 +97,12 @@ export class FacturXDecoder extends CIIBaseDecoder {
98
97
  // Reverse charge: every line has the VAT category AE
99
98
  const reverseCharge = this.isReverseChargeDocument();
100
99
 
100
+ // the invoicing period (BG-14) and the seller electronic address (BT-34), when stated
101
+ const invoicingPeriod = this.invoicingPeriodFields(this.extractInvoicingPeriod());
102
+ // the buyer reference (BT-10), when stated
103
+ const buyerReference = this.extractBuyerReference();
104
+ const electronicAddress = this.extractSellerElectronicAddress();
105
+
101
106
  const paidAmount = readPaidAmount(this.getText('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount'), 'facturx');
102
107
 
103
108
  // Create the common invoice data
@@ -126,6 +131,11 @@ export class FacturXDecoder extends CIIBaseDecoder {
126
131
  notes: notes,
127
132
  // the day of supply as the document states it (BT-72)
128
133
  ...(deliveryDate === undefined ? {} : { deliveryDate }),
134
+ ...invoicingPeriod,
135
+ // the specification identifier (BT-24), with the metadata of a one-date period
136
+ ...this.metadataFields(invoicingPeriod),
137
+ ...(buyerReference === undefined ? {} : { buyerReference }),
138
+ ...(electronicAddress === undefined ? {} : { electronicAddress }),
129
139
  objectActions: [],
130
140
  ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
131
141
  // the paid amount (BT-113); the amount due (BT-115) follows from it and the totals
@@ -168,6 +178,9 @@ export class FacturXDecoder extends CIIBaseDecoder {
168
178
  // the tax registration identifier (BT-32 for the seller), the Steuernummer: scheme FC
169
179
  const taxNumber = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
170
180
 
181
+ // the party identifiers (BT-29 seller, BT-46 buyer)
182
+ const additionalIdentifiers = this.extractPartyIdentifiers(partyXPath);
183
+
171
184
  // Create contact object
172
185
  return {
173
186
  type: 'company',
@@ -181,7 +194,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
181
194
  registrationId: registrationId,
182
195
  registrationName: '',
183
196
  ...(taxNumber ? { taxNumber } : {})
184
- }
197
+ },
198
+ ...(additionalIdentifiers.length > 0 ? { additionalIdentifiers } : {})
185
199
  } as business.TContact;
186
200
  }
187
201