@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
@@ -50,3 +50,109 @@ export const getWritableDueDate = (issueTimestamp: unknown, dueInDays: unknown,
50
50
  dueDate.setUTCDate(dueDate.getUTCDate() + dueInDays);
51
51
  return dueDate;
52
52
  };
53
+
54
+ /**
55
+ * The UTC midnight of a calendar day, or undefined when there is no such day
56
+ * @param year The year, 1 or later
57
+ * @param month The month, 1 to 12
58
+ * @param day The day of the month
59
+ */
60
+ const utcMidnightOf = (year: number, month: number, day: number): number | undefined => {
61
+ if (year < 1) {
62
+ return undefined;
63
+ }
64
+ const date = new Date(0);
65
+ date.setUTCFullYear(year, month - 1, day);
66
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
67
+ return undefined;
68
+ }
69
+ return date.getTime();
70
+ };
71
+
72
+ /**
73
+ * Whether a time of day is one: hours 0 to 23, minutes and seconds 0 to 59
74
+ * @param hours The hours
75
+ * @param minutes The minutes
76
+ * @param seconds The seconds, 0 when not stated
77
+ */
78
+ const isTimeOfDay = (hours: number, minutes: number, seconds: number): boolean =>
79
+ hours <= 23 && minutes <= 59 && seconds <= 59;
80
+
81
+ /**
82
+ * Whether a time zone offset is one an `xsd:date` or `xsd:dateTime` may carry:
83
+ * at most 14:00
84
+ * @param hours The hours of the offset
85
+ * @param minutes The minutes of the offset
86
+ */
87
+ const isZoneOffset = (hours: number, minutes: number): boolean =>
88
+ hours < 14 ? minutes <= 59 : hours === 14 && minutes === 0;
89
+
90
+ /**
91
+ * The calendar day a UBL `xsd:date` names (BT-2, BT-9, BT-26, BT-72, BT-73,
92
+ * BT-74), as the UTC midnight of that day; undefined when the value is no such
93
+ * date. The value is `YYYY-MM-DD`, optionally followed by a time zone: `Z`, or
94
+ * `+hh:mm` / `-hh:mm` of at most 14:00. The zone says where the day is; it
95
+ * does not name another day, so `2026-08-01+02:00` and `2026-08-01-12:00` both
96
+ * name 1 August 2026. EN 16931 dates are calendar days without a zone, and CII
97
+ * format 102 has none, so the day is what every format reads and writes.
98
+ * @param value The date as the document states it
99
+ */
100
+ export const parseXsdDateDay = (value: string): number | undefined => {
101
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(value.trim());
102
+ if (!match) {
103
+ return undefined;
104
+ }
105
+ if (match[4] !== undefined && !isZoneOffset(Number(match[4]), Number(match[5]))) {
106
+ return undefined;
107
+ }
108
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
109
+ };
110
+
111
+ /**
112
+ * The calendar day an ISO 8601 date, or date and time, names, as the UTC
113
+ * midnight of that day; undefined when the value is no such date. A CII date
114
+ * without a format code may be stated so. The value is `YYYY-MM-DD`,
115
+ * optionally followed by a time of day `Thh:mm`, `Thh:mm:ss` or
116
+ * `Thh:mm:ss.fff`, and then optionally by a time zone: `Z`, or `+hh:mm` /
117
+ * `-hh:mm` of at most 14:00. Neither the time of day nor the zone moves the
118
+ * day the document states: `2026-09-11T00:30:00+02:00` is 22:30 UTC on
119
+ * 10 September, but the document states 11 September, and that is the day an
120
+ * invoice is dated with (§ 14 Abs. 4 Satz 1 Nr. 3 UStG asks for the issue date,
121
+ * a calendar day).
122
+ * @param value The date as the document states it
123
+ */
124
+ export const parseIsoDateTimeDay = (value: string): number | undefined => {
125
+ const match =
126
+ /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(value.trim());
127
+ if (!match) {
128
+ return undefined;
129
+ }
130
+ if (match[4] !== undefined && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
131
+ return undefined;
132
+ }
133
+ if (match[7] !== undefined && !isZoneOffset(Number(match[7]), Number(match[8]))) {
134
+ return undefined;
135
+ }
136
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
137
+ };
138
+
139
+ /**
140
+ * The calendar day a CII date in format 102 (`YYYYMMDD`), 203
141
+ * (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`) names, as the UTC midnight of that
142
+ * day; undefined when the value is no such date in that format. A time of day
143
+ * in formats 203 and 204 does not move the day stated; the formats carry no
144
+ * time zone.
145
+ * @param value The date as the document states it
146
+ * @param format The format code: '102', '203' or '204'
147
+ */
148
+ export const parseCiiDateDay = (value: string, format: '102' | '203' | '204'): number | undefined => {
149
+ const timeDigits = { '102': '', '203': '(\\d{2})(\\d{2})', '204': '(\\d{2})(\\d{2})(\\d{2})' }[format];
150
+ const match = new RegExp(`^(\\d{4})(\\d{2})(\\d{2})${timeDigits}$`).exec(value.trim());
151
+ if (!match) {
152
+ return undefined;
153
+ }
154
+ if (format !== '102' && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
155
+ return undefined;
156
+ }
157
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
158
+ };
@@ -97,12 +97,19 @@ export class FormatDetector {
97
97
  if (/xrechnung/i.test(guidelineId)) {
98
98
  return InvoiceFormat.XRECHNUNG;
99
99
  }
100
- if (/factur-x/i.test(guidelineId) || /urn:cen\.eu:en16931:2017/i.test(guidelineId)) {
100
+ // an identifier that names Factur-X or ZUGFeRD decides; ZUGFeRD 2.0 BASIC and EXTENDED name ZUGFeRD
101
+ // after the EN 16931 identifier they extend (urn:cen.eu:en16931:2017#...#urn:zugferd.de:2p0:...)
102
+ if (/factur-x/i.test(guidelineId)) {
101
103
  return InvoiceFormat.FACTURX;
102
104
  }
103
- if (/zugferd/i.test(guidelineId) || /urn:ferd:/i.test(guidelineId) || /urn:zugferd/i.test(guidelineId)) {
105
+ if (/zugferd/i.test(guidelineId) || /urn:ferd:/i.test(guidelineId)) {
104
106
  return InvoiceFormat.ZUGFERD;
105
107
  }
108
+ // the EN 16931 profile identifier is shared by Factur-X 1.0 and ZUGFeRD 2.x, one standard with one
109
+ // XML: an XML-only document of it reads as Factur-X
110
+ if (/urn:cen\.eu:en16931:2017/i.test(guidelineId)) {
111
+ return InvoiceFormat.FACTURX;
112
+ }
106
113
  return InvoiceFormat.CII;
107
114
  }
108
115
 
@@ -287,13 +294,11 @@ export class FormatDetector {
287
294
  for (const idNode of Array.from(idNodes)) {
288
295
  const profileText = idNode.textContent || '';
289
296
 
290
- // Check for ZUGFeRD profiles (v1 and v2)
297
+ // Check for ZUGFeRD profiles: v1, the ZUGFeRD 2.0 identifiers (urn:zugferd.de:2p0:...) and the
298
+ // urn:zugferd:... identifiers earlier releases of this library wrote
291
299
  if (
292
300
  profileText.includes('zugferd') ||
293
301
  profileText.includes('urn:ferd:') ||
294
- profileText === CII_PROFILE_IDS.ZUGFERD_BASIC ||
295
- profileText === CII_PROFILE_IDS.ZUGFERD_COMFORT ||
296
- profileText === CII_PROFILE_IDS.ZUGFERD_EXTENDED ||
297
302
  profileText === CII_PROFILE_IDS.ZUGFERD_V1_BASIC ||
298
303
  profileText === CII_PROFILE_IDS.ZUGFERD_V1_COMFORT ||
299
304
  profileText === CII_PROFILE_IDS.ZUGFERD_V1_EXTENDED
@@ -301,12 +306,13 @@ export class FormatDetector {
301
306
  return InvoiceFormat.ZUGFERD;
302
307
  }
303
308
 
304
- // Check for Factur-X profiles
309
+ // Check for Factur-X profiles; the EN 16931 profile of ZUGFeRD 2.x has the same identifier, and
310
+ // an XML-only document of it is Factur-X: ZUGFeRD 2.x and Factur-X 1.0 are one standard
305
311
  if (
306
312
  profileText.includes('factur-x') ||
307
313
  profileText === CII_PROFILE_IDS.FACTURX_MINIMUM ||
308
- profileText === CII_PROFILE_IDS.FACTURX_BASIC ||
309
- profileText === CII_PROFILE_IDS.FACTURX_EN16931
314
+ profileText === CII_PROFILE_IDS.FACTURX_BASIC_WL ||
315
+ profileText === CII_PROFILE_IDS.EN16931
310
316
  ) {
311
317
  return InvoiceFormat.FACTURX;
312
318
  }
@@ -0,0 +1,30 @@
1
+ import type { business } from '@tsclass/tsclass';
2
+
3
+ /** The contact of a party: the seller contact (BG-6) or the buyer contact (BG-9) */
4
+ export interface IPartyContact {
5
+ /** Contact point (BT-41, BT-56) */
6
+ name?: string;
7
+ /** Telephone number (BT-42, BT-57) */
8
+ phone?: string;
9
+ /** Email address (BT-43, BT-58) */
10
+ email?: string;
11
+ }
12
+
13
+ /** A party with the contact metadata the decoders read back (`metadata.contactInformation`) */
14
+ type TPartyWithContactMetadata = business.TContact & { metadata?: { contactInformation?: IPartyContact } };
15
+
16
+ /**
17
+ * The contact a party states, as the encoders write it: the contact metadata first, otherwise
18
+ * the party's own name, telephone number and email address; none when the party states neither.
19
+ * @param party The seller or the buyer
20
+ */
21
+ export const getPartyContact = (party: business.TContact | undefined): IPartyContact | undefined => {
22
+ const explicit = (party as TPartyWithContactMetadata | undefined)?.metadata?.contactInformation;
23
+ if (explicit && (explicit.name || explicit.phone || explicit.email)) {
24
+ return explicit;
25
+ }
26
+ if (party?.email || party?.phone) {
27
+ return { name: party.name, phone: party.phone, email: party.email };
28
+ }
29
+ return undefined;
30
+ };
@@ -0,0 +1,61 @@
1
+ import type { TAccountingDoc } from '../../interfaces/common.js';
2
+ import { EInvoiceFormatError } from '../../errors.js';
3
+
4
+ /** A party identifier: the seller identifier (BT-29) or the buyer identifier (BT-46), with its scheme */
5
+ export interface IPartyIdentifier {
6
+ value: string;
7
+ /** The identification scheme (BT-29-1, BT-46-1): an ISO/IEC 6523 ICD code, or SEPA for a creditor identifier */
8
+ scheme?: string;
9
+ }
10
+
11
+ /**
12
+ * The party identifiers (BT-29 seller, BT-46 buyer) a party carries in
13
+ * `additionalIdentifiers`, where the decoders keep the ones a document states;
14
+ * the envelope has no typed field for them. Entries without a value are
15
+ * skipped, a blank scheme is none.
16
+ * @param party The seller or the buyer
17
+ */
18
+ export const getPartyIdentifiers = (party: unknown): IPartyIdentifier[] => {
19
+ const entries: unknown = (party as { additionalIdentifiers?: unknown } | undefined)?.additionalIdentifiers;
20
+ if (!Array.isArray(entries)) {
21
+ return [];
22
+ }
23
+ const identifiers: IPartyIdentifier[] = [];
24
+ for (const entry of entries) {
25
+ const { value, scheme } = (entry ?? {}) as { value?: unknown; scheme?: unknown };
26
+ if (typeof value !== 'string' || !value.trim()) {
27
+ continue;
28
+ }
29
+ const trimmedScheme = typeof scheme === 'string' ? scheme.trim() : '';
30
+ identifiers.push({ value: value.trim(), ...(trimmedScheme ? { scheme: trimmedScheme } : {}) });
31
+ }
32
+ return identifiers;
33
+ };
34
+
35
+ /**
36
+ * Refuses a document whose buyer carries an identifier (BT-46) with the scheme
37
+ * SEPA, naming the rule. The SEPA scheme is the creditor identifier (BT-90) of
38
+ * the seller or the payee. The CEN artefacts 1.3.16 report it on a buyer as
39
+ * BR-CL-10, "Any identifier identification scheme identifier MUST be coded
40
+ * using one of the ISO 6523 ICD list.": UBL admits it in a party identification
41
+ * of the seller and the payee only, and CII in no `ram:GlobalID` at all, which
42
+ * is where CII writes an identifier with a scheme. Nothing is left out
43
+ * silently.
44
+ * @param accountingDoc The document
45
+ * @param targetFormat The format being written
46
+ */
47
+ export const assertPartyIdentifiersWritable = (accountingDoc: Pick<TAccountingDoc, 'to'>, targetFormat: string): void => {
48
+ const entries: unknown = (accountingDoc.to as { additionalIdentifiers?: unknown } | undefined)?.additionalIdentifiers;
49
+ if (!Array.isArray(entries)) {
50
+ return;
51
+ }
52
+ entries.forEach((entry: unknown, index: number) => {
53
+ const { scheme } = (entry ?? {}) as { scheme?: unknown };
54
+ if (typeof scheme === 'string' && scheme.trim().toUpperCase() === 'SEPA') {
55
+ throw new EInvoiceFormatError(
56
+ `BR-CL-10: Any identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. to.additionalIdentifiers[${index}] states the scheme SEPA, the creditor identifier (BT-90) of a seller or payee, which a buyer identifier (BT-46) cannot have`,
57
+ { targetFormat, unsupportedFeatures: ['BR-CL-10'] },
58
+ );
59
+ }
60
+ });
61
+ };
@@ -0,0 +1,13 @@
1
+ import { UBL_CUSTOMIZATION_IDS } from '../ubl/ubl.types.js';
2
+
3
+ /**
4
+ * Whether a specification identifier (BT-24) is that of PEPPOL BIS Billing 3.0,
5
+ * `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0`,
6
+ * the one identifier the PEPPOL BIS Billing 3.0 rules apply to. PINT, the
7
+ * A-NZ, Singapore and other PEPPOL derivatives, and the PEPPOL business process
8
+ * type (BT-23, `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0`), which an
9
+ * EN 16931 document may state as well, do not make a document one.
10
+ * @param customizationId The specification identifier
11
+ */
12
+ export const isPeppolBisBilling3 = (customizationId: string | undefined): boolean =>
13
+ (customizationId ?? '').trim() === UBL_CUSTOMIZATION_IDS.PEPPOL_BIS;
@@ -0,0 +1,102 @@
1
+ import type { TAccountingDoc } from '../../interfaces/common.js';
2
+ import { EInvoiceFormatError } from '../../errors.js';
3
+ import { DOMParser, xpath } from '../../plugins.js';
4
+ import { CII_NAMESPACES } from '../cii/cii.types.js';
5
+ import { UBL_NAMESPACES } from '../ubl/ubl.types.js';
6
+ import { getPartyIdentifiers } from './party.identifier.js';
7
+
8
+ /**
9
+ * The official text of EN 16931 BR-CO-26 (CEN/TC 434 EN 16931 validation
10
+ * artefacts 1.3.16, fatal in UBL and CII)
11
+ */
12
+ export const BR_CO_26_TEXT =
13
+ 'In order for the buyer to automatically identify a supplier, the Seller identifier (BT-29), the Seller legal registration identifier (BT-30) and/or the Seller VAT identifier (BT-31) shall be present.';
14
+
15
+ const ublSelect = xpath.useNamespaces({ cac: UBL_NAMESPACES.CAC, cbc: UBL_NAMESPACES.CBC });
16
+ const ciiSelect = xpath.useNamespaces({ rsm: CII_NAMESPACES.RSM, ram: CII_NAMESPACES.RAM });
17
+ const nodesOf = (result: xpath.SelectReturnType): Node[] => (Array.isArray(result) ? result : []);
18
+
19
+ /**
20
+ * Whether the seller of a UBL document meets BR-CO-26, as the CEN artefacts
21
+ * check it on `cac:AccountingSupplierParty`: a VAT identifier (BT-31, a
22
+ * `cac:PartyTaxScheme` with the scheme VAT), a seller identifier (BT-29, a
23
+ * `cac:PartyIdentification` whose scheme is not SEPA) or a legal registration
24
+ * identifier (BT-30, `cac:PartyLegalEntity/cbc:CompanyID`). A document without
25
+ * a seller party is not checked; BR-06 and BR-08 report that.
26
+ * @param doc The UBL document
27
+ */
28
+ export const ublSellerMeetsBrCo26 = (doc: Document): boolean => {
29
+ const sellers = nodesOf(ublSelect('/*/cac:AccountingSupplierParty', doc));
30
+ return sellers.every((seller) => {
31
+ const vatSchemes = nodesOf(ublSelect('./cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', seller)).filter(
32
+ (node) => (node.textContent ?? '').trim().toUpperCase() === 'VAT',
33
+ );
34
+ const hasVatIdentifier = vatSchemes.some(
35
+ (scheme) => nodesOf(ublSelect('../../cbc:CompanyID', scheme)).length > 0,
36
+ );
37
+ const hasSellerIdentifier = nodesOf(ublSelect("./cac:Party/cac:PartyIdentification/cbc:ID[not(@schemeID = 'SEPA')]", seller)).length > 0;
38
+ const hasLegalRegistration = nodesOf(ublSelect('./cac:Party/cac:PartyLegalEntity/cbc:CompanyID', seller)).length > 0;
39
+ return hasVatIdentifier || hasSellerIdentifier || hasLegalRegistration;
40
+ });
41
+ };
42
+
43
+ /**
44
+ * Whether the seller of a CII document meets BR-CO-26, as the CEN artefacts
45
+ * check it on `ram:SellerTradeParty`: a seller identifier (BT-29, `ram:ID` or
46
+ * `ram:GlobalID`), a legal registration identifier (BT-30,
47
+ * `ram:SpecifiedLegalOrganization/ram:ID`) or a VAT identifier (BT-31,
48
+ * `ram:SpecifiedTaxRegistration/ram:ID` with the scheme VA).
49
+ * @param doc The CII document
50
+ */
51
+ export const ciiSellerMeetsBrCo26 = (doc: Document): boolean =>
52
+ nodesOf(ciiSelect('//ram:SellerTradeParty', doc)).every(
53
+ (seller) =>
54
+ nodesOf(
55
+ ciiSelect(
56
+ "./ram:ID | ./ram:GlobalID | ./ram:SpecifiedLegalOrganization/ram:ID | ./ram:SpecifiedTaxRegistration/ram:ID[@schemeID='VA']",
57
+ seller,
58
+ ),
59
+ ).length > 0,
60
+ );
61
+
62
+ /**
63
+ * Whether the seller of an envelope states an identifier BR-CO-26 accepts:
64
+ * the VAT identifier (BT-31, `registrationDetails.vatId`), the legal
65
+ * registration identifier (BT-30, `registrationDetails.registrationId`) or a
66
+ * seller identifier (BT-29), which a decoded UBL document keeps in the
67
+ * party's `additionalIdentifiers`. A tax number (BT-32) is none of them.
68
+ * @param accountingDoc The document
69
+ */
70
+ export const envelopeSellerMeetsBrCo26 = (accountingDoc: Pick<TAccountingDoc, 'from'>): boolean => {
71
+ const seller = accountingDoc.from;
72
+ if (!seller) {
73
+ return true;
74
+ }
75
+ const details = seller.registrationDetails;
76
+ const hasSellerIdentifier = getPartyIdentifiers(seller).some((identifier) => identifier.scheme?.toUpperCase() !== 'SEPA');
77
+ return Boolean(details?.vatId?.trim() || details?.registrationId?.trim() || hasSellerIdentifier);
78
+ };
79
+
80
+ const writtenParser = new DOMParser();
81
+
82
+ /**
83
+ * Refuses a written document whose seller fails BR-CO-26, naming the rule; no
84
+ * identifier is put in place of a missing one. The seller states its VAT
85
+ * identifier (`registrationDetails.vatId`, BT-31) or its legal registration
86
+ * identifier (`registrationDetails.registrationId`, BT-30); a tax number
87
+ * (BT-32) alone does not identify it. A seller identifier (BT-29) is written
88
+ * from the party's `additionalIdentifiers` (no typed field in the envelope).
89
+ * @param xml The written document
90
+ * @param syntax The syntax it is written in
91
+ * @param targetFormat The format being written
92
+ */
93
+ export const assertSellerIdentified = (xml: string, syntax: 'ubl' | 'cii', targetFormat: string): void => {
94
+ const doc = writtenParser.parseFromString(xml, 'application/xml');
95
+ const meets = syntax === 'ubl' ? ublSellerMeetsBrCo26(doc) : ciiSellerMeetsBrCo26(doc);
96
+ if (!meets) {
97
+ throw new EInvoiceFormatError(
98
+ `BR-CO-26: ${BR_CO_26_TEXT} The seller states none of them: set from.registrationDetails.vatId (BT-31) or .registrationId (BT-30), or give the seller a seller identifier (BT-29) in from.additionalIdentifiers, where a decoded document keeps it; a tax number (BT-32, .taxNumber) does not identify the seller`,
99
+ { targetFormat, unsupportedFeatures: ['BR-CO-26'] },
100
+ );
101
+ }
102
+ };