@fin.cx/einvoice 8.3.0 → 9.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 (55) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +14 -0
  3. package/dist_ts/einvoice.js +17 -1
  4. package/dist_ts/formats/cii/cii.encoder.js +5 -1
  5. package/dist_ts/formats/cii/facturx/facturx.decoder.js +5 -1
  6. package/dist_ts/formats/cii/facturx/facturx.encoder.js +11 -4
  7. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +5 -1
  8. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +13 -8
  9. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +5 -1
  10. package/dist_ts/formats/semantic/semantic.adapter.js +10 -3
  11. package/dist_ts/formats/semantic/semantic.validator.js +17 -2
  12. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +0 -7
  13. package/dist_ts/formats/ubl/generic/ubl.encoder.js +20 -25
  14. package/dist_ts/formats/ubl/ubl.encoder.js +5 -1
  15. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +5 -1
  16. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -5
  17. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +1 -23
  18. package/dist_ts/formats/utils/country.code.d.ts +15 -0
  19. package/dist_ts/formats/utils/country.code.js +41 -0
  20. package/dist_ts/formats/utils/document.totals.d.ts +1 -3
  21. package/dist_ts/formats/utils/document.totals.js +1 -2
  22. package/dist_ts/formats/utils/paid.amount.d.ts +44 -0
  23. package/dist_ts/formats/utils/paid.amount.js +124 -0
  24. package/dist_ts/formats/utils/preceding.invoice.d.ts +12 -2
  25. package/dist_ts/formats/utils/preceding.invoice.js +22 -3
  26. package/dist_ts/formats/validation/codelist.validator.js +2 -2
  27. package/dist_ts/formats/validation/en16931.business-rules.validator.js +20 -18
  28. package/dist_ts/formats/validation/validation.types.js +23 -6
  29. package/dist_ts/interfaces/common.d.ts +1 -0
  30. package/dist_ts/interfaces/en16931-metadata.d.ts +0 -1
  31. package/package.json +2 -2
  32. package/readme.md +37 -0
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/einvoice.ts +18 -0
  35. package/ts/formats/cii/cii.encoder.ts +4 -0
  36. package/ts/formats/cii/facturx/facturx.decoder.ts +5 -0
  37. package/ts/formats/cii/facturx/facturx.encoder.ts +10 -3
  38. package/ts/formats/cii/zugferd/zugferd.decoder.ts +5 -0
  39. package/ts/formats/cii/zugferd/zugferd.encoder.ts +12 -7
  40. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +10 -0
  41. package/ts/formats/semantic/semantic.adapter.ts +9 -2
  42. package/ts/formats/semantic/semantic.validator.ts +17 -1
  43. package/ts/formats/ubl/generic/ubl.encoder.ts +25 -26
  44. package/ts/formats/ubl/ubl.encoder.ts +4 -0
  45. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +5 -0
  46. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +0 -23
  47. package/ts/formats/utils/country.code.ts +53 -0
  48. package/ts/formats/utils/document.totals.ts +1 -4
  49. package/ts/formats/utils/paid.amount.ts +160 -0
  50. package/ts/formats/utils/preceding.invoice.ts +23 -2
  51. package/ts/formats/validation/codelist.validator.ts +2 -2
  52. package/ts/formats/validation/en16931.business-rules.validator.ts +27 -25
  53. package/ts/formats/validation/validation.types.ts +22 -5
  54. package/ts/interfaces/common.ts +1 -0
  55. package/ts/interfaces/en16931-metadata.ts +0 -1
@@ -3,7 +3,8 @@
3
3
  * Validates invoices against EN16931 Business Terms and Business Groups
4
4
  */
5
5
 
6
- import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
6
+ import { computeDocumentTotals, findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
7
+ import { findPaymentProblem } from '../utils/paid.amount.js';
7
8
  import type { ValidationResult } from '../validation/validation.types.js';
8
9
  import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
9
10
  import type { EInvoice } from '../../einvoice.js';
@@ -57,6 +58,21 @@ export class SemanticModelValidator {
57
58
  return results;
58
59
  }
59
60
 
61
+ // The amount due (BT-115) follows from the paid amount; a paid amount that cannot be written
62
+ // (more decimals than the currency has, or advance payments that do not add up to it) is
63
+ // reported and the model rules are skipped, as for a line amount that is no number
64
+ const paymentProblem = findPaymentProblem(invoice, computeDocumentTotals(invoice));
65
+ if (paymentProblem) {
66
+ results.push({
67
+ ruleId: paymentProblem.ruleId,
68
+ source: 'SEMANTIC',
69
+ severity: 'error',
70
+ message: paymentProblem.message,
71
+ field: paymentProblem.field,
72
+ });
73
+ return results;
74
+ }
75
+
60
76
  // Convert to semantic model
61
77
  const model = this.adapter.toSemanticModel(invoice);
62
78
 
@@ -7,7 +7,9 @@ import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js'
7
7
  import { getWritableDueDate } from '../../utils/date.value.js';
8
8
  import { getPaymentTermsNote } from '../../utils/payment.terms.js';
9
9
  import { computeDocumentTotals } from '../../utils/document.totals.js';
10
+ import { getPaymentTotals } from '../../utils/paid.amount.js';
10
11
  import { toPlainDecimalString } from '../../utils/number.text.js';
12
+ import { resolveCountryCode } from '../../utils/country.code.js';
11
13
 
12
14
  /**
13
15
  * UBL Encoder implementation
@@ -253,17 +255,18 @@ export class UBLEncoder extends UBLBaseEncoder {
253
255
  this.appendElement(doc, postalAddressNode, 'cbc:PostalZone', party.address.postalCode);
254
256
  }
255
257
 
256
- // Country
257
- if (party.address.country || party.address.countryCode) {
258
- const countryNode = doc.createElement('cac:Country');
259
- postalAddressNode.appendChild(countryNode);
260
-
261
- const countryCode = party.address.countryCode || this.getCountryCode(party.address.country);
262
- this.appendElement(doc, countryNode, 'cbc:IdentificationCode', countryCode);
263
-
264
- if (party.address.country) {
265
- this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
266
- }
258
+ // Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
259
+ // derived from a name (BR-CL-14)
260
+ const countryNode = doc.createElement('cac:Country');
261
+ postalAddressNode.appendChild(countryNode);
262
+ this.appendElement(
263
+ doc,
264
+ countryNode,
265
+ 'cbc:IdentificationCode',
266
+ resolveCountryCode(party.address, elementName === 'cac:AccountingSupplierParty' ? 'seller' : 'buyer', 'ubl'),
267
+ );
268
+ if (party.address.country) {
269
+ this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
267
270
  }
268
271
 
269
272
  // Party tax scheme (VAT ID)
@@ -479,10 +482,19 @@ export class UBLEncoder extends UBLBaseEncoder {
479
482
  taxInclusiveAmountElement.textContent = amount(totals.grandTotal);
480
483
  legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement);
481
484
 
482
- // Payable amount
485
+ // Paid amount (BT-113), before the payable amount in the schema order
486
+ const payment = getPaymentTotals(invoice, totals);
487
+ if (payment.paidAmount) {
488
+ const prepaidAmountElement = doc.createElement('cbc:PrepaidAmount');
489
+ prepaidAmountElement.setAttribute('currencyID', invoice.currency);
490
+ prepaidAmountElement.textContent = amount(payment.paidAmount);
491
+ legalMonetaryTotalNode.appendChild(prepaidAmountElement);
492
+ }
493
+
494
+ // Payable amount (BT-115)
483
495
  const payableAmountElement = doc.createElement('cbc:PayableAmount');
484
496
  payableAmountElement.setAttribute('currencyID', invoice.currency);
485
- payableAmountElement.textContent = amount(totals.duePayable);
497
+ payableAmountElement.textContent = amount(payment.duePayable);
486
498
  legalMonetaryTotalNode.appendChild(payableAmountElement);
487
499
  }
488
500
 
@@ -576,19 +588,6 @@ export class UBLEncoder extends UBLBaseEncoder {
576
588
  parentElement.appendChild(element);
577
589
  }
578
590
 
579
- /**
580
- * Helper method to get country code from country name
581
- * Simple implementation that assumes the country name is already a code
582
- * @param countryName Country name
583
- * @returns Country code (2-letter ISO code)
584
- */
585
- private getCountryCode(countryName: string): string {
586
- // In a real implementation, this would map country names to ISO codes
587
- // For now, just return the first 2 characters or "XX" as fallback
588
- if (!countryName) return 'XX';
589
- return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
590
- }
591
-
592
591
  /**
593
592
  * Preserves metadata from invoice to enhance UBL XML output
594
593
  * @param doc XML document
@@ -3,6 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interf
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
  import { getWritableDate } from '../utils/date.value.js';
5
5
  import { assertVatCategoryWritable } from '../utils/vat.category.js';
6
+ import { computeDocumentTotals } from '../utils/document.totals.js';
7
+ import { getPaymentTotals } from '../utils/paid.amount.js';
6
8
 
7
9
  /**
8
10
  * The children of the document root in the order the UBL 2.1 schema requires
@@ -51,6 +53,8 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
51
53
  public async encode(invoice: TAccountingDoc): Promise<string> {
52
54
  // a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
53
55
  assertVatCategoryWritable(invoice, 'ubl');
56
+ // a paid amount the advance payments do not add up to, or with more decimals than the currency has, is refused
57
+ getPaymentTotals(invoice, computeDocumentTotals(invoice));
54
58
  // a credit note is a CreditNote document; an invoice, a debit note and a self-billed invoice are Invoice documents that differ in their type code
55
59
  if (invoice.accountingDocType === 'creditnote') {
56
60
  return this.encodeCreditNote(invoice);
@@ -12,6 +12,7 @@ import {
12
12
  getPrecedingInvoiceFields,
13
13
  type TPrecedingInvoiceReference,
14
14
  } from '../../utils/preceding.invoice.js';
15
+ import { readPaidAmount } from '../../utils/paid.amount.js';
15
16
 
16
17
  /**
17
18
  * Decoder for XRechnung (UBL) format
@@ -240,6 +241,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
240
241
  const hasPaymentInformation = Object.values(paymentInformation).some(value => Boolean(value));
241
242
  const hasDateInformation = Object.values(dateInformation).some(value => Boolean(value));
242
243
 
244
+ // the paid amount (BT-113); the amount due (BT-115) follows from it and the totals
245
+ const paidAmount = readPaidAmount(this.getText('/*/cac:LegalMonetaryTotal/cbc:PrepaidAmount'), 'ubl');
246
+
243
247
  // Create the common invoice data with metadata only when the source XML actually contains it.
244
248
  const invoiceData: any = {
245
249
  type: 'accounting-doc' as const,
@@ -278,6 +282,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
278
282
  }
279
283
  : {}),
280
284
  ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
285
+ ...(paidAmount === undefined ? {} : { paidAmount }),
281
286
  };
282
287
 
283
288
  if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
@@ -89,9 +89,6 @@ export class XRechnungEncoder extends UBLEncoder {
89
89
  }
90
90
  }
91
91
 
92
- // Add country code handling for German addresses
93
- this.fixGermanCountryCodes(doc);
94
-
95
92
  // Preserve business references from metadata
96
93
  this.addBusinessReferences(doc, metadata?.businessReferences);
97
94
 
@@ -207,26 +204,6 @@ export class XRechnungEncoder extends UBLEncoder {
207
204
  }
208
205
  }
209
206
 
210
- /**
211
- * Fixes German country codes in the document
212
- * @param doc XML document
213
- */
214
- private fixGermanCountryCodes(doc: Document): void {
215
- const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
216
- for (let i = 0; i < countryNodes.length; i++) {
217
- const node = countryNodes[i];
218
- if (node.textContent) {
219
- const text = node.textContent.toLowerCase();
220
- if (text === 'germany' || text === 'deutschland' || text === 'de') {
221
- node.textContent = 'DE';
222
- } else if (text.length > 2) {
223
- // Try to use first 2 characters as country code
224
- node.textContent = text.substring(0, 2).toUpperCase();
225
- }
226
- }
227
- }
228
- }
229
-
230
207
  /**
231
208
  * Adds business references from metadata to the document
232
209
  * @param doc XML document
@@ -0,0 +1,53 @@
1
+ import type { business } from '@tsclass/tsclass';
2
+ import { EInvoiceFormatError } from '../../errors.js';
3
+ import { CodeLists } from '../validation/validation.types.js';
4
+
5
+ /** The party whose address a country code is written for */
6
+ export type TCountryCodeParty = 'seller' | 'buyer';
7
+
8
+ const partyFields: Record<TCountryCodeParty, { path: string; term: string; presenceRule: string }> = {
9
+ // BR-09: the seller postal address (BG-5) states the seller country code (BT-40)
10
+ seller: { path: 'from.address', term: 'BT-40', presenceRule: 'BR-09' },
11
+ // BR-11: the buyer postal address (BG-8) states the buyer country code (BT-55)
12
+ buyer: { path: 'to.address', term: 'BT-55', presenceRule: 'BR-11' },
13
+ };
14
+
15
+ /**
16
+ * The country code an encoder writes for a party's address: an ISO 3166-1
17
+ * alpha-2 code EN 16931 accepts (BR-CL-14, `CodeLists.ISO3166`), taken from
18
+ * `countryCode`, or from `country` when that is already such a code; letter
19
+ * case does not matter. A code is never derived from a country name, so a
20
+ * name, an unknown code or a missing country is refused with an
21
+ * `EInvoiceFormatError` naming the field and the business term.
22
+ * @param address The party's address
23
+ * @param party Whether it is the seller's or the buyer's
24
+ * @param targetFormat The format being written
25
+ */
26
+ export const resolveCountryCode = (
27
+ address: Partial<business.IAddress> | undefined,
28
+ party: TCountryCodeParty,
29
+ targetFormat: string,
30
+ ): string => {
31
+ const { path, term, presenceRule } = partyFields[party];
32
+ const refuse = (rule: string, message: string): never => {
33
+ throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
34
+ };
35
+ const countryCode = (address?.countryCode ?? '').trim();
36
+ if (countryCode) {
37
+ if (!CodeLists.ISO3166.codes.has(countryCode.toUpperCase())) {
38
+ refuse('BR-CL-14', `the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is "${countryCode}"`);
39
+ }
40
+ return countryCode.toUpperCase();
41
+ }
42
+ const country = (address?.country ?? '').trim();
43
+ if (!country) {
44
+ refuse(presenceRule, `the ${party} postal address states a country code (${term}), ${path}.countryCode and ${path}.country are missing`);
45
+ }
46
+ if (!CodeLists.ISO3166.codes.has(country.toUpperCase())) {
47
+ refuse(
48
+ 'BR-CL-14',
49
+ `the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is missing and ${path}.country is "${country}", which is not one; a code is not derived from a country name`,
50
+ );
51
+ }
52
+ return country.toUpperCase();
53
+ };
@@ -96,10 +96,8 @@ export interface IDocumentTotals {
96
96
  vatGroups: IDocumentVatGroup[];
97
97
  /** Invoice total VAT amount (BT-110): the sum of the rounded VAT category tax amounts */
98
98
  taxTotal: Decimal;
99
- /** Invoice total amount with VAT (BT-112): BT-109 + BT-110 */
99
+ /** Invoice total amount with VAT (BT-112): BT-109 + BT-110; the paid amount and the amount due are `getPaymentTotals` */
100
100
  grandTotal: Decimal;
101
- /** Amount due for payment (BT-115): BT-112, as the envelope states no paid amount yet */
102
- duePayable: Decimal;
103
101
  }
104
102
 
105
103
  /**
@@ -168,6 +166,5 @@ export const computeDocumentTotals = (
168
166
  vatGroups,
169
167
  taxTotal,
170
168
  grandTotal,
171
- duePayable: grandTotal,
172
169
  };
173
170
  };
@@ -0,0 +1,160 @@
1
+ import type { TAccountingDoc, TAdvancePayment } from '../../interfaces/common.js';
2
+ import { EInvoiceFormatError, EInvoiceParsingError } from '../../errors.js';
3
+ import { Decimal } from './decimal.js';
4
+ import { toPlainDecimalString } from './number.text.js';
5
+ import type { IDocumentTotals } from './document.totals.js';
6
+
7
+ /** The paid amount (BT-113) and the amount due for payment (BT-115) an encoder writes */
8
+ export interface IPaymentTotals {
9
+ /** Paid amount (BT-113) as the document states it; undefined when it states none or zero, and nothing is written */
10
+ paidAmount?: Decimal;
11
+ /** Amount due for payment (BT-115): BT-112 − BT-113 (BR-CO-16; the envelope has no rounding amount BT-114); zero or negative when paid in full or overpaid */
12
+ duePayable: Decimal;
13
+ }
14
+
15
+ /** Why a paid amount cannot be written: the rule it breaks, the field and what is wrong */
16
+ export interface IPaymentProblem {
17
+ ruleId: string;
18
+ field: string;
19
+ message: string;
20
+ }
21
+
22
+ /** Carries a payment problem out of the computation */
23
+ class PaymentProblemSignal extends Error {
24
+ constructor(public readonly problem: IPaymentProblem) {
25
+ super(problem.message);
26
+ }
27
+ }
28
+
29
+ const refuse = (field: string, message: string, ruleId = 'PAID-AMOUNT'): never => {
30
+ throw new PaymentProblemSignal({ ruleId, field, message });
31
+ };
32
+
33
+ /** An amount as the document states it, at its full precision; a value that is no finite number is refused */
34
+ const statedDecimal = (value: unknown, field: string): Decimal => {
35
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
36
+ return refuse(field, `${field} is no finite number: ${String(value)}`);
37
+ }
38
+ return new Decimal(toPlainDecimalString(value));
39
+ };
40
+
41
+ /** The number of decimals of a stated amount */
42
+ const decimalsOf = (value: number): number => (toPlainDecimalString(value).split('.')[1] ?? '').length;
43
+
44
+ /** The document types that can be a final invoice (Endrechnung) and deduct advance payments */
45
+ const finalInvoiceTypes: TAccountingDoc['accountingDocType'][] = ['invoice', 'corrected-invoice', 'self-billed-invoice'];
46
+
47
+ /** What the paid amount and the amount due are computed from */
48
+ export type TPaymentSource = Pick<TAccountingDoc, 'accountingDocType' | 'paidAmount'> & {
49
+ advancePayments?: TAdvancePayment[];
50
+ };
51
+
52
+ /**
53
+ * The paid amount and the amount due of a document, as an encoder writes them.
54
+ * - When the document lists `advancePayments`, its `paidAmount` equals their
55
+ * gross sum, the `net` plus the `vat` of every group of every payment, to
56
+ * the last decimal (the rule `@tsclass/tsclass` sets). A document that
57
+ * breaks it, or lists advance payments without a paid amount, is refused;
58
+ * neither value is recomputed from the other.
59
+ * - The paid amount (BT-113) has at most the currency's decimals, two at most
60
+ * (BR-DEC-16); one with more is refused, not rounded.
61
+ * - A paid amount of zero is nothing paid, as an absent one: BT-113 is not
62
+ * written and BT-115 is BT-112.
63
+ * @param accountingDoc The document
64
+ * @param totals Its totals (`computeDocumentTotals`)
65
+ */
66
+ const computePaymentTotals = (accountingDoc: TPaymentSource, totals: IDocumentTotals): IPaymentTotals => {
67
+ const advancePayments = accountingDoc.advancePayments ?? [];
68
+ if (advancePayments.length > 0 && !finalInvoiceTypes.includes(accountingDoc.accountingDocType)) {
69
+ refuse(
70
+ 'advancePayments',
71
+ `advancePayments: a ${accountingDoc.accountingDocType} deducts no advance payments; only an invoice, a corrected invoice or a self-billed invoice is a final invoice (§ 14 Abs. 5 Satz 2 UStG)`,
72
+ );
73
+ }
74
+ const statedPaid = accountingDoc.paidAmount;
75
+ if (advancePayments.length > 0) {
76
+ const grossSum = Decimal.sum(
77
+ advancePayments.flatMap((payment, paymentIndex) =>
78
+ payment.vatGroups.flatMap((group, groupIndex) => [
79
+ statedDecimal(group.net, `advancePayments[${paymentIndex}].vatGroups[${groupIndex}].net`),
80
+ statedDecimal(group.vat, `advancePayments[${paymentIndex}].vatGroups[${groupIndex}].vat`),
81
+ ]),
82
+ ),
83
+ );
84
+ if (statedPaid === undefined || !statedDecimal(statedPaid, 'paidAmount').equals(grossSum)) {
85
+ refuse(
86
+ 'paidAmount',
87
+ `paidAmount (${statedPaid === undefined ? 'absent' : toPlainDecimalString(statedPaid)}) does not equal the gross sum of the advancePayments (${grossSum.toString()}), the net plus the vat of every group of every payment; neither value is recomputed from the other`,
88
+ );
89
+ }
90
+ }
91
+ if (statedPaid === undefined) {
92
+ return { duePayable: totals.grandTotal };
93
+ }
94
+ const paid = statedDecimal(statedPaid, 'paidAmount');
95
+ if (decimalsOf(statedPaid) > totals.minorUnits) {
96
+ refuse('paidAmount', `paidAmount: the paid amount (BT-113) has at most ${totals.minorUnits} decimals, it is ${toPlainDecimalString(statedPaid)}`, 'BR-DEC-16');
97
+ }
98
+ if (paid.isZero()) {
99
+ return { duePayable: totals.grandTotal };
100
+ }
101
+ return { paidAmount: paid, duePayable: totals.grandTotal.subtract(paid) };
102
+ };
103
+
104
+ /**
105
+ * The paid amount and the amount due as an encoder writes them (see
106
+ * `computePaymentTotals` above for the rules); a document that breaks one is
107
+ * refused with an `EInvoiceFormatError` naming the field.
108
+ * @param accountingDoc The document
109
+ * @param totals Its totals (`computeDocumentTotals`)
110
+ */
111
+ export const getPaymentTotals = (accountingDoc: TPaymentSource, totals: IDocumentTotals): IPaymentTotals => {
112
+ try {
113
+ return computePaymentTotals(accountingDoc, totals);
114
+ } catch (error) {
115
+ if (error instanceof PaymentProblemSignal) {
116
+ throw new EInvoiceFormatError(`${error.problem.ruleId}: ${error.problem.message}`, {
117
+ unsupportedFeatures: [error.problem.field],
118
+ });
119
+ }
120
+ throw error;
121
+ }
122
+ };
123
+
124
+ /**
125
+ * What keeps a document's paid amount from being written, for a validator to
126
+ * report instead of throwing; undefined when there is nothing
127
+ * @param accountingDoc The document
128
+ * @param totals Its totals (`computeDocumentTotals`)
129
+ */
130
+ export const findPaymentProblem = (accountingDoc: TPaymentSource, totals: IDocumentTotals): IPaymentProblem | undefined => {
131
+ try {
132
+ computePaymentTotals(accountingDoc, totals);
133
+ return undefined;
134
+ } catch (error) {
135
+ if (error instanceof PaymentProblemSignal) {
136
+ return error.problem;
137
+ }
138
+ throw error;
139
+ }
140
+ };
141
+
142
+ /**
143
+ * The paid amount (BT-113) a decoder reads: the number the document states,
144
+ * or undefined when it states none or zero (a stated zero is nothing paid, as
145
+ * an absent one). A paid amount that is no number is refused, since the amount
146
+ * due depends on it.
147
+ * @param text The text of the paid amount element, '' when absent
148
+ * @param format The format being read
149
+ */
150
+ export const readPaidAmount = (text: string, format: string): number | undefined => {
151
+ const trimmed = text.trim();
152
+ if (!trimmed) {
153
+ return undefined;
154
+ }
155
+ const value = Number(trimmed);
156
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(trimmed) || !Number.isFinite(value)) {
157
+ throw new EInvoiceParsingError(`The paid amount (BT-113) is no decimal number: ${trimmed}`, { format });
158
+ }
159
+ return value === 0 ? undefined : value;
160
+ };
@@ -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,
@@ -96,7 +96,7 @@ export class CodeListValidator {
96
96
  const buyerCountry = invoice.to?.address?.countryCode;
97
97
  if (buyerCountry && !CodeLists.ISO3166.codes.has(buyerCountry.toUpperCase())) {
98
98
  this.addError(
99
- 'BR-CL-15',
99
+ 'BR-CL-14',
100
100
  `Invalid buyer country code: ${buyerCountry}. Must be ISO 3166-1 alpha-2`,
101
101
  'EN16931',
102
102
  'to.address.countryCode',
@@ -110,7 +110,7 @@ export class CodeListValidator {
110
110
  const deliveryCountry = invoice.metadata?.deliveryAddress?.countryCode;
111
111
  if (deliveryCountry && !CodeLists.ISO3166.codes.has(deliveryCountry.toUpperCase())) {
112
112
  this.addError(
113
- 'BR-CL-16',
113
+ 'BR-CL-14',
114
114
  `Invalid delivery country code: ${deliveryCountry}. Must be ISO 3166-1 alpha-2`,
115
115
  'EN16931',
116
116
  'metadata.deliveryAddress.countryCode',
@@ -247,31 +247,33 @@ export class EN16931BusinessRulesValidator {
247
247
  );
248
248
  }
249
249
 
250
- // BR-CO-16: Amount due for payment = Invoice total with VAT - Paid amount
251
- const paidAmount = useDecimal
252
- ? new Decimal(invoice.metadata?.paidAmount || 0)
253
- : invoice.metadata?.paidAmount || 0;
254
- const expectedDueAmount = useDecimal
255
- ? (expectedGrossTotal as Decimal).subtract(paidAmount)
256
- : (expectedGrossTotal as number) - (paidAmount as number);
257
- const declaredDueAmount = useDecimal
258
- ? new Decimal(invoice.metadata?.amountDue || (useDecimal ? (expectedGrossTotal as Decimal).toNumber() : expectedGrossTotal))
259
- : invoice.metadata?.amountDue || expectedGrossTotal;
260
-
261
- const isDueEqual = useDecimal
262
- ? this.decimalCalculator!.areEqual(expectedDueAmount, declaredDueAmount)
263
- : this.currencyCalculator
264
- ? this.currencyCalculator.areEqual(expectedDueAmount as number, declaredDueAmount as number)
265
- : Math.abs((expectedDueAmount as number) - (declaredDueAmount as number)) < 0.01;
266
-
267
- if (!isDueEqual) {
268
- this.addError(
269
- 'BR-CO-16',
270
- `Amount due (${useDecimal ? (declaredDueAmount as Decimal).toFixed(2) : (declaredDueAmount as number).toFixed(2)}) does not match calculation (${useDecimal ? (expectedDueAmount as Decimal).toFixed(2) : (expectedDueAmount as number).toFixed(2)})`,
271
- 'amountDue',
272
- useDecimal ? (declaredDueAmount as Decimal).toNumber() : declaredDueAmount as number,
273
- useDecimal ? (expectedDueAmount as Decimal).toNumber() : expectedDueAmount as number
274
- );
250
+ // BR-CO-16: Amount due for payment = Invoice total with VAT - Paid amount. The envelope's
251
+ // paid amount (BT-113) is the one the encoders write; the amount due (BT-115) is compared when
252
+ // a declared one is at hand (`metadata.amountDue`), since the envelope derives it otherwise
253
+ const paidAmountValue = invoice.paidAmount ?? 0;
254
+ const declaredDueValue = invoice.metadata?.amountDue;
255
+ if (declaredDueValue !== undefined) {
256
+ const paidAmount = useDecimal ? new Decimal(paidAmountValue) : paidAmountValue;
257
+ const expectedDueAmount = useDecimal
258
+ ? (expectedGrossTotal as Decimal).subtract(paidAmount)
259
+ : (expectedGrossTotal as number) - (paidAmount as number);
260
+ const declaredDueAmount = useDecimal ? new Decimal(declaredDueValue) : declaredDueValue;
261
+
262
+ const isDueEqual = useDecimal
263
+ ? this.decimalCalculator!.areEqual(expectedDueAmount, declaredDueAmount)
264
+ : this.currencyCalculator
265
+ ? this.currencyCalculator.areEqual(expectedDueAmount as number, declaredDueAmount as number)
266
+ : Math.abs((expectedDueAmount as number) - (declaredDueAmount as number)) < 0.01;
267
+
268
+ if (!isDueEqual) {
269
+ this.addError(
270
+ 'BR-CO-16',
271
+ `Amount due (${useDecimal ? (declaredDueAmount as Decimal).toFixed(2) : (declaredDueAmount as number).toFixed(2)}) does not match calculation (${useDecimal ? (expectedDueAmount as Decimal).toFixed(2) : (expectedDueAmount as number).toFixed(2)})`,
272
+ 'amountDue',
273
+ useDecimal ? (declaredDueAmount as Decimal).toNumber() : declaredDueAmount as number,
274
+ useDecimal ? (expectedDueAmount as Decimal).toNumber() : expectedDueAmount as number
275
+ );
276
+ }
275
277
  }
276
278
  }
277
279
 
@@ -96,13 +96,30 @@ export const CodeLists = {
96
96
  ])
97
97
  },
98
98
 
99
- // ISO 3166-1 alpha-2 Country codes
99
+ // ISO 3166-1 alpha-2 country codes, as EN 16931 accepts them (BR-CL-14, BR-CL-15): the code list
100
+ // of the CEN/TC 434 validation artefacts 1.3.16 for UBL, which adds 1A (Kosovo) and XI (Northern
101
+ // Ireland) to the codes ISO 3166-1 assigns
100
102
  ISO3166: {
101
- version: '2020',
103
+ version: 'EN 16931 validation artefacts 1.3.16',
102
104
  codes: new Set([
103
- 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH', 'GB', 'IE', 'PT', 'GR',
104
- 'SE', 'NO', 'DK', 'FI', 'PL', 'CZ', 'HU', 'RO', 'BG', 'HR', 'SI', 'SK',
105
- 'LT', 'LV', 'EE', 'LU', 'MT', 'CY', 'US', 'CA', 'AU', 'NZ', 'JP', 'CN'
105
+ '1A', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU',
106
+ 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM',
107
+ 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF',
108
+ 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY',
109
+ 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET',
110
+ 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI',
111
+ 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN',
112
+ 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
113
+ 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ',
114
+ 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD',
115
+ 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT',
116
+ 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO',
117
+ 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN',
118
+ 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC',
119
+ 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST',
120
+ 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
121
+ 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC',
122
+ 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW',
106
123
  ])
107
124
  },
108
125
 
@@ -93,6 +93,7 @@ export type { TCorrectedInvoice } from '@tsclass/tsclass/dist_ts/finance/index.j
93
93
  export type { TCorrectedInvoiceReference } from '@tsclass/tsclass/dist_ts/finance/index.js';
94
94
  export type { TInvoiceCorrection } from '@tsclass/tsclass/dist_ts/finance/index.js';
95
95
  export type { TRelatedDocument } from '@tsclass/tsclass/dist_ts/finance/index.js';
96
+ export type { TAdvancePayment } from '@tsclass/tsclass/dist_ts/finance/index.js';
96
97
  export type { TCreditNote } from '@tsclass/tsclass/dist_ts/finance/index.js';
97
98
  export type { TDebitNote } from '@tsclass/tsclass/dist_ts/finance/index.js';
98
99
  export type { TSelfBilledInvoice } from '@tsclass/tsclass/dist_ts/finance/index.js';
@@ -20,7 +20,6 @@ export interface IEInvoiceMetadata {
20
20
  vatAccountingCurrency?: string; // BT-6
21
21
  documentTypeCode?: string; // BT-3
22
22
  paymentMeansCode?: string; // BT-81
23
- paidAmount?: number; // BT-113
24
23
  amountDue?: number; // BT-115
25
24
 
26
25
  // Tax identifiers