@fin.cx/einvoice 8.1.0 → 8.2.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +14 -1
- package/dist_ts/einvoice.js +17 -2
- package/dist_ts/formats/base/base.decoder.js +4 -3
- package/dist_ts/formats/base/base.encoder.d.ts +1 -1
- package/dist_ts/formats/cii/cii.decoder.d.ts +27 -1
- package/dist_ts/formats/cii/cii.decoder.js +61 -1
- package/dist_ts/formats/cii/cii.encoder.d.ts +14 -3
- package/dist_ts/formats/cii/cii.encoder.js +58 -6
- package/dist_ts/formats/cii/cii.types.d.ts +1 -0
- package/dist_ts/formats/cii/cii.types.js +3 -2
- package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +1 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +11 -8
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +2 -2
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +18 -15
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +1 -1
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +12 -10
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -1
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +26 -19
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +19 -1
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +51 -10
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +23 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +69 -14
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -1
- package/dist_ts/formats/ubl/ubl.encoder.d.ts +16 -3
- package/dist_ts/formats/ubl/ubl.encoder.js +75 -13
- package/dist_ts/formats/ubl/ubl.types.d.ts +6 -0
- package/dist_ts/formats/ubl/ubl.types.js +9 -3
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +46 -14
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +1 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +7 -12
- package/dist_ts/formats/utils/date.value.d.ts +28 -0
- package/dist_ts/formats/utils/date.value.js +50 -0
- package/dist_ts/formats/utils/document.typecode.d.ts +3 -3
- package/dist_ts/formats/utils/document.typecode.js +8 -4
- package/dist_ts/formats/utils/format.detector.js +6 -4
- package/dist_ts/formats/utils/payment.terms.d.ts +18 -0
- package/dist_ts/formats/utils/payment.terms.js +32 -0
- package/dist_ts/formats/utils/preceding.invoice.d.ts +26 -0
- package/dist_ts/formats/utils/preceding.invoice.js +52 -0
- package/dist_ts/formats/validation/integrated.validator.js +6 -3
- package/dist_ts/formats/validation/xrechnung.validator.js +6 -4
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +1 -1
- package/dist_ts/interfaces/common.d.ts +6 -2
- package/package.json +3 -3
- package/readme.md +77 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +18 -1
- package/ts/formats/base/base.decoder.ts +3 -2
- package/ts/formats/base/base.encoder.ts +1 -1
- package/ts/formats/cii/cii.decoder.ts +74 -1
- package/ts/formats/cii/cii.encoder.ts +62 -6
- package/ts/formats/cii/cii.types.ts +2 -1
- package/ts/formats/cii/facturx/facturx.decoder.ts +11 -7
- package/ts/formats/cii/facturx/facturx.encoder.ts +18 -14
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +12 -9
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +26 -18
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +58 -9
- package/ts/formats/ubl/generic/ubl.encoder.ts +76 -13
- package/ts/formats/ubl/ubl.decoder.ts +1 -1
- package/ts/formats/ubl/ubl.encoder.ts +77 -13
- package/ts/formats/ubl/ubl.types.ts +8 -2
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +56 -13
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +7 -12
- package/ts/formats/utils/date.value.ts +52 -0
- package/ts/formats/utils/document.typecode.ts +7 -3
- package/ts/formats/utils/format.detector.ts +5 -5
- package/ts/formats/utils/payment.terms.ts +37 -0
- package/ts/formats/utils/preceding.invoice.ts +74 -0
- package/ts/formats/validation/integrated.validator.ts +5 -2
- package/ts/formats/validation/xrechnung.validator.ts +5 -3
- package/ts/index.ts +3 -0
- package/ts/interfaces/common.ts +6 -1
|
@@ -6,9 +6,16 @@ import type {
|
|
|
6
6
|
TInvoiceDocument,
|
|
7
7
|
} from '../../../interfaces/common.js';
|
|
8
8
|
import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
|
|
9
|
-
import { business, finance } from '../../../plugins.js';
|
|
9
|
+
import { business, finance, xpath } from '../../../plugins.js';
|
|
10
10
|
import { EN16931Validator } from '../../validation/en16931.validator.js';
|
|
11
11
|
|
|
12
|
+
/** XPath selection with the ZUGFeRD v1 namespaces */
|
|
13
|
+
const zugferdV1Select = xpath.useNamespaces({
|
|
14
|
+
rsm: ZUGFERD_V1_NAMESPACES.RSM,
|
|
15
|
+
ram: ZUGFERD_V1_NAMESPACES.RAM,
|
|
16
|
+
udt: ZUGFERD_V1_NAMESPACES.UDT,
|
|
17
|
+
});
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Decoder for ZUGFeRD v1 invoice format
|
|
14
21
|
*/
|
|
@@ -28,6 +35,47 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
28
35
|
};
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
|
|
40
|
+
* missing or invalid one is refused with an `EInvoiceParsingError`.
|
|
41
|
+
*/
|
|
42
|
+
protected override extractIssueDate(): number {
|
|
43
|
+
const issueDatePath = '/rsm:CrossIndustryDocument/rsm:HeaderExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
|
|
44
|
+
return this.parseRequiredCIIDate(this.v1Text(issueDatePath), this.v1Text(`${issueDatePath}/@format`));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads the payment due date of the v1 header payment terms; undefined when
|
|
49
|
+
* the document states none.
|
|
50
|
+
*/
|
|
51
|
+
protected override extractDueDate(): number | undefined {
|
|
52
|
+
const dueDatePath =
|
|
53
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
|
|
54
|
+
const dueDate = this.v1Text(dueDatePath);
|
|
55
|
+
return dueDate ? this.parseRequiredCIIDate(dueDate, this.v1Text(`${dueDatePath}/@format`)) : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The trimmed text of the first node a v1 path selects, or '' */
|
|
59
|
+
private v1Text(path: string): string {
|
|
60
|
+
return String(zugferdV1Select(`string(${path})`, this.doc)).trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reads the actual delivery date of the v1 header delivery
|
|
65
|
+
* (`ram:ApplicableSupplyChainTradeDelivery`); undefined when the document
|
|
66
|
+
* does not state one.
|
|
67
|
+
*/
|
|
68
|
+
protected override extractDeliveryDate(): number | undefined {
|
|
69
|
+
const deliveryDatePath =
|
|
70
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString';
|
|
71
|
+
const deliveryDate = String(zugferdV1Select(`string(${deliveryDatePath})`, this.doc)).trim();
|
|
72
|
+
if (!deliveryDate) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const format = String(zugferdV1Select(`string(${deliveryDatePath}/@format)`, this.doc)).trim();
|
|
76
|
+
return this.parseRequiredCIIDate(deliveryDate, format);
|
|
77
|
+
}
|
|
78
|
+
|
|
31
79
|
/**
|
|
32
80
|
* Decodes a ZUGFeRD v1 credit note
|
|
33
81
|
* @returns Promise resolving to a TCreditNote object
|
|
@@ -37,7 +85,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
37
85
|
}
|
|
38
86
|
|
|
39
87
|
/**
|
|
40
|
-
* Decodes a ZUGFeRD v1 invoice document: an invoice, a debit note or a self-billed invoice
|
|
88
|
+
* Decodes a ZUGFeRD v1 invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
|
|
41
89
|
* @param accountingDocType The type the document type code stands for
|
|
42
90
|
* @returns Promise resolving to the invoice document
|
|
43
91
|
*/
|
|
@@ -59,9 +107,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
59
107
|
const invoiceId = this.getText('//ram:ID');
|
|
60
108
|
|
|
61
109
|
// Extract issue date
|
|
62
|
-
const
|
|
63
|
-
const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
|
|
64
|
-
const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
|
|
110
|
+
const issueDate = this.extractIssueDate();
|
|
65
111
|
|
|
66
112
|
// Extract seller information
|
|
67
113
|
const seller = this.extractParty('//ram:SellerTradeParty');
|
|
@@ -73,9 +119,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
73
119
|
const items = this.extractItems();
|
|
74
120
|
|
|
75
121
|
// Extract due date
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
|
122
|
+
// no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days, what this decoder returned before it read the v1 dates
|
|
123
|
+
const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
|
|
79
124
|
|
|
80
125
|
// Extract currency
|
|
81
126
|
const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
|
|
@@ -99,6 +144,9 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
99
144
|
notes = allNotes.slice(1); // Remove subject from notes
|
|
100
145
|
}
|
|
101
146
|
|
|
147
|
+
// Extract the actual delivery date, if stated
|
|
148
|
+
const deliveryDate = this.extractDeliveryDate();
|
|
149
|
+
|
|
102
150
|
// Check for reverse charge
|
|
103
151
|
const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
|
|
104
152
|
|
|
@@ -126,7 +174,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
126
174
|
reverseCharge: reverseCharge,
|
|
127
175
|
currency: currencyCode as finance.TCurrency,
|
|
128
176
|
notes: notes,
|
|
129
|
-
|
|
177
|
+
// the day of supply as the document states it (BT-72), never the issue date in its place
|
|
178
|
+
...(deliveryDate === undefined ? {} : { deliveryDate }),
|
|
130
179
|
objectActions: []
|
|
131
180
|
};
|
|
132
181
|
|
|
@@ -3,6 +3,9 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../int
|
|
|
3
3
|
import { UBLDocumentType } from '../ubl.types.js';
|
|
4
4
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
5
5
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
6
|
+
import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js';
|
|
7
|
+
import { getWritableDueDate } from '../../utils/date.value.js';
|
|
8
|
+
import { getPaymentTermsNote } from '../../utils/payment.terms.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* UBL Encoder implementation
|
|
@@ -24,13 +27,15 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
24
27
|
|
|
25
28
|
// Add credit note specific data
|
|
26
29
|
this.addCreditNoteSpecificData(doc, creditNote);
|
|
30
|
+
|
|
31
|
+
this.orderRootElements(doc);
|
|
27
32
|
|
|
28
33
|
// Serialize to string
|
|
29
34
|
return new XMLSerializer().serializeToString(doc);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
|
-
* Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
|
|
38
|
+
* Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
|
|
34
39
|
* @param invoice Invoice document to encode
|
|
35
40
|
* @returns UBL XML string
|
|
36
41
|
*/
|
|
@@ -44,6 +49,8 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
44
49
|
|
|
45
50
|
// Add invoice specific data
|
|
46
51
|
this.addInvoiceSpecificData(doc, invoice);
|
|
52
|
+
|
|
53
|
+
this.orderRootElements(doc);
|
|
47
54
|
|
|
48
55
|
// Serialize to string
|
|
49
56
|
return new XMLSerializer().serializeToString(doc);
|
|
@@ -71,16 +78,14 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
71
78
|
this.appendElement(doc, root, 'cbc:ID', invoice.id);
|
|
72
79
|
|
|
73
80
|
// Issue Date
|
|
74
|
-
this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date));
|
|
81
|
+
this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date, 'BT-2 invoice issue date'));
|
|
75
82
|
|
|
76
83
|
// Due Date - the CreditNote schema has no DueDate element; its payment due date lives in cac:PaymentMeans
|
|
77
84
|
if (documentType === UBLDocumentType.INVOICE) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
|
|
81
|
-
this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
|
|
85
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'ubl');
|
|
86
|
+
this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
|
|
82
87
|
}
|
|
83
|
-
// Document Type Code (380 invoice, 381 credit note, 383 debit note, 389 self-billed invoice; the element differs per schema)
|
|
88
|
+
// Document Type Code (380 invoice, 381 credit note, 383 debit note, 384 corrected invoice, 389 self-billed invoice; the element differs per schema)
|
|
84
89
|
this.appendElement(
|
|
85
90
|
doc,
|
|
86
91
|
root,
|
|
@@ -104,12 +109,21 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
104
109
|
|
|
105
110
|
// Document Currency Code
|
|
106
111
|
this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
|
|
112
|
+
|
|
113
|
+
// Invoicing period (BG-14), in schema order before the document references
|
|
114
|
+
this.addInvoicePeriod(doc, root, invoice);
|
|
115
|
+
|
|
116
|
+
// Preceding invoice references (BG-3)
|
|
117
|
+
this.addBillingReferences(doc, root, invoice);
|
|
107
118
|
|
|
108
119
|
// Add accounting supplier party (seller)
|
|
109
120
|
this.addParty(doc, root, 'cac:AccountingSupplierParty', invoice.from);
|
|
110
121
|
|
|
111
122
|
// Add accounting customer party (buyer)
|
|
112
123
|
this.addParty(doc, root, 'cac:AccountingCustomerParty', invoice.to);
|
|
124
|
+
|
|
125
|
+
// Actual delivery date (BT-72), in schema order after the parties
|
|
126
|
+
this.addDelivery(doc, root, invoice);
|
|
113
127
|
|
|
114
128
|
// Add payment terms
|
|
115
129
|
this.addPaymentTerms(doc, root, invoice);
|
|
@@ -127,6 +141,57 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
127
141
|
this.preserveMetadata(doc, root, invoice);
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Writes the invoicing period (BG-14: BT-73 and BT-74) the document states.
|
|
146
|
+
* @param doc XML document
|
|
147
|
+
* @param root Document root
|
|
148
|
+
* @param invoice Accounting document
|
|
149
|
+
*/
|
|
150
|
+
private addInvoicePeriod(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
151
|
+
if (!invoice.periodOfPerformance) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const invoicePeriod = doc.createElement('cac:InvoicePeriod');
|
|
155
|
+
this.appendElement(doc, invoicePeriod, 'cbc:StartDate', this.formatDate(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date'));
|
|
156
|
+
this.appendElement(doc, invoicePeriod, 'cbc:EndDate', this.formatDate(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date'));
|
|
157
|
+
root.appendChild(invoicePeriod);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Writes one preceding invoice reference (BG-3) per invoice the document
|
|
162
|
+
* states it corrects: the number (BT-25) and, when stated, the issue date (BT-26).
|
|
163
|
+
* @param doc XML document
|
|
164
|
+
* @param root Document root
|
|
165
|
+
* @param invoice Accounting document
|
|
166
|
+
*/
|
|
167
|
+
private addBillingReferences(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
168
|
+
for (const reference of getPrecedingInvoiceReferences(invoice)) {
|
|
169
|
+
const billingReference = doc.createElement('cac:BillingReference');
|
|
170
|
+
const invoiceDocumentReference = doc.createElement('cac:InvoiceDocumentReference');
|
|
171
|
+
this.appendElement(doc, invoiceDocumentReference, 'cbc:ID', reference.documentId);
|
|
172
|
+
if (reference.issueDate !== undefined) {
|
|
173
|
+
this.appendElement(doc, invoiceDocumentReference, 'cbc:IssueDate', this.formatDate(reference.issueDate, 'BT-26 preceding invoice issue date'));
|
|
174
|
+
}
|
|
175
|
+
billingReference.appendChild(invoiceDocumentReference);
|
|
176
|
+
root.appendChild(billingReference);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Writes the actual delivery date (BT-72) the document states.
|
|
182
|
+
* @param doc XML document
|
|
183
|
+
* @param root Document root
|
|
184
|
+
* @param invoice Accounting document
|
|
185
|
+
*/
|
|
186
|
+
private addDelivery(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
187
|
+
if (invoice.deliveryDate === undefined) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const delivery = doc.createElement('cac:Delivery');
|
|
191
|
+
this.appendElement(doc, delivery, 'cbc:ActualDeliveryDate', this.formatDate(invoice.deliveryDate, 'BT-72 actual delivery date'));
|
|
192
|
+
root.appendChild(delivery);
|
|
193
|
+
}
|
|
194
|
+
|
|
130
195
|
/**
|
|
131
196
|
* Adds credit note specific data to the document
|
|
132
197
|
* @param doc XML document
|
|
@@ -254,7 +319,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
254
319
|
parentElement.appendChild(paymentTermsNode);
|
|
255
320
|
|
|
256
321
|
// Payment terms note
|
|
257
|
-
this.appendElement(doc, paymentTermsNode, 'cbc:Note',
|
|
322
|
+
this.appendElement(doc, paymentTermsNode, 'cbc:Note', getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'ubl'));
|
|
258
323
|
|
|
259
324
|
// Add payment means if available
|
|
260
325
|
if (invoice.paymentOptions) {
|
|
@@ -280,11 +345,9 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
280
345
|
// Payment means code - default to credit transfer
|
|
281
346
|
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30');
|
|
282
347
|
|
|
283
|
-
// Payment due date
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
|
|
287
|
-
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
|
|
348
|
+
// Payment due date, counted from the issue date the document states
|
|
349
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'ubl');
|
|
350
|
+
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
|
|
288
351
|
|
|
289
352
|
// Add payment channel code if available
|
|
290
353
|
if (paymentOptions.description) {
|
|
@@ -89,7 +89,7 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
|
89
89
|
protected abstract decodeCreditNote(): Promise<TCreditNote>;
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
|
|
92
|
+
* Decodes a UBL invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
|
|
93
93
|
* @param accountingDocType The type the document type code stands for
|
|
94
94
|
* @returns Promise resolving to the invoice document
|
|
95
95
|
*/
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
import { BaseEncoder } from '../base/base.encoder.js';
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
|
+
import { getWritableDate } from '../utils/date.value.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The children of the document root in the order the UBL 2.1 schema requires
|
|
8
|
+
* them (UBL-Invoice-2.1.xsd and UBL-CreditNote-2.1.xsd, local names).
|
|
9
|
+
*/
|
|
10
|
+
const UBL_ROOT_SEQUENCES: Record<UBLDocumentType, readonly string[]> = {
|
|
11
|
+
[UBLDocumentType.INVOICE]: [
|
|
12
|
+
'UBLExtensions', 'UBLVersionID', 'CustomizationID', 'ProfileID', 'ProfileExecutionID', 'ID',
|
|
13
|
+
'CopyIndicator', 'UUID', 'IssueDate', 'IssueTime', 'DueDate', 'InvoiceTypeCode', 'Note',
|
|
14
|
+
'TaxPointDate', 'DocumentCurrencyCode', 'TaxCurrencyCode', 'PricingCurrencyCode',
|
|
15
|
+
'PaymentCurrencyCode', 'PaymentAlternativeCurrencyCode', 'AccountingCostCode', 'AccountingCost',
|
|
16
|
+
'LineCountNumeric', 'BuyerReference', 'InvoicePeriod', 'OrderReference', 'BillingReference',
|
|
17
|
+
'DespatchDocumentReference', 'ReceiptDocumentReference', 'StatementDocumentReference',
|
|
18
|
+
'OriginatorDocumentReference', 'ContractDocumentReference', 'AdditionalDocumentReference',
|
|
19
|
+
'ProjectReference', 'Signature', 'AccountingSupplierParty', 'AccountingCustomerParty',
|
|
20
|
+
'PayeeParty', 'BuyerCustomerParty', 'SellerSupplierParty', 'TaxRepresentativeParty', 'Delivery',
|
|
21
|
+
'DeliveryTerms', 'PaymentMeans', 'PaymentTerms', 'PrepaidPayment', 'AllowanceCharge',
|
|
22
|
+
'TaxExchangeRate', 'PricingExchangeRate', 'PaymentExchangeRate', 'PaymentAlternativeExchangeRate',
|
|
23
|
+
'TaxTotal', 'WithholdingTaxTotal', 'LegalMonetaryTotal', 'InvoiceLine',
|
|
24
|
+
],
|
|
25
|
+
[UBLDocumentType.CREDIT_NOTE]: [
|
|
26
|
+
'UBLExtensions', 'UBLVersionID', 'CustomizationID', 'ProfileID', 'ProfileExecutionID', 'ID',
|
|
27
|
+
'CopyIndicator', 'UUID', 'IssueDate', 'IssueTime', 'TaxPointDate', 'CreditNoteTypeCode', 'Note',
|
|
28
|
+
'DocumentCurrencyCode', 'TaxCurrencyCode', 'PricingCurrencyCode', 'PaymentCurrencyCode',
|
|
29
|
+
'PaymentAlternativeCurrencyCode', 'AccountingCostCode', 'AccountingCost', 'LineCountNumeric',
|
|
30
|
+
'BuyerReference', 'InvoicePeriod', 'DiscrepancyResponse', 'OrderReference', 'BillingReference',
|
|
31
|
+
'DespatchDocumentReference', 'ReceiptDocumentReference', 'ContractDocumentReference',
|
|
32
|
+
'AdditionalDocumentReference', 'StatementDocumentReference', 'OriginatorDocumentReference',
|
|
33
|
+
'Signature', 'AccountingSupplierParty', 'AccountingCustomerParty', 'PayeeParty',
|
|
34
|
+
'BuyerCustomerParty', 'SellerSupplierParty', 'TaxRepresentativeParty', 'Delivery',
|
|
35
|
+
'DeliveryTerms', 'PaymentMeans', 'PaymentTerms', 'TaxExchangeRate', 'PricingExchangeRate',
|
|
36
|
+
'PaymentExchangeRate', 'PaymentAlternativeExchangeRate', 'AllowanceCharge', 'TaxTotal',
|
|
37
|
+
'LegalMonetaryTotal', 'CreditNoteLine',
|
|
38
|
+
],
|
|
39
|
+
};
|
|
4
40
|
|
|
5
41
|
/**
|
|
6
42
|
* Base encoder for UBL-based invoice formats
|
|
@@ -27,7 +63,7 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
27
63
|
protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
|
|
28
64
|
|
|
29
65
|
/**
|
|
30
|
-
* Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
|
|
66
|
+
* Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
|
|
31
67
|
* @param invoice Invoice document to encode
|
|
32
68
|
* @returns UBL XML string
|
|
33
69
|
*/
|
|
@@ -47,20 +83,48 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
47
83
|
}
|
|
48
84
|
|
|
49
85
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
86
|
+
* Puts the children of the document root into the order the schema requires.
|
|
87
|
+
* Elements are added in several passes (the generic encoder, a customization,
|
|
88
|
+
* preserved metadata), each inserting where it can; this final pass makes the
|
|
89
|
+
* result follow the schema sequence. The sort is stable, so repeated elements
|
|
90
|
+
* (notes, references, lines) keep their order, and an element the sequence
|
|
91
|
+
* does not name stays behind the element it followed.
|
|
92
|
+
* @param doc XML document
|
|
53
93
|
*/
|
|
54
|
-
protected
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
protected orderRootElements(doc: Document): void {
|
|
95
|
+
const root = doc.documentElement;
|
|
96
|
+
const sequence = UBL_ROOT_SEQUENCES[root.localName as UBLDocumentType];
|
|
97
|
+
if (!sequence) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const children: Element[] = [];
|
|
101
|
+
for (let node = root.firstChild; node; node = node.nextSibling) {
|
|
102
|
+
if (node.nodeType === 1) {
|
|
103
|
+
children.push(node as Element);
|
|
104
|
+
}
|
|
58
105
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
let lastRank = -1;
|
|
107
|
+
const ranked = children.map((element, position) => {
|
|
108
|
+
// an element made with createElement('cac:X') carries its prefix in localName as well
|
|
109
|
+
const rank = sequence.indexOf(element.nodeName.slice(element.nodeName.indexOf(':') + 1));
|
|
110
|
+
lastRank = rank === -1 ? lastRank : rank;
|
|
111
|
+
return { element, rank: lastRank, position };
|
|
112
|
+
});
|
|
113
|
+
ranked.sort((a, b) => a.rank - b.rank || a.position - b.position);
|
|
114
|
+
for (const { element } of ranked) {
|
|
115
|
+
root.appendChild(element);
|
|
63
116
|
}
|
|
64
|
-
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Formats a date as an ISO string (YYYY-MM-DD, UTC). A value that is no
|
|
121
|
+
* valid timestamp is refused with an `EInvoiceFormatError` naming the field;
|
|
122
|
+
* no date is put in its place.
|
|
123
|
+
* @param timestamp Timestamp to format
|
|
124
|
+
* @param field The business term the date is written as
|
|
125
|
+
* @returns Formatted date string
|
|
126
|
+
*/
|
|
127
|
+
protected formatDate(timestamp: number, field: string): string {
|
|
128
|
+
return getWritableDate(timestamp, field, 'ubl').toISOString().split('T')[0];
|
|
65
129
|
}
|
|
66
130
|
}
|
|
@@ -17,8 +17,14 @@ export enum UBLDocumentType {
|
|
|
17
17
|
CREDIT_NOTE = 'CreditNote'
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// UBL customization IDs for different formats
|
|
20
|
+
// UBL customization IDs (specification identifier, BT-24) for different formats
|
|
21
21
|
export const UBL_CUSTOMIZATION_IDS = {
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* XRechnung 3.0 (CIUS), as the KoSIT XRechnung Schematron defines it (`XR-CIUS-ID` in
|
|
24
|
+
* common.sch: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_' and
|
|
25
|
+
* the version '3.0'). XRechnung 1.x and 2.x used the prefix
|
|
26
|
+
* 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_'.
|
|
27
|
+
*/
|
|
28
|
+
XRECHNUNG: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
|
|
23
29
|
PEPPOL_BIS: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0'
|
|
24
30
|
};
|
|
@@ -8,6 +8,10 @@ import type {
|
|
|
8
8
|
import { business, finance } from '../../../plugins.js';
|
|
9
9
|
import { UBLDocumentType } from '../ubl.types.js';
|
|
10
10
|
import { EN16931Validator } from '../../validation/en16931.validator.js';
|
|
11
|
+
import {
|
|
12
|
+
getPrecedingInvoiceFields,
|
|
13
|
+
type TPrecedingInvoiceReference,
|
|
14
|
+
} from '../../utils/preceding.invoice.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Decoder for XRechnung (UBL) format
|
|
@@ -23,7 +27,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
* Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
|
|
30
|
+
* Decodes a UBL invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
|
|
27
31
|
* @param accountingDocType The type the document type code stands for
|
|
28
32
|
* @returns Promise resolving to the invoice document
|
|
29
33
|
*/
|
|
@@ -56,14 +60,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
56
60
|
const issueDate = this.parseRequiredUblDate(issueDateText);
|
|
57
61
|
const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR';
|
|
58
62
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
// The payment due date (BT-9): cbc:DueDate on an Invoice, cac:PaymentMeans/cbc:PaymentDueDate
|
|
64
|
+
// on a CreditNote, cac:PaymentTerms/cbc:PaymentDueDate as a last resort; the payment term in
|
|
65
|
+
// days is the calendar days from the issue date to it.
|
|
66
|
+
// no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 30 days
|
|
67
|
+
let dueInDays = 30;
|
|
68
|
+
const dueDateText = (
|
|
69
|
+
this.getText('/*/cbc:DueDate', this.doc) ||
|
|
70
|
+
this.getText('/*/cac:PaymentMeans/cbc:PaymentDueDate', this.doc) ||
|
|
71
|
+
this.getText('/*/cac:PaymentTerms/cbc:PaymentDueDate', this.doc)
|
|
72
|
+
).trim();
|
|
62
73
|
if (dueDateText) {
|
|
63
|
-
|
|
64
|
-
const issueDateObj = new Date(issueDate);
|
|
65
|
-
const diffTime = Math.abs(dueDateObj.getTime() - issueDateObj.getTime());
|
|
66
|
-
dueInDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
74
|
+
dueInDays = Math.round((this.parseRequiredUblDate(dueDateText) - issueDate) / (1000 * 60 * 60 * 24));
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
// Extract items
|
|
@@ -168,10 +176,11 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
168
176
|
const paymentTermsNote = this.getText('//cac:PaymentTerms/cbc:Note', this.doc);
|
|
169
177
|
const discountPercent = this.getText('//cac:PaymentTerms/cbc:SettlementDiscountPercent', this.doc);
|
|
170
178
|
|
|
171
|
-
// Extract period
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
179
|
+
// Extract the document level period (BG-14) and delivery date (BT-72); a line has
|
|
180
|
+
// an invoice period (BG-26) and a delivery of its own, which are not the document's
|
|
181
|
+
const periodStart = this.getText('/*/cac:InvoicePeriod/cbc:StartDate', this.doc);
|
|
182
|
+
const periodEnd = this.getText('/*/cac:InvoicePeriod/cbc:EndDate', this.doc);
|
|
183
|
+
const deliveryDate = this.getText('/*/cac:Delivery/cbc:ActualDeliveryDate', this.doc);
|
|
175
184
|
|
|
176
185
|
// Extract notes (excluding PaymentTerms notes)
|
|
177
186
|
const allNotes: string[] = [];
|
|
@@ -255,7 +264,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
255
264
|
reverseCharge: false,
|
|
256
265
|
currency: currencyCode as finance.TCurrency,
|
|
257
266
|
notes: notes,
|
|
258
|
-
objectActions: []
|
|
267
|
+
objectActions: [],
|
|
268
|
+
// the day and the period of supply as the document states them (BT-72, BG-14)
|
|
269
|
+
...(deliveryDate ? { deliveryDate: this.parseRequiredUblDate(deliveryDate) } : {}),
|
|
270
|
+
...(periodStart && periodEnd
|
|
271
|
+
? {
|
|
272
|
+
periodOfPerformance: {
|
|
273
|
+
from: this.parseRequiredUblDate(periodStart),
|
|
274
|
+
to: this.parseRequiredUblDate(periodEnd),
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
278
|
+
...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
|
|
259
279
|
};
|
|
260
280
|
|
|
261
281
|
if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
|
|
@@ -281,6 +301,29 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
281
301
|
}
|
|
282
302
|
}
|
|
283
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Reads the preceding invoice references (BG-3): the number (BT-25) and,
|
|
306
|
+
* when stated, the issue date (BT-26) of each.
|
|
307
|
+
*/
|
|
308
|
+
private extractPrecedingInvoiceReferences(): TPrecedingInvoiceReference[] {
|
|
309
|
+
const referenceNodes = this.select(
|
|
310
|
+
'./cac:BillingReference/cac:InvoiceDocumentReference',
|
|
311
|
+
this.doc.documentElement,
|
|
312
|
+
);
|
|
313
|
+
const references: TPrecedingInvoiceReference[] = [];
|
|
314
|
+
for (const referenceNode of Array.isArray(referenceNodes) ? referenceNodes : []) {
|
|
315
|
+
const documentId = this.getText('./cbc:ID', referenceNode).trim();
|
|
316
|
+
if (!documentId) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const issueDate = this.getText('./cbc:IssueDate', referenceNode).trim();
|
|
320
|
+
references.push(
|
|
321
|
+
issueDate ? { documentId, issueDate: this.parseRequiredUblDate(issueDate) } : { documentId },
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return references;
|
|
325
|
+
}
|
|
326
|
+
|
|
284
327
|
/**
|
|
285
328
|
* Extracts party information from XML
|
|
286
329
|
* @param partyPath XPath to the party element
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { UBLEncoder } from '../generic/ubl.encoder.js';
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
|
|
3
3
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
4
|
+
import { UBL_CUSTOMIZATION_IDS } from '../ubl.types.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Encoder for XRechnung (UBL) format
|
|
@@ -25,7 +26,7 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Encodes an invoice document (invoice, debit note or self-billed invoice) into XRechnung XML
|
|
29
|
+
* Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into XRechnung XML
|
|
29
30
|
* @param invoice Invoice document to encode
|
|
30
31
|
* @returns XRechnung XML string
|
|
31
32
|
*/
|
|
@@ -52,10 +53,10 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
52
53
|
// Extract metadata if available
|
|
53
54
|
const metadata = (invoice as any).metadata?.extensions;
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
+
// Specification identifier (BT-24): XRechnung 3.0
|
|
56
57
|
const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
|
|
57
58
|
if (customizationId) {
|
|
58
|
-
customizationId.textContent =
|
|
59
|
+
customizationId.textContent = UBL_CUSTOMIZATION_IDS.XRECHNUNG;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// Add or update Buyer Reference (required for XRechnung)
|
|
@@ -73,15 +74,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
73
74
|
buyerRef.textContent = buyerReferenceValue;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
// Update payment terms to German
|
|
77
|
-
const paymentTermsNotes = root.getElementsByTagName('cac:PaymentTerms');
|
|
78
|
-
if (paymentTermsNotes.length > 0) {
|
|
79
|
-
const noteElement = paymentTermsNotes[0].getElementsByTagName('cbc:Note')[0];
|
|
80
|
-
if (noteElement && noteElement.textContent) {
|
|
81
|
-
noteElement.textContent = `Zahlung innerhalb von ${invoice.dueInDays || 30} Tagen`;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
77
|
// Add electronic address for parties if available
|
|
86
78
|
this.addElectronicAddressToParty(doc, 'cac:AccountingSupplierParty', invoice.from);
|
|
87
79
|
this.addElectronicAddressToParty(doc, 'cac:AccountingCustomerParty', invoice.to);
|
|
@@ -114,6 +106,9 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
114
106
|
|
|
115
107
|
// Enhance line items with metadata
|
|
116
108
|
this.enhanceLineItems(doc, invoice);
|
|
109
|
+
|
|
110
|
+
// the customizations above insert where they can; the schema order is restored last
|
|
111
|
+
this.orderRootElements(doc);
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
/**
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EInvoiceFormatError } from '../../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calendar dates (BT-2, BT-7, BT-9, BT-26, BT-72, BT-73, BT-74) carry no time
|
|
5
|
+
* of day and no time zone: UBL writes them as `xsd:date`, CII in format 102
|
|
6
|
+
* (`YYYYMMDD`). In the accounting document envelope such a day is the UTC
|
|
7
|
+
* midnight of that day, which is what the decoders return. The encoders write
|
|
8
|
+
* it with the UTC fields of the date and count days in UTC, so the day
|
|
9
|
+
* written does not depend on the time zone of the machine that writes it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The date an encoder is about to write, as a `Date`. A value that is no
|
|
14
|
+
* finite timestamp, or lies outside the range of a JavaScript date, is refused
|
|
15
|
+
* with an `EInvoiceFormatError` naming the field: the encoders never write a
|
|
16
|
+
* date the document does not state, such as today in place of a missing one.
|
|
17
|
+
* @param timestamp The timestamp (milliseconds since the epoch) to write
|
|
18
|
+
* @param field The business term it is written as, for example `BT-2 invoice issue date`
|
|
19
|
+
* @param targetFormat The format being written
|
|
20
|
+
*/
|
|
21
|
+
export const getWritableDate = (timestamp: unknown, field: string, targetFormat: string): Date => {
|
|
22
|
+
const date = typeof timestamp === 'number' && Number.isFinite(timestamp) ? new Date(timestamp) : undefined;
|
|
23
|
+
if (!date || Number.isNaN(date.getTime())) {
|
|
24
|
+
throw new EInvoiceFormatError(`${field} is no valid date: ${String(timestamp)}`, {
|
|
25
|
+
targetFormat,
|
|
26
|
+
unsupportedFeatures: [field],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return date;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The payment due date (BT-9) a document states: its issue date plus
|
|
34
|
+
* `dueInDays` calendar days. `0` is due on the issue date. A `dueInDays` that
|
|
35
|
+
* is no whole number of days is refused with an `EInvoiceFormatError`; no other
|
|
36
|
+
* term is put in its place.
|
|
37
|
+
* @param issueTimestamp The issue date (BT-2)
|
|
38
|
+
* @param dueInDays The payment term in days, as the document states it
|
|
39
|
+
* @param targetFormat The format being written
|
|
40
|
+
*/
|
|
41
|
+
export const getWritableDueDate = (issueTimestamp: unknown, dueInDays: unknown, targetFormat: string): Date => {
|
|
42
|
+
const dueDate = getWritableDate(issueTimestamp, 'BT-2 invoice issue date', targetFormat);
|
|
43
|
+
if (typeof dueInDays !== 'number' || !Number.isInteger(dueInDays)) {
|
|
44
|
+
throw new EInvoiceFormatError(`BT-9 payment due date: dueInDays is no whole number of days: ${String(dueInDays)}`, {
|
|
45
|
+
targetFormat,
|
|
46
|
+
unsupportedFeatures: ['BT-9 payment due date'],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// calendar days counted in UTC: a calendar day is its UTC midnight, whatever the server's zone
|
|
50
|
+
dueDate.setUTCDate(dueDate.getUTCDate() + dueInDays);
|
|
51
|
+
return dueDate;
|
|
52
|
+
};
|