@fin.cx/einvoice 8.3.1 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/einvoice.d.ts +26 -0
  3. package/dist_ts/einvoice.js +61 -3
  4. package/dist_ts/formats/base/base.decoder.d.ts +18 -1
  5. package/dist_ts/formats/base/base.decoder.js +30 -17
  6. package/dist_ts/formats/cii/cii.decoder.d.ts +89 -6
  7. package/dist_ts/formats/cii/cii.decoder.js +125 -30
  8. package/dist_ts/formats/cii/cii.encoder.d.ts +71 -1
  9. package/dist_ts/formats/cii/cii.encoder.js +195 -12
  10. package/dist_ts/formats/cii/cii.types.d.ts +2 -5
  11. package/dist_ts/formats/cii/cii.types.js +7 -9
  12. package/dist_ts/formats/cii/cii.validator.d.ts +13 -6
  13. package/dist_ts/formats/cii/cii.validator.js +29 -30
  14. package/dist_ts/formats/cii/facturx/facturx.decoder.js +19 -3
  15. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +0 -6
  16. package/dist_ts/formats/cii/facturx/facturx.encoder.js +17 -26
  17. package/dist_ts/formats/cii/facturx/facturx.types.d.ts +0 -5
  18. package/dist_ts/formats/cii/facturx/facturx.types.js +1 -8
  19. package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +0 -5
  20. package/dist_ts/formats/cii/facturx/facturx.validator.js +5 -29
  21. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +19 -2
  22. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +0 -7
  23. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +15 -68
  24. package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +0 -5
  25. package/dist_ts/formats/cii/zugferd/zugferd.types.js +1 -8
  26. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +26 -0
  27. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +47 -1
  28. package/dist_ts/formats/cii/zugferd/zugferd.validator.js +6 -2
  29. package/dist_ts/formats/semantic/semantic.adapter.js +29 -20
  30. package/dist_ts/formats/semantic/semantic.validator.js +17 -2
  31. package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +0 -4
  32. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -42
  33. package/dist_ts/formats/ubl/generic/ubl.encoder.js +29 -26
  34. package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
  35. package/dist_ts/formats/ubl/ubl.decoder.js +9 -1
  36. package/dist_ts/formats/ubl/ubl.encoder.js +13 -5
  37. package/dist_ts/formats/ubl/ubl.validator.d.ts +13 -0
  38. package/dist_ts/formats/ubl/ubl.validator.js +28 -1
  39. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +14 -1
  40. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -7
  41. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +30 -45
  42. package/dist_ts/formats/ubl/xrechnung.validator.d.ts +5 -4
  43. package/dist_ts/formats/ubl/xrechnung.validator.js +71 -68
  44. package/dist_ts/formats/utils/date.value.d.ts +11 -0
  45. package/dist_ts/formats/utils/date.value.js +33 -1
  46. package/dist_ts/formats/utils/document.totals.d.ts +1 -3
  47. package/dist_ts/formats/utils/document.totals.js +1 -2
  48. package/dist_ts/formats/utils/format.detector.js +16 -10
  49. package/dist_ts/formats/utils/paid.amount.d.ts +44 -0
  50. package/dist_ts/formats/utils/paid.amount.js +124 -0
  51. package/dist_ts/formats/utils/party.contact.d.ts +16 -0
  52. package/dist_ts/formats/utils/party.contact.js +16 -0
  53. package/dist_ts/formats/utils/party.identifier.d.ts +28 -0
  54. package/dist_ts/formats/utils/party.identifier.js +49 -0
  55. package/dist_ts/formats/utils/peppol.profile.d.ts +10 -0
  56. package/dist_ts/formats/utils/peppol.profile.js +12 -0
  57. package/dist_ts/formats/utils/preceding.invoice.d.ts +12 -2
  58. package/dist_ts/formats/utils/preceding.invoice.js +22 -3
  59. package/dist_ts/formats/utils/seller.identifier.d.ts +46 -0
  60. package/dist_ts/formats/utils/seller.identifier.js +78 -0
  61. package/dist_ts/formats/utils/stated.values.d.ts +80 -0
  62. package/dist_ts/formats/utils/stated.values.js +418 -0
  63. package/dist_ts/formats/utils/vat.category.d.ts +30 -2
  64. package/dist_ts/formats/utils/vat.category.js +36 -6
  65. package/dist_ts/formats/utils/vat.id.d.ts +18 -0
  66. package/dist_ts/formats/utils/vat.id.js +22 -0
  67. package/dist_ts/formats/validation/conformance.harness.js +6 -6
  68. package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -19
  69. package/dist_ts/formats/validation/facturx.validator.js +6 -6
  70. package/dist_ts/formats/validation/integrated.validator.js +4 -13
  71. package/dist_ts/formats/validation/peppol.validator.js +6 -13
  72. package/dist_ts/formats/validation/validation.types.d.ts +5 -0
  73. package/dist_ts/formats/validation/validation.types.js +6 -1
  74. package/dist_ts/formats/validation/vat-categories.validator.d.ts +21 -42
  75. package/dist_ts/formats/validation/vat-categories.validator.js +137 -431
  76. package/dist_ts/formats/validation/xrechnung.validator.d.ts +11 -58
  77. package/dist_ts/formats/validation/xrechnung.validator.js +58 -324
  78. package/dist_ts/index.d.ts +1 -0
  79. package/dist_ts/index.js +1 -1
  80. package/dist_ts/interfaces/common.d.ts +1 -0
  81. package/dist_ts/interfaces/en16931-metadata.d.ts +0 -5
  82. package/dist_ts/interfaces/stated.values.d.ts +93 -0
  83. package/dist_ts/interfaces/stated.values.js +2 -0
  84. package/package.json +2 -2
  85. package/readme.md +182 -6
  86. package/ts/00_commitinfo_data.ts +1 -1
  87. package/ts/einvoice.ts +70 -2
  88. package/ts/formats/base/base.decoder.ts +32 -29
  89. package/ts/formats/cii/cii.decoder.ts +159 -32
  90. package/ts/formats/cii/cii.encoder.ts +210 -14
  91. package/ts/formats/cii/cii.types.ts +7 -9
  92. package/ts/formats/cii/cii.validator.ts +30 -32
  93. package/ts/formats/cii/facturx/facturx.decoder.ts +21 -2
  94. package/ts/formats/cii/facturx/facturx.encoder.ts +19 -26
  95. package/ts/formats/cii/facturx/facturx.types.ts +0 -9
  96. package/ts/formats/cii/facturx/facturx.validator.ts +5 -43
  97. package/ts/formats/cii/zugferd/zugferd.decoder.ts +21 -1
  98. package/ts/formats/cii/zugferd/zugferd.encoder.ts +16 -73
  99. package/ts/formats/cii/zugferd/zugferd.types.ts +0 -9
  100. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +66 -0
  101. package/ts/formats/cii/zugferd/zugferd.validator.ts +5 -1
  102. package/ts/formats/semantic/semantic.adapter.ts +28 -19
  103. package/ts/formats/semantic/semantic.validator.ts +17 -1
  104. package/ts/formats/ubl/en16931.ubl.validator.ts +5 -64
  105. package/ts/formats/ubl/generic/ubl.encoder.ts +31 -25
  106. package/ts/formats/ubl/ubl.decoder.ts +12 -0
  107. package/ts/formats/ubl/ubl.encoder.ts +12 -4
  108. package/ts/formats/ubl/ubl.validator.ts +29 -0
  109. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +13 -0
  110. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +33 -45
  111. package/ts/formats/ubl/xrechnung.validator.ts +75 -127
  112. package/ts/formats/utils/date.value.ts +33 -0
  113. package/ts/formats/utils/document.totals.ts +1 -4
  114. package/ts/formats/utils/format.detector.ts +15 -9
  115. package/ts/formats/utils/paid.amount.ts +160 -0
  116. package/ts/formats/utils/party.contact.ts +30 -0
  117. package/ts/formats/utils/party.identifier.ts +61 -0
  118. package/ts/formats/utils/peppol.profile.ts +13 -0
  119. package/ts/formats/utils/preceding.invoice.ts +23 -2
  120. package/ts/formats/utils/seller.identifier.ts +102 -0
  121. package/ts/formats/utils/stated.values.ts +499 -0
  122. package/ts/formats/utils/vat.category.ts +47 -5
  123. package/ts/formats/utils/vat.id.ts +24 -0
  124. package/ts/formats/validation/conformance.harness.ts +5 -5
  125. package/ts/formats/validation/en16931.business-rules.validator.ts +3 -26
  126. package/ts/formats/validation/facturx.validator.ts +5 -5
  127. package/ts/formats/validation/integrated.validator.ts +3 -16
  128. package/ts/formats/validation/peppol.validator.ts +5 -16
  129. package/ts/formats/validation/validation.types.ts +7 -1
  130. package/ts/formats/validation/vat-categories.validator.ts +179 -761
  131. package/ts/formats/validation/xrechnung.validator.ts +61 -382
  132. package/ts/index.ts +9 -0
  133. package/ts/interfaces/common.ts +1 -0
  134. package/ts/interfaces/en16931-metadata.ts +3 -8
  135. package/ts/interfaces/stated.values.ts +94 -0
  136. package/ts/readme.md +1 -1
  137. package/dist_ts/formats/utils/eu.memberstates.d.ts +0 -11
  138. package/dist_ts/formats/utils/eu.memberstates.js +0 -16
  139. package/ts/formats/utils/eu.memberstates.ts +0 -16
@@ -16,7 +16,13 @@ export type TPrecedingInvoiceReference = TCorrectedInvoiceReference;
16
16
  * taken only from what the document states:
17
17
  * - a corrected invoice: the invoice it corrects (`correctedInvoice`);
18
18
  * - a corrected invoice or a credit note: every related document with the
19
- * relation `'corrects'`.
19
+ * relation `'corrects'`;
20
+ * - a document that can be a final invoice (an invoice, a corrected invoice or
21
+ * a self-billed invoice): the advance invoice of every advance payment that
22
+ * states one (`advancePayments[].invoice`), after the references above. The
23
+ * amounts are the paid amount (BT-113); the deduction per rate that § 14
24
+ * Abs. 5 Satz 2 UStG asks of a final invoice has no element in EN 16931
25
+ * (BMF letter of 15 October 2024, Rn. 48).
20
26
  * Other documents and other relations write none.
21
27
  */
22
28
  export const getPrecedingInvoiceReferences = (
@@ -40,6 +46,17 @@ export const getPrecedingInvoiceReferences = (
40
46
  }
41
47
  }
42
48
  }
49
+ if ('advancePayments' in accountingDoc) {
50
+ for (const advancePayment of accountingDoc.advancePayments ?? []) {
51
+ if (advancePayment.invoice) {
52
+ references.push(
53
+ advancePayment.invoice.issueDate === undefined
54
+ ? { documentId: advancePayment.invoice.documentId }
55
+ : { documentId: advancePayment.invoice.documentId, issueDate: advancePayment.invoice.issueDate },
56
+ );
57
+ }
58
+ }
59
+ }
43
60
  return references;
44
61
  };
45
62
 
@@ -48,7 +65,11 @@ export const getPrecedingInvoiceReferences = (
48
65
  * inverse of {@link getPrecedingInvoiceReferences}: on a corrected invoice the
49
66
  * first reference is the invoice it corrects and further ones are related
50
67
  * documents it corrects; on a credit note every reference is a related
51
- * document it corrects. Other documents keep no reference, as before.
68
+ * document it corrects. Other documents keep no reference, as before. The
69
+ * XML does not tell an advance invoice from a corrected one, so a corrected
70
+ * invoice's advance invoices read back as related documents it corrects, and
71
+ * an invoice's advance invoices are not read: `advancePayments` needs the net
72
+ * and the tax per rate of every payment, which the XML does not carry.
52
73
  */
53
74
  export const getPrecedingInvoiceFields = (
54
75
  accountingDocType: TAccountingDocType,
@@ -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
+ };
@@ -0,0 +1,499 @@
1
+ import type {
2
+ IEInvoiceStatedValues,
3
+ IStatedAllowanceCharge,
4
+ IStatedDocumentTotals,
5
+ IStatedLine,
6
+ IStatedVatBreakdown,
7
+ } from '../../interfaces/stated.values.js';
8
+ import { xpath } from '../../plugins.js';
9
+ import { CII_NAMESPACES } from '../cii/cii.types.js';
10
+ import { UBL_NAMESPACES } from '../ubl/ubl.types.js';
11
+ import { Decimal } from './decimal.js';
12
+ import type { IDocumentTotals } from './document.totals.js';
13
+ import { toPlainDecimalString } from './number.text.js';
14
+
15
+ /** A stated value that is no decimal number, by business term and the text found */
16
+ export interface IStatedValueProblem {
17
+ businessTerm: string;
18
+ text: string;
19
+ }
20
+
21
+ /** The EN 16931 names of the amounts read, for a message */
22
+ const BUSINESS_TERM_NAMES: Record<string, string> = {
23
+ 'BT-92': 'document level allowance amount',
24
+ 'BT-93': 'document level allowance base amount',
25
+ 'BT-94': 'document level allowance percentage',
26
+ 'BT-96': 'document level allowance VAT rate',
27
+ 'BT-99': 'document level charge amount',
28
+ 'BT-100': 'document level charge base amount',
29
+ 'BT-101': 'document level charge percentage',
30
+ 'BT-103': 'document level charge VAT rate',
31
+ 'BT-106': 'sum of invoice line net amount',
32
+ 'BT-107': 'sum of allowances on document level',
33
+ 'BT-108': 'sum of charges on document level',
34
+ 'BT-109': 'invoice total amount without VAT',
35
+ 'BT-110': 'invoice total VAT amount',
36
+ 'BT-111': 'invoice total VAT amount in accounting currency',
37
+ 'BT-112': 'invoice total amount with VAT',
38
+ 'BT-113': 'paid amount',
39
+ 'BT-114': 'rounding amount',
40
+ 'BT-115': 'amount due for payment',
41
+ 'BT-116': 'VAT category taxable amount',
42
+ 'BT-117': 'VAT category tax amount',
43
+ 'BT-119': 'VAT category rate',
44
+ 'BT-131': 'invoice line net amount',
45
+ 'BT-152': 'invoiced item VAT rate',
46
+ };
47
+
48
+ /**
49
+ * The message for a stated amount that is no decimal number, as the paid
50
+ * amount's has been: "The paid amount (BT-113) is no decimal number: …"
51
+ * @param problem The problem
52
+ */
53
+ export const statedValueProblemMessage = (problem: IStatedValueProblem): string =>
54
+ `The ${BUSINESS_TERM_NAMES[problem.businessTerm] ?? 'amount'} (${problem.businessTerm}) is no decimal number: ${problem.text}`;
55
+
56
+ /** The stated values of a document, and the stated amounts that are no number */
57
+ export interface IStatedValuesRead {
58
+ values: IEInvoiceStatedValues;
59
+ problems: IStatedValueProblem[];
60
+ /** How many VAT totals (BT-110) in the document currency the document states; BR-CO-15 asks for exactly one */
61
+ documentCurrencyTaxTotals?: number;
62
+ /** Each `cac:TaxTotal` of a UBL document with its tax amount and the tax amounts of its own subtotals, which BR-CO-14 compares */
63
+ ublTaxTotals?: Array<{ taxAmount?: number; subtotalTaxAmounts: Array<number | undefined> }>;
64
+ }
65
+
66
+ /** A finding on stated values: the rule, its official text and where it applies */
67
+ export interface IStatedValueFinding {
68
+ ruleId: string;
69
+ message: string;
70
+ location: string;
71
+ }
72
+
73
+ const nodesOf = (result: xpath.SelectReturnType): Node[] => (Array.isArray(result) ? result : []);
74
+
75
+ /**
76
+ * Reads the stated values of one document, with an XPath selector for its
77
+ * syntax; an amount whose text is no decimal number is left undefined and
78
+ * listed as a problem
79
+ */
80
+ class StatedValueReader {
81
+ public readonly problems: IStatedValueProblem[] = [];
82
+
83
+ constructor(private readonly select: xpath.XPathSelect, private readonly doc: Document) {}
84
+
85
+ public nodes(path: string, context: Node = this.doc): Node[] {
86
+ return nodesOf(this.select(path, context));
87
+ }
88
+
89
+ public text(path: string, context: Node = this.doc): string | undefined {
90
+ const [node] = this.nodes(path, context);
91
+ const text = node?.textContent?.trim();
92
+ return text ? text : undefined;
93
+ }
94
+
95
+ public amount(path: string, businessTerm: string, context: Node = this.doc): number | undefined {
96
+ const text = this.text(path, context);
97
+ if (text === undefined) {
98
+ return undefined;
99
+ }
100
+ const value = Number(text);
101
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(text) || !Number.isFinite(value)) {
102
+ this.problems.push({ businessTerm, text });
103
+ return undefined;
104
+ }
105
+ return value;
106
+ }
107
+ }
108
+
109
+ /** Whether an xs:boolean indicator states the value ('true' or '1', 'false' or '0') */
110
+ const isIndicator = (text: string | undefined, value: boolean): boolean =>
111
+ value ? text === 'true' || text === '1' : text === 'false' || text === '0';
112
+
113
+ /** Only the fields a document states, so that an absent one is undefined */
114
+ const stated = <T extends object>(fields: T): T =>
115
+ Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)) as T;
116
+
117
+ const ublSelect = xpath.useNamespaces({ cac: UBL_NAMESPACES.CAC, cbc: UBL_NAMESPACES.CBC });
118
+
119
+ /**
120
+ * Reads the stated values of a UBL invoice or credit note: the document totals
121
+ * (`cac:LegalMonetaryTotal`, BT-106 to BT-115, the VAT total `cac:TaxTotal/cbc:TaxAmount`
122
+ * in the document currency for BT-110 and in the VAT accounting currency for
123
+ * BT-111), the VAT breakdown (`cac:TaxSubtotal`, BT-116 to BT-121), the line
124
+ * amounts (BT-126, BT-131, BT-151, BT-152) and the document level allowances
125
+ * and charges (`cac:AllowanceCharge`, BT-92 to BT-105)
126
+ * @param doc The UBL document
127
+ */
128
+ export const readUblStatedValues = (doc: Document): IStatedValuesRead => {
129
+ const read = new StatedValueReader(ublSelect, doc);
130
+ const total = '/*/cac:LegalMonetaryTotal';
131
+ const documentCurrency = read.text('/*/cbc:DocumentCurrencyCode');
132
+ const accountingCurrency = read.text('/*/cbc:TaxCurrencyCode');
133
+ const taxTotalIn = (currency: string | undefined, businessTerm: string): number | undefined =>
134
+ currency === undefined ? undefined : read.amount(`/*/cac:TaxTotal/cbc:TaxAmount[@currencyID = '${currency}']`, businessTerm);
135
+ const accountingTaxTotal = accountingCurrency && accountingCurrency !== documentCurrency ? taxTotalIn(accountingCurrency, 'BT-111') : undefined;
136
+ // BT-110 and the VAT breakdown (BG-23) are the VAT total in the document currency and its subtotals;
137
+ // a second total in the VAT accounting currency (BT-111) may state subtotals of its own
138
+ const documentTaxTotals = read
139
+ .nodes('/*/cac:TaxTotal')
140
+ .filter((taxTotal) => documentCurrency !== undefined && read.text(`./cbc:TaxAmount[@currencyID = '${documentCurrency}']`, taxTotal) !== undefined);
141
+ // a total without a currency, which the UBL schema does not allow, is read as the document's
142
+ const candidateTotals = documentTaxTotals.length > 0 ? documentTaxTotals : read.nodes('/*/cac:TaxTotal');
143
+ const [breakdownTotal] = candidateTotals.filter((taxTotal) => read.nodes('./cac:TaxSubtotal', taxTotal).length > 0);
144
+ const breakdownTaxTotal = breakdownTotal === undefined ? undefined : read.amount('./cbc:TaxAmount', 'BT-110', breakdownTotal);
145
+ const totals: IStatedDocumentTotals = stated({
146
+ lineTotal: read.amount(`${total}/cbc:LineExtensionAmount`, 'BT-106'),
147
+ allowanceTotal: read.amount(`${total}/cbc:AllowanceTotalAmount`, 'BT-107'),
148
+ chargeTotal: read.amount(`${total}/cbc:ChargeTotalAmount`, 'BT-108'),
149
+ taxExclusiveTotal: read.amount(`${total}/cbc:TaxExclusiveAmount`, 'BT-109'),
150
+ taxTotal: breakdownTaxTotal ?? taxTotalIn(documentCurrency, 'BT-110'),
151
+ taxTotalInAccountingCurrency:
152
+ accountingTaxTotal === undefined || accountingCurrency === undefined ? undefined : { amount: accountingTaxTotal, currency: accountingCurrency },
153
+ taxInclusiveTotal: read.amount(`${total}/cbc:TaxInclusiveAmount`, 'BT-112'),
154
+ paidAmount: read.amount(`${total}/cbc:PrepaidAmount`, 'BT-113'),
155
+ roundingAmount: read.amount(`${total}/cbc:PayableRoundingAmount`, 'BT-114'),
156
+ amountDue: read.amount(`${total}/cbc:PayableAmount`, 'BT-115'),
157
+ });
158
+ const vatBreakdown: IStatedVatBreakdown[] = (breakdownTotal === undefined ? [] : read.nodes('./cac:TaxSubtotal', breakdownTotal)).map((subtotal) =>
159
+ stated({
160
+ taxableAmount: read.amount('./cbc:TaxableAmount', 'BT-116', subtotal),
161
+ taxAmount: read.amount('./cbc:TaxAmount', 'BT-117', subtotal),
162
+ categoryCode: read.text('./cac:TaxCategory/cbc:ID', subtotal),
163
+ rate: read.amount('./cac:TaxCategory/cbc:Percent', 'BT-119', subtotal),
164
+ exemptionReason: read.text('./cac:TaxCategory/cbc:TaxExemptionReason', subtotal),
165
+ exemptionReasonCode: read.text('./cac:TaxCategory/cbc:TaxExemptionReasonCode', subtotal),
166
+ }),
167
+ );
168
+ const lines: IStatedLine[] = read.nodes('/*/cac:InvoiceLine | /*/cac:CreditNoteLine').map((line) =>
169
+ stated({
170
+ lineId: read.text('./cbc:ID', line),
171
+ netAmount: read.amount('./cbc:LineExtensionAmount', 'BT-131', line),
172
+ vatCategoryCode: read.text('./cac:Item/cac:ClassifiedTaxCategory/cbc:ID', line),
173
+ vatRate: read.amount('./cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', 'BT-152', line),
174
+ }),
175
+ );
176
+ const allowanceCharges = (charge: boolean): IStatedAllowanceCharge[] =>
177
+ read
178
+ .nodes('/*/cac:AllowanceCharge')
179
+ .filter((node) => isIndicator(read.text('./cbc:ChargeIndicator', node), charge))
180
+ .map((node) =>
181
+ stated({
182
+ amount: read.amount('./cbc:Amount', charge ? 'BT-99' : 'BT-92', node),
183
+ baseAmount: read.amount('./cbc:BaseAmount', charge ? 'BT-100' : 'BT-93', node),
184
+ percentage: read.amount('./cbc:MultiplierFactorNumeric', charge ? 'BT-101' : 'BT-94', node),
185
+ vatCategoryCode: read.text('./cac:TaxCategory/cbc:ID', node),
186
+ vatRate: read.amount('./cac:TaxCategory/cbc:Percent', charge ? 'BT-103' : 'BT-96', node),
187
+ reason: read.text('./cbc:AllowanceChargeReason', node),
188
+ reasonCode: read.text('./cbc:AllowanceChargeReasonCode', node),
189
+ }),
190
+ );
191
+ return {
192
+ values: { totals, vatBreakdown, lines, allowances: allowanceCharges(false), charges: allowanceCharges(true) },
193
+ problems: read.problems,
194
+ documentCurrencyTaxTotals:
195
+ documentCurrency === undefined ? 0 : read.nodes(`/*/cac:TaxTotal/cbc:TaxAmount[@currencyID = '${documentCurrency}']`).length,
196
+ ublTaxTotals: read.nodes('/*/cac:TaxTotal').map((taxTotal) => ({
197
+ taxAmount: read.amount('./cbc:TaxAmount', 'BT-110', taxTotal),
198
+ subtotalTaxAmounts: read.nodes('./cac:TaxSubtotal', taxTotal).map((subtotal) => read.amount('./cbc:TaxAmount', 'BT-117', subtotal)),
199
+ })),
200
+ };
201
+ };
202
+
203
+ const ciiSelect = xpath.useNamespaces({ rsm: CII_NAMESPACES.RSM, ram: CII_NAMESPACES.RAM, udt: CII_NAMESPACES.UDT });
204
+
205
+ /**
206
+ * Reads the stated values of a CII (D16B) document: the document totals of the
207
+ * header settlement (`ram:SpecifiedTradeSettlementHeaderMonetarySummation`,
208
+ * BT-106 to BT-115, `ram:TaxTotalAmount` in the invoice currency for BT-110 and
209
+ * in the VAT accounting currency for BT-111), the VAT breakdown (the header
210
+ * `ram:ApplicableTradeTax`, BT-116 to BT-121), the line amounts (BT-126,
211
+ * BT-131, BT-151, BT-152) and the document level allowances and charges (the
212
+ * header `ram:SpecifiedTradeAllowanceCharge`, BT-92 to BT-105)
213
+ * @param doc The CII document
214
+ */
215
+ export const readCiiStatedValues = (doc: Document): IStatedValuesRead => {
216
+ const read = new StatedValueReader(ciiSelect, doc);
217
+ const settlement = '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement';
218
+ const total = `${settlement}/ram:SpecifiedTradeSettlementHeaderMonetarySummation`;
219
+ const documentCurrency = read.text(`${settlement}/ram:InvoiceCurrencyCode`);
220
+ const accountingCurrency = read.text(`${settlement}/ram:TaxCurrencyCode`);
221
+ // CII tells BT-110 and BT-111 apart by their currency (BR-CO-15 reads BT-110 as the one in the invoice currency)
222
+ const taxTotalIn = (currency: string | undefined, businessTerm: string): number | undefined =>
223
+ currency === undefined ? undefined : read.amount(`${total}/ram:TaxTotalAmount[@currencyID = '${currency}']`, businessTerm);
224
+ const accountingTaxTotal = accountingCurrency && accountingCurrency !== documentCurrency ? taxTotalIn(accountingCurrency, 'BT-111') : undefined;
225
+ const totals: IStatedDocumentTotals = stated({
226
+ lineTotal: read.amount(`${total}/ram:LineTotalAmount`, 'BT-106'),
227
+ allowanceTotal: read.amount(`${total}/ram:AllowanceTotalAmount`, 'BT-107'),
228
+ chargeTotal: read.amount(`${total}/ram:ChargeTotalAmount`, 'BT-108'),
229
+ taxExclusiveTotal: read.amount(`${total}/ram:TaxBasisTotalAmount`, 'BT-109'),
230
+ taxTotal: taxTotalIn(documentCurrency, 'BT-110'),
231
+ taxTotalInAccountingCurrency:
232
+ accountingTaxTotal === undefined || accountingCurrency === undefined ? undefined : { amount: accountingTaxTotal, currency: accountingCurrency },
233
+ taxInclusiveTotal: read.amount(`${total}/ram:GrandTotalAmount`, 'BT-112'),
234
+ paidAmount: read.amount(`${total}/ram:TotalPrepaidAmount`, 'BT-113'),
235
+ roundingAmount: read.amount(`${total}/ram:RoundingAmount`, 'BT-114'),
236
+ amountDue: read.amount(`${total}/ram:DuePayableAmount`, 'BT-115'),
237
+ });
238
+ const vatBreakdown: IStatedVatBreakdown[] = read.nodes(`${settlement}/ram:ApplicableTradeTax`).map((tax) =>
239
+ stated({
240
+ taxableAmount: read.amount('./ram:BasisAmount', 'BT-116', tax),
241
+ taxAmount: read.amount('./ram:CalculatedAmount', 'BT-117', tax),
242
+ categoryCode: read.text('./ram:CategoryCode', tax),
243
+ rate: read.amount('./ram:RateApplicablePercent', 'BT-119', tax),
244
+ exemptionReason: read.text('./ram:ExemptionReason', tax),
245
+ exemptionReasonCode: read.text('./ram:ExemptionReasonCode', tax),
246
+ }),
247
+ );
248
+ const lines: IStatedLine[] = read.nodes('/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem').map((line) =>
249
+ stated({
250
+ lineId: read.text('./ram:AssociatedDocumentLineDocument/ram:LineID', line),
251
+ netAmount: read.amount('./ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount', 'BT-131', line),
252
+ vatCategoryCode: read.text('./ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode', line),
253
+ vatRate: read.amount('./ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', 'BT-152', line),
254
+ }),
255
+ );
256
+ const allowanceCharges = (charge: boolean): IStatedAllowanceCharge[] =>
257
+ read
258
+ .nodes(`${settlement}/ram:SpecifiedTradeAllowanceCharge`)
259
+ .filter((node) => isIndicator(read.text('./ram:ChargeIndicator/udt:Indicator', node), charge))
260
+ .map((node) =>
261
+ stated({
262
+ amount: read.amount('./ram:ActualAmount', charge ? 'BT-99' : 'BT-92', node),
263
+ baseAmount: read.amount('./ram:BasisAmount', charge ? 'BT-100' : 'BT-93', node),
264
+ percentage: read.amount('./ram:CalculationPercent', charge ? 'BT-101' : 'BT-94', node),
265
+ vatCategoryCode: read.text('./ram:CategoryTradeTax/ram:CategoryCode', node),
266
+ vatRate: read.amount('./ram:CategoryTradeTax/ram:RateApplicablePercent', charge ? 'BT-103' : 'BT-96', node),
267
+ reason: read.text('./ram:Reason', node),
268
+ reasonCode: read.text('./ram:ReasonCode', node),
269
+ }),
270
+ );
271
+ return {
272
+ values: { totals, vatBreakdown, lines, allowances: allowanceCharges(false), charges: allowanceCharges(true) },
273
+ problems: read.problems,
274
+ documentCurrencyTaxTotals:
275
+ documentCurrency === undefined ? 0 : read.nodes(`${total}/ram:TaxTotalAmount[@currencyID = '${documentCurrency}']`).length,
276
+ };
277
+ };
278
+
279
+ /** The official texts of the EN 16931 rules on the document totals (CEN artefacts 1.3.16) */
280
+ const TOTALS_RULES = {
281
+ 'BR-CO-10': 'Sum of Invoice line net amount (BT-106) = Σ Invoice line net amount (BT-131).',
282
+ 'BR-CO-11': 'Sum of allowances on document level (BT-107) = Σ Document level allowance amount (BT-92).',
283
+ 'BR-CO-12': 'Sum of charges on document level (BT-108) = Σ Document level charge amount (BT-99).',
284
+ 'BR-CO-13':
285
+ 'Invoice total amount without VAT (BT-109) = Σ Invoice line net amount (BT-131) - Sum of allowances on document level (BT-107) + Sum of charges on document level (BT-108).',
286
+ 'BR-CO-14': 'Invoice total VAT amount (BT-110) = Σ VAT category tax amount (BT-117).',
287
+ 'BR-CO-15': 'Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).',
288
+ 'BR-CO-16': 'Amount due for payment (BT-115) = Invoice total amount with VAT (BT-112) -Paid amount (BT-113) +Rounding amount (BT-114).',
289
+ 'BR-CO-17': 'VAT category tax amount (BT-117) = VAT category taxable amount (BT-116) x (VAT category rate (BT-119) / 100), rounded to two decimals.',
290
+ } as const;
291
+
292
+ const decimal = (value: number): Decimal => new Decimal(toPlainDecimalString(value));
293
+ const sum = (values: Array<number | undefined>): Decimal => Decimal.sum(values.map((value) => decimal(value ?? 0)));
294
+ /** XPath's round(x × 100) div 100: two decimals, a half rounded towards positive infinity */
295
+ const roundCents = (value: Decimal): Decimal => value.multiply(100).add('0.5').round(0, 'FLOOR').divide(100);
296
+ /** XPath's round(x): an integer, a half rounded towards positive infinity */
297
+ const roundInteger = (value: Decimal): Decimal => value.add('0.5').round(0, 'FLOOR');
298
+
299
+ /**
300
+ * Checks the stated values of a document against each other as the EN 16931
301
+ * rules BR-CO-10 to BR-CO-17 do in the CEN artefacts 1.3.16: each sum rounded
302
+ * to two decimals, BR-CO-17 with the artefacts' tolerance of one unit (UBL
303
+ * strictly within it, CII up to it). A rule whose amounts the document does
304
+ * not state is not checked; the rules on the presence of the totals (BR-12 to
305
+ * BR-15) report that.
306
+ * @param read The stated values read from the document
307
+ * @param syntax The document's syntax
308
+ */
309
+ export const findStatedValueDisagreements = (read: IStatedValuesRead, syntax: 'ubl' | 'cii'): IStatedValueFinding[] => {
310
+ const { values } = read;
311
+ const locations = syntax === 'ubl' ? UBL_TOTALS_LOCATIONS : CII_TOTALS_LOCATIONS;
312
+ const findings: IStatedValueFinding[] = [];
313
+ const report = (ruleId: keyof typeof TOTALS_RULES, detail: string) =>
314
+ findings.push({ ruleId, message: `${TOTALS_RULES[ruleId]} ${detail}`, location: locations[ruleId] });
315
+ const { totals } = values;
316
+ const text = (value: Decimal) => value.toString(2);
317
+
318
+ if (totals.lineTotal !== undefined) {
319
+ const expected = roundCents(sum(values.lines.map((line) => line.netAmount)));
320
+ if (!decimal(totals.lineTotal).equals(expected)) {
321
+ report('BR-CO-10', `The document states ${totals.lineTotal}, the sum of its line net amounts is ${text(expected)}.`);
322
+ }
323
+ }
324
+ const checkSum = (ruleId: 'BR-CO-11' | 'BR-CO-12', stated: number | undefined, entries: IStatedAllowanceCharge[]) => {
325
+ if (stated === undefined && entries.length === 0) {
326
+ return;
327
+ }
328
+ const expected = roundCents(sum(entries.map((entry) => entry.amount)));
329
+ if (stated === undefined || !decimal(stated).equals(expected)) {
330
+ report(ruleId, `The document states ${stated ?? 'none'}, the sum of its amounts is ${text(expected)}.`);
331
+ }
332
+ };
333
+ checkSum('BR-CO-11', totals.allowanceTotal, values.allowances);
334
+ checkSum('BR-CO-12', totals.chargeTotal, values.charges);
335
+ if (totals.taxExclusiveTotal !== undefined && totals.lineTotal !== undefined) {
336
+ const expected = roundCents(decimal(totals.lineTotal).subtract(totals.allowanceTotal ?? 0).add(totals.chargeTotal ?? 0));
337
+ if (!decimal(totals.taxExclusiveTotal).equals(expected)) {
338
+ report('BR-CO-13', `The document states ${totals.taxExclusiveTotal}, BT-106 - BT-107 + BT-108 is ${text(expected)}.`);
339
+ }
340
+ }
341
+ if (syntax === 'ubl') {
342
+ // each VAT total with subtotals, on its own, as the UBL artefacts check it
343
+ (read.ublTaxTotals ?? []).forEach((taxTotal) => {
344
+ if (taxTotal.subtotalTaxAmounts.length === 0) {
345
+ return;
346
+ }
347
+ const expected = roundCents(sum(taxTotal.subtotalTaxAmounts));
348
+ if (taxTotal.taxAmount === undefined || !decimal(taxTotal.taxAmount).equals(expected)) {
349
+ report('BR-CO-14', `A VAT total states ${taxTotal.taxAmount ?? 'none'}, the sum of its VAT category tax amounts is ${text(expected)}.`);
350
+ }
351
+ });
352
+ } else if (totals.taxTotal !== undefined) {
353
+ // the VAT total in the invoice currency against every header VAT breakdown
354
+ const expected = roundCents(sum(values.vatBreakdown.map((group) => group.taxAmount)));
355
+ if (!decimal(totals.taxTotal).equals(expected)) {
356
+ report('BR-CO-14', `The document states ${totals.taxTotal}, the sum of its VAT category tax amounts is ${text(expected)}.`);
357
+ }
358
+ }
359
+ if (totals.taxInclusiveTotal !== undefined && totals.taxExclusiveTotal !== undefined) {
360
+ const inclusive = decimal(totals.taxInclusiveTotal);
361
+ const exclusive = decimal(totals.taxExclusiveTotal);
362
+ const withTax = totals.taxTotal === undefined ? undefined : roundCents(exclusive.add(totals.taxTotal));
363
+ // both ask for exactly one VAT total in the document currency; CII also accepts a total with VAT equal to the one without
364
+ const agrees =
365
+ syntax === 'ubl'
366
+ ? read.documentCurrencyTaxTotals === 1 && withTax !== undefined && inclusive.equals(withTax)
367
+ : (read.documentCurrencyTaxTotals === 1 && withTax !== undefined && inclusive.equals(withTax)) || inclusive.equals(exclusive);
368
+ if (!agrees) {
369
+ report('BR-CO-15', `The document states ${totals.taxInclusiveTotal}, BT-109 + BT-110 is ${withTax === undefined ? 'not stated' : text(withTax)}.`);
370
+ }
371
+ }
372
+ if (totals.amountDue !== undefined && totals.taxInclusiveTotal !== undefined) {
373
+ const expected = decimal(totals.taxInclusiveTotal).subtract(totals.paidAmount ?? 0).add(totals.roundingAmount ?? 0);
374
+ // UBL compares both sides rounded to two decimals, CII the stated amounts as they are
375
+ const agrees =
376
+ syntax === 'ubl'
377
+ ? roundCents(decimal(totals.amountDue).subtract(totals.roundingAmount ?? 0)).equals(
378
+ roundCents(decimal(totals.taxInclusiveTotal).subtract(totals.paidAmount ?? 0)),
379
+ )
380
+ : decimal(totals.amountDue).equals(expected);
381
+ if (!agrees) {
382
+ report('BR-CO-16', `The document states ${totals.amountDue}, BT-112 - BT-113 + BT-114 is ${text(expected)}.`);
383
+ }
384
+ }
385
+ values.vatBreakdown.forEach((group, index) => {
386
+ if (group.taxAmount === undefined) {
387
+ return;
388
+ }
389
+ const taxAmount = decimal(group.taxAmount);
390
+ const rate = group.rate === undefined ? undefined : decimal(group.rate);
391
+ let agrees: boolean;
392
+ if (rate === undefined || roundInteger(rate).isZero()) {
393
+ agrees = roundInteger(taxAmount).isZero();
394
+ } else {
395
+ const expected = roundCents(decimal(group.taxableAmount ?? 0).abs().multiply(rate.divide(100)));
396
+ // the artefacts accept a tax amount within one unit of the computed one
397
+ agrees =
398
+ syntax === 'ubl'
399
+ ? taxAmount.abs().subtract(1).lessThan(expected) && taxAmount.abs().add(1).greaterThan(expected)
400
+ : taxAmount.abs().subtract(1).lessThanOrEqual(expected) && taxAmount.abs().add(1).greaterThanOrEqual(expected);
401
+ }
402
+ if (!agrees) {
403
+ findings.push({
404
+ ruleId: 'BR-CO-17',
405
+ message: `${TOTALS_RULES['BR-CO-17']} VAT breakdown ${index + 1} (${group.categoryCode ?? '?'} ${group.rate ?? ''}) states ${group.taxAmount} on ${group.taxableAmount ?? 'no taxable amount'}.`,
406
+ location: locations['BR-CO-17'],
407
+ });
408
+ }
409
+ });
410
+ return findings;
411
+ };
412
+
413
+ /** Where the rules on the totals apply in a UBL document */
414
+ const UBL_TOTALS_LOCATIONS: Record<keyof typeof TOTALS_RULES, string> = {
415
+ 'BR-CO-10': '/*/cac:LegalMonetaryTotal/cbc:LineExtensionAmount',
416
+ 'BR-CO-11': '/*/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount',
417
+ 'BR-CO-12': '/*/cac:LegalMonetaryTotal/cbc:ChargeTotalAmount',
418
+ 'BR-CO-13': '/*/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
419
+ 'BR-CO-14': '/*/cac:TaxTotal/cbc:TaxAmount',
420
+ 'BR-CO-15': '/*/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
421
+ 'BR-CO-16': '/*/cac:LegalMonetaryTotal/cbc:PayableAmount',
422
+ 'BR-CO-17': '/*/cac:TaxTotal/cac:TaxSubtotal',
423
+ };
424
+
425
+ const CII_TOTALS = '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation';
426
+ /** Where the rules on the totals apply in a CII document */
427
+ const CII_TOTALS_LOCATIONS: Record<keyof typeof TOTALS_RULES, string> = {
428
+ 'BR-CO-10': `${CII_TOTALS}/ram:LineTotalAmount`,
429
+ 'BR-CO-11': `${CII_TOTALS}/ram:AllowanceTotalAmount`,
430
+ 'BR-CO-12': `${CII_TOTALS}/ram:ChargeTotalAmount`,
431
+ 'BR-CO-13': `${CII_TOTALS}/ram:TaxBasisTotalAmount`,
432
+ 'BR-CO-14': `${CII_TOTALS}/ram:TaxTotalAmount`,
433
+ 'BR-CO-15': `${CII_TOTALS}/ram:GrandTotalAmount`,
434
+ 'BR-CO-16': `${CII_TOTALS}/ram:DuePayableAmount`,
435
+ 'BR-CO-17': '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax',
436
+ };
437
+
438
+ /** The package's code for a stated value the envelope computes differently */
439
+ export const STATED_VALUE_DIFFERS_CODE = 'EINVOICE-STATED-DIFFERS';
440
+
441
+ /**
442
+ * The stated values of a loaded document that the envelope's computation
443
+ * (`computeDocumentTotals`, what `totalNet`, `totalVat`, `totalGross` and the
444
+ * encoders give) does not reproduce: the totals BT-106, BT-109, BT-110,
445
+ * BT-112 and BT-115, each line's net amount (BT-131) and VAT category
446
+ * (BT-151), and each VAT breakdown's category, rate and amounts (BG-23). No
447
+ * EN 16931 rule is broken by such a difference, so it is the package's own
448
+ * finding: the envelope has no document or line allowances and charges and no
449
+ * price base quantity, and every line is standard rated or reverse charge.
450
+ * Neither value is changed.
451
+ * @param values The stated values
452
+ * @param totals The envelope's computation
453
+ * @param paidAmount The envelope's paid amount (BT-113)
454
+ */
455
+ export const findEnvelopeDifferences = (
456
+ values: IEInvoiceStatedValues,
457
+ totals: IDocumentTotals,
458
+ paidAmount: number | undefined,
459
+ ): IStatedValueFinding[] => {
460
+ const findings: IStatedValueFinding[] = [];
461
+ const differs = (businessTerm: string, field: string, stated: string | number, computed: string | number) =>
462
+ findings.push({
463
+ ruleId: STATED_VALUE_DIFFERS_CODE,
464
+ message: `The document states ${businessTerm} ${stated}; the envelope computes ${computed}. The stated value is statedValues.${field}; the envelope cannot express document or line allowances and charges (BG-20, BG-21, BG-27, BG-28), a price base quantity (BT-149) or a VAT category other than S and AE, and neither value is changed.`,
465
+ location: `statedValues.${field}`,
466
+ });
467
+ const compareAmount = (businessTerm: string, field: string, stated: number | undefined, computed: Decimal) => {
468
+ if (stated !== undefined && !decimal(stated).equals(computed)) {
469
+ differs(businessTerm, field, stated, computed.toString(totals.minorUnits));
470
+ }
471
+ };
472
+ compareAmount('BT-106', 'totals.lineTotal', values.totals.lineTotal, totals.lineTotal);
473
+ compareAmount('BT-109', 'totals.taxExclusiveTotal', values.totals.taxExclusiveTotal, totals.taxBasisTotal);
474
+ compareAmount('BT-110', 'totals.taxTotal', values.totals.taxTotal, totals.taxTotal);
475
+ compareAmount('BT-112', 'totals.taxInclusiveTotal', values.totals.taxInclusiveTotal, totals.grandTotal);
476
+ compareAmount('BT-115', 'totals.amountDue', values.totals.amountDue, totals.grandTotal.subtract(paidAmount ?? 0));
477
+ values.lines.forEach((line, index) => {
478
+ const computed = totals.lineNetAmounts[index];
479
+ if (computed !== undefined) {
480
+ compareAmount('BT-131', `lines[${index}].netAmount`, line.netAmount, computed);
481
+ }
482
+ const category = totals.lineVatCategories[index];
483
+ if (line.vatCategoryCode !== undefined && category !== undefined && line.vatCategoryCode !== category) {
484
+ differs('BT-151', `lines[${index}].vatCategoryCode`, line.vatCategoryCode, category);
485
+ }
486
+ });
487
+ values.vatBreakdown.forEach((group, index) => {
488
+ const computed = totals.vatGroups.find(
489
+ (candidate) => candidate.category === group.categoryCode && group.rate !== undefined && decimal(candidate.rate).equals(decimal(group.rate)),
490
+ );
491
+ if (!computed) {
492
+ differs('BG-23', `vatBreakdown[${index}]`, `${group.categoryCode ?? '?'} ${group.rate ?? ''}`.trim(), 'no breakdown of that category and rate');
493
+ return;
494
+ }
495
+ compareAmount('BT-116', `vatBreakdown[${index}].taxableAmount`, group.taxableAmount, computed.taxableAmount);
496
+ compareAmount('BT-117', `vatBreakdown[${index}].taxAmount`, group.taxAmount, computed.taxAmount);
497
+ });
498
+ return findings;
499
+ };