@fin.cx/einvoice 5.0.2 → 5.1.2
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 +4 -8
- package/dist_ts/einvoice.js +46 -7
- package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
- package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
- package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
- package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
- package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
- package/dist_ts/formats/semantic/semantic.validator.js +596 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
- package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
- package/dist_ts/formats/utils/currency.utils.js +250 -0
- package/dist_ts/formats/utils/decimal.d.ts +151 -0
- package/dist_ts/formats/utils/decimal.js +435 -0
- package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
- package/dist_ts/formats/validation/codelist.validator.js +188 -0
- package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
- package/dist_ts/formats/validation/conformance.harness.js +471 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
- package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
- package/dist_ts/formats/validation/facturx.validator.js +522 -0
- package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
- package/dist_ts/formats/validation/integrated.validator.js +311 -0
- package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
- package/dist_ts/formats/validation/peppol.validator.js +529 -0
- package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
- package/dist_ts/formats/validation/schematron.downloader.js +254 -0
- package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
- package/dist_ts/formats/validation/schematron.integration.js +230 -0
- package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
- package/dist_ts/formats/validation/schematron.validator.js +288 -0
- package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
- package/dist_ts/formats/validation/schematron.worker.js +185 -0
- package/dist_ts/formats/validation/validation.types.d.ts +163 -0
- package/dist_ts/formats/validation/validation.types.js +181 -0
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
- package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
- package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
- package/dist_ts/interfaces/en16931-metadata.js +6 -0
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.d.ts +6 -0
- package/dist_ts_install/download-schematron.js +62 -0
- package/dist_ts_install/download-test-samples.d.ts +19 -0
- package/dist_ts_install/download-test-samples.js +169 -0
- package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
- package/dist_ts_install/download-xrechnung-rules.js +146 -0
- package/dist_ts_install/index.d.ts +8 -0
- package/dist_ts_install/index.js +246 -0
- package/package.json +13 -8
- package/readme.md +255 -810
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +63 -14
- package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
- package/ts/formats/semantic/bt-bg.model.ts +524 -0
- package/ts/formats/semantic/semantic.adapter.ts +600 -0
- package/ts/formats/semantic/semantic.validator.ts +654 -0
- package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
- package/ts/formats/utils/currency.utils.ts +299 -0
- package/ts/formats/utils/decimal.ts +509 -0
- package/ts/formats/validation/codelist.validator.ts +317 -0
- package/ts/formats/validation/conformance.harness.ts +591 -0
- package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
- package/ts/formats/validation/facturx.validator.ts +579 -0
- package/ts/formats/validation/integrated.validator.ts +405 -0
- package/ts/formats/validation/peppol.validator.ts +589 -0
- package/ts/formats/validation/schematron.downloader.ts +304 -0
- package/ts/formats/validation/schematron.integration.ts +285 -0
- package/ts/formats/validation/schematron.validator.ts +348 -0
- package/ts/formats/validation/schematron.worker.ts +221 -0
- package/ts/formats/validation/validation.types.ts +274 -0
- package/ts/formats/validation/vat-categories.validator.ts +845 -0
- package/ts/formats/validation/xrechnung.validator.ts +494 -0
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +104 -0
- package/ts/plugins.ts +8 -0
- package/ts/tspublish.json +3 -0
- package/dist_ts/classes.decoder.d.ts +0 -40
- package/dist_ts/classes.decoder.js +0 -320
- package/dist_ts/classes.encoder.d.ts +0 -51
- package/dist_ts/classes.encoder.js +0 -293
- package/dist_ts/classes.xinvoice.d.ts +0 -136
- package/dist_ts/classes.xinvoice.js +0 -352
- package/dist_ts/formats/base.decoder.d.ts +0 -19
- package/dist_ts/formats/base.decoder.js +0 -130
- package/dist_ts/formats/base.validator.d.ts +0 -44
- package/dist_ts/formats/base.validator.js +0 -39
- package/dist_ts/formats/decoder.factory.d.ts +0 -15
- package/dist_ts/formats/decoder.factory.js +0 -46
- package/dist_ts/formats/factorx.decoder.d.ts +0 -18
- package/dist_ts/formats/factorx.decoder.js +0 -178
- package/dist_ts/formats/factorx.encoder.d.ts +0 -51
- package/dist_ts/formats/factorx.encoder.js +0 -293
- package/dist_ts/formats/facturx.decoder.d.ts +0 -18
- package/dist_ts/formats/facturx.decoder.js +0 -210
- package/dist_ts/formats/facturx.encoder.d.ts +0 -51
- package/dist_ts/formats/facturx.encoder.js +0 -293
- package/dist_ts/formats/facturx.validator.d.ts +0 -67
- package/dist_ts/formats/facturx.validator.js +0 -266
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
- package/dist_ts/formats/ubl.validator.d.ts +0 -82
- package/dist_ts/formats/ubl.validator.js +0 -306
- package/dist_ts/formats/validator.factory.d.ts +0 -18
- package/dist_ts/formats/validator.factory.js +0 -78
- package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
- package/dist_ts/formats/xinvoice.decoder.js +0 -332
- package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
- package/dist_ts/formats/xinvoice.encoder.js +0 -282
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency Calculator using Decimal Arithmetic
|
|
3
|
+
* EN16931-compliant monetary calculations with exact precision
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Decimal, decimal, RoundingMode } from './decimal.js';
|
|
7
|
+
import type { TCurrency } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
8
|
+
import { getCurrencyMinorUnits } from './currency.utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Currency-aware calculator using decimal arithmetic for EN16931 compliance
|
|
12
|
+
*/
|
|
13
|
+
export class DecimalCurrencyCalculator {
|
|
14
|
+
private readonly currency: TCurrency;
|
|
15
|
+
private readonly minorUnits: number;
|
|
16
|
+
private readonly roundingMode: RoundingMode;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
currency: TCurrency,
|
|
20
|
+
roundingMode: RoundingMode = 'HALF_UP'
|
|
21
|
+
) {
|
|
22
|
+
this.currency = currency;
|
|
23
|
+
this.minorUnits = getCurrencyMinorUnits(currency);
|
|
24
|
+
this.roundingMode = roundingMode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Round a decimal value according to currency rules
|
|
29
|
+
*/
|
|
30
|
+
round(value: Decimal | number | string): Decimal {
|
|
31
|
+
const decimalValue = value instanceof Decimal ? value : new Decimal(value);
|
|
32
|
+
return decimalValue.round(this.minorUnits, this.roundingMode);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate line net amount: (quantity × unitPrice) - discount
|
|
37
|
+
*/
|
|
38
|
+
calculateLineNet(
|
|
39
|
+
quantity: Decimal | number | string,
|
|
40
|
+
unitPrice: Decimal | number | string,
|
|
41
|
+
discount: Decimal | number | string = '0'
|
|
42
|
+
): Decimal {
|
|
43
|
+
const qty = quantity instanceof Decimal ? quantity : new Decimal(quantity);
|
|
44
|
+
const price = unitPrice instanceof Decimal ? unitPrice : new Decimal(unitPrice);
|
|
45
|
+
const disc = discount instanceof Decimal ? discount : new Decimal(discount);
|
|
46
|
+
|
|
47
|
+
const gross = qty.multiply(price);
|
|
48
|
+
const net = gross.subtract(disc);
|
|
49
|
+
|
|
50
|
+
return this.round(net);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate VAT amount from base and rate
|
|
55
|
+
*/
|
|
56
|
+
calculateVAT(
|
|
57
|
+
baseAmount: Decimal | number | string,
|
|
58
|
+
vatRate: Decimal | number | string
|
|
59
|
+
): Decimal {
|
|
60
|
+
const base = baseAmount instanceof Decimal ? baseAmount : new Decimal(baseAmount);
|
|
61
|
+
const rate = vatRate instanceof Decimal ? vatRate : new Decimal(vatRate);
|
|
62
|
+
|
|
63
|
+
const vat = base.percentage(rate);
|
|
64
|
+
return this.round(vat);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate total with VAT
|
|
69
|
+
*/
|
|
70
|
+
calculateGrossAmount(
|
|
71
|
+
netAmount: Decimal | number | string,
|
|
72
|
+
vatAmount: Decimal | number | string
|
|
73
|
+
): Decimal {
|
|
74
|
+
const net = netAmount instanceof Decimal ? netAmount : new Decimal(netAmount);
|
|
75
|
+
const vat = vatAmount instanceof Decimal ? vatAmount : new Decimal(vatAmount);
|
|
76
|
+
|
|
77
|
+
return this.round(net.add(vat));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calculate sum of line items
|
|
82
|
+
*/
|
|
83
|
+
sumLineItems(items: Array<{
|
|
84
|
+
quantity: Decimal | number | string;
|
|
85
|
+
unitPrice: Decimal | number | string;
|
|
86
|
+
discount?: Decimal | number | string;
|
|
87
|
+
}>): Decimal {
|
|
88
|
+
let total = Decimal.ZERO;
|
|
89
|
+
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const lineNet = this.calculateLineNet(
|
|
92
|
+
item.quantity,
|
|
93
|
+
item.unitPrice,
|
|
94
|
+
item.discount
|
|
95
|
+
);
|
|
96
|
+
total = total.add(lineNet);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.round(total);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculate VAT breakdown by rate
|
|
104
|
+
*/
|
|
105
|
+
calculateVATBreakdown(items: Array<{
|
|
106
|
+
netAmount: Decimal | number | string;
|
|
107
|
+
vatRate: Decimal | number | string;
|
|
108
|
+
}>): Array<{
|
|
109
|
+
rate: Decimal;
|
|
110
|
+
baseAmount: Decimal;
|
|
111
|
+
vatAmount: Decimal;
|
|
112
|
+
}> {
|
|
113
|
+
// Group by VAT rate
|
|
114
|
+
const groups = new Map<string, {
|
|
115
|
+
rate: Decimal;
|
|
116
|
+
baseAmount: Decimal;
|
|
117
|
+
}>();
|
|
118
|
+
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
const net = item.netAmount instanceof Decimal ? item.netAmount : new Decimal(item.netAmount);
|
|
121
|
+
const rate = item.vatRate instanceof Decimal ? item.vatRate : new Decimal(item.vatRate);
|
|
122
|
+
const rateKey = rate.toString();
|
|
123
|
+
|
|
124
|
+
if (groups.has(rateKey)) {
|
|
125
|
+
const group = groups.get(rateKey)!;
|
|
126
|
+
group.baseAmount = group.baseAmount.add(net);
|
|
127
|
+
} else {
|
|
128
|
+
groups.set(rateKey, {
|
|
129
|
+
rate,
|
|
130
|
+
baseAmount: net
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Calculate VAT for each group
|
|
136
|
+
const breakdown: Array<{
|
|
137
|
+
rate: Decimal;
|
|
138
|
+
baseAmount: Decimal;
|
|
139
|
+
vatAmount: Decimal;
|
|
140
|
+
}> = [];
|
|
141
|
+
|
|
142
|
+
for (const group of groups.values()) {
|
|
143
|
+
breakdown.push({
|
|
144
|
+
rate: group.rate,
|
|
145
|
+
baseAmount: this.round(group.baseAmount),
|
|
146
|
+
vatAmount: this.calculateVAT(group.baseAmount, group.rate)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return breakdown;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if two amounts are equal within currency precision
|
|
155
|
+
*/
|
|
156
|
+
areEqual(
|
|
157
|
+
amount1: Decimal | number | string,
|
|
158
|
+
amount2: Decimal | number | string
|
|
159
|
+
): boolean {
|
|
160
|
+
const a1 = amount1 instanceof Decimal ? amount1 : new Decimal(amount1);
|
|
161
|
+
const a2 = amount2 instanceof Decimal ? amount2 : new Decimal(amount2);
|
|
162
|
+
|
|
163
|
+
// Round both to currency precision before comparing
|
|
164
|
+
const rounded1 = this.round(a1);
|
|
165
|
+
const rounded2 = this.round(a2);
|
|
166
|
+
|
|
167
|
+
return rounded1.equals(rounded2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Calculate payment terms discount
|
|
172
|
+
*/
|
|
173
|
+
calculatePaymentDiscount(
|
|
174
|
+
amount: Decimal | number | string,
|
|
175
|
+
discountRate: Decimal | number | string
|
|
176
|
+
): Decimal {
|
|
177
|
+
const amt = amount instanceof Decimal ? amount : new Decimal(amount);
|
|
178
|
+
const rate = discountRate instanceof Decimal ? discountRate : new Decimal(discountRate);
|
|
179
|
+
|
|
180
|
+
const discount = amt.percentage(rate);
|
|
181
|
+
return this.round(discount);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Distribute a total amount across items proportionally
|
|
186
|
+
*/
|
|
187
|
+
distributeAmount(
|
|
188
|
+
totalToDistribute: Decimal | number | string,
|
|
189
|
+
items: Array<{ value: Decimal | number | string }>
|
|
190
|
+
): Decimal[] {
|
|
191
|
+
const total = totalToDistribute instanceof Decimal ? totalToDistribute : new Decimal(totalToDistribute);
|
|
192
|
+
|
|
193
|
+
// Calculate sum of all item values
|
|
194
|
+
const itemSum = items.reduce((sum, item) => {
|
|
195
|
+
const value = item.value instanceof Decimal ? item.value : new Decimal(item.value);
|
|
196
|
+
return sum.add(value);
|
|
197
|
+
}, Decimal.ZERO);
|
|
198
|
+
|
|
199
|
+
if (itemSum.isZero()) {
|
|
200
|
+
// Can't distribute if sum is zero
|
|
201
|
+
return items.map(() => Decimal.ZERO);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const distributed: Decimal[] = [];
|
|
205
|
+
let distributedSum = Decimal.ZERO;
|
|
206
|
+
|
|
207
|
+
// Distribute proportionally
|
|
208
|
+
for (let i = 0; i < items.length; i++) {
|
|
209
|
+
const itemValue = items[i].value instanceof Decimal ? items[i].value : new Decimal(items[i].value);
|
|
210
|
+
|
|
211
|
+
if (i === items.length - 1) {
|
|
212
|
+
// Last item gets the remainder to avoid rounding errors
|
|
213
|
+
distributed.push(total.subtract(distributedSum));
|
|
214
|
+
} else {
|
|
215
|
+
const itemDecimal = itemValue instanceof Decimal ? itemValue : new Decimal(itemValue);
|
|
216
|
+
const proportion = itemDecimal.divide(itemSum);
|
|
217
|
+
const distributedAmount = this.round(total.multiply(proportion));
|
|
218
|
+
distributed.push(distributedAmount);
|
|
219
|
+
distributedSum = distributedSum.add(distributedAmount);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return distributed;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Calculate compound amount (e.g., for multiple charges/allowances)
|
|
228
|
+
*/
|
|
229
|
+
calculateCompoundAmount(
|
|
230
|
+
baseAmount: Decimal | number | string,
|
|
231
|
+
adjustments: Array<{
|
|
232
|
+
type: 'charge' | 'allowance';
|
|
233
|
+
value: Decimal | number | string;
|
|
234
|
+
isPercentage?: boolean;
|
|
235
|
+
}>
|
|
236
|
+
): Decimal {
|
|
237
|
+
let result = baseAmount instanceof Decimal ? baseAmount : new Decimal(baseAmount);
|
|
238
|
+
|
|
239
|
+
for (const adjustment of adjustments) {
|
|
240
|
+
const value = adjustment.value instanceof Decimal ? adjustment.value : new Decimal(adjustment.value);
|
|
241
|
+
|
|
242
|
+
let adjustmentAmount: Decimal;
|
|
243
|
+
if (adjustment.isPercentage) {
|
|
244
|
+
adjustmentAmount = result.percentage(value);
|
|
245
|
+
} else {
|
|
246
|
+
adjustmentAmount = value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (adjustment.type === 'charge') {
|
|
250
|
+
result = result.add(adjustmentAmount);
|
|
251
|
+
} else {
|
|
252
|
+
result = result.subtract(adjustmentAmount);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return this.round(result);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Validate monetary calculation according to EN16931 rules
|
|
261
|
+
*/
|
|
262
|
+
validateCalculation(
|
|
263
|
+
expected: Decimal | number | string,
|
|
264
|
+
calculated: Decimal | number | string,
|
|
265
|
+
ruleName: string
|
|
266
|
+
): {
|
|
267
|
+
valid: boolean;
|
|
268
|
+
expected: string;
|
|
269
|
+
calculated: string;
|
|
270
|
+
difference?: string;
|
|
271
|
+
rule: string;
|
|
272
|
+
} {
|
|
273
|
+
const exp = expected instanceof Decimal ? expected : new Decimal(expected);
|
|
274
|
+
const calc = calculated instanceof Decimal ? calculated : new Decimal(calculated);
|
|
275
|
+
|
|
276
|
+
const roundedExp = this.round(exp);
|
|
277
|
+
const roundedCalc = this.round(calc);
|
|
278
|
+
|
|
279
|
+
const valid = roundedExp.equals(roundedCalc);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
valid,
|
|
283
|
+
expected: roundedExp.toFixed(this.minorUnits),
|
|
284
|
+
calculated: roundedCalc.toFixed(this.minorUnits),
|
|
285
|
+
difference: valid ? undefined : roundedExp.subtract(roundedCalc).abs().toFixed(this.minorUnits),
|
|
286
|
+
rule: ruleName
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Format amount for display
|
|
292
|
+
*/
|
|
293
|
+
formatAmount(amount: Decimal | number | string): string {
|
|
294
|
+
const amt = amount instanceof Decimal ? amount : new Decimal(amount);
|
|
295
|
+
const rounded = this.round(amt);
|
|
296
|
+
return `${rounded.toFixed(this.minorUnits)} ${this.currency}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get currency information
|
|
301
|
+
*/
|
|
302
|
+
getCurrencyInfo(): {
|
|
303
|
+
code: TCurrency;
|
|
304
|
+
minorUnits: number;
|
|
305
|
+
roundingMode: RoundingMode;
|
|
306
|
+
} {
|
|
307
|
+
return {
|
|
308
|
+
code: this.currency,
|
|
309
|
+
minorUnits: this.minorUnits,
|
|
310
|
+
roundingMode: this.roundingMode
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Factory function to create a decimal currency calculator
|
|
317
|
+
*/
|
|
318
|
+
export function createDecimalCalculator(
|
|
319
|
+
currency: TCurrency,
|
|
320
|
+
roundingMode?: RoundingMode
|
|
321
|
+
): DecimalCurrencyCalculator {
|
|
322
|
+
return new DecimalCurrencyCalculator(currency, roundingMode);
|
|
323
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 4217 Currency utilities for EN16931 compliance
|
|
3
|
+
* Provides currency-aware rounding and decimal handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ISO 4217 Currency minor units (decimal places)
|
|
8
|
+
* Based on ISO 4217:2015 standard
|
|
9
|
+
*
|
|
10
|
+
* Most currencies use 2 decimal places, but there are exceptions:
|
|
11
|
+
* - 0 decimals: JPY, KRW, CLP, etc.
|
|
12
|
+
* - 3 decimals: BHD, IQD, JOD, KWD, OMR, TND
|
|
13
|
+
* - 4 decimals: CLF (Chilean Unit of Account)
|
|
14
|
+
*/
|
|
15
|
+
export const ISO4217MinorUnits: Record<string, number> = {
|
|
16
|
+
// Major currencies
|
|
17
|
+
'EUR': 2, // Euro
|
|
18
|
+
'USD': 2, // US Dollar
|
|
19
|
+
'GBP': 2, // British Pound
|
|
20
|
+
'CHF': 2, // Swiss Franc
|
|
21
|
+
'CAD': 2, // Canadian Dollar
|
|
22
|
+
'AUD': 2, // Australian Dollar
|
|
23
|
+
'NZD': 2, // New Zealand Dollar
|
|
24
|
+
'CNY': 2, // Chinese Yuan
|
|
25
|
+
'INR': 2, // Indian Rupee
|
|
26
|
+
'MXN': 2, // Mexican Peso
|
|
27
|
+
'BRL': 2, // Brazilian Real
|
|
28
|
+
'RUB': 2, // Russian Ruble
|
|
29
|
+
'ZAR': 2, // South African Rand
|
|
30
|
+
'SGD': 2, // Singapore Dollar
|
|
31
|
+
'HKD': 2, // Hong Kong Dollar
|
|
32
|
+
'NOK': 2, // Norwegian Krone
|
|
33
|
+
'SEK': 2, // Swedish Krona
|
|
34
|
+
'DKK': 2, // Danish Krone
|
|
35
|
+
'PLN': 2, // Polish Zloty
|
|
36
|
+
'CZK': 2, // Czech Koruna
|
|
37
|
+
'HUF': 2, // Hungarian Forint (technically 2, though often shown as 0)
|
|
38
|
+
'RON': 2, // Romanian Leu
|
|
39
|
+
'BGN': 2, // Bulgarian Lev
|
|
40
|
+
'HRK': 2, // Croatian Kuna
|
|
41
|
+
'TRY': 2, // Turkish Lira
|
|
42
|
+
'ISK': 0, // Icelandic Króna (0 decimals)
|
|
43
|
+
|
|
44
|
+
// Zero decimal currencies
|
|
45
|
+
'JPY': 0, // Japanese Yen
|
|
46
|
+
'KRW': 0, // South Korean Won
|
|
47
|
+
'CLP': 0, // Chilean Peso
|
|
48
|
+
'PYG': 0, // Paraguayan Guaraní
|
|
49
|
+
'RWF': 0, // Rwandan Franc
|
|
50
|
+
'VND': 0, // Vietnamese Dong
|
|
51
|
+
'XAF': 0, // CFA Franc BEAC
|
|
52
|
+
'XOF': 0, // CFA Franc BCEAO
|
|
53
|
+
'XPF': 0, // CFP Franc
|
|
54
|
+
'BIF': 0, // Burundian Franc
|
|
55
|
+
'DJF': 0, // Djiboutian Franc
|
|
56
|
+
'GNF': 0, // Guinean Franc
|
|
57
|
+
'KMF': 0, // Comorian Franc
|
|
58
|
+
'MGA': 0, // Malagasy Ariary
|
|
59
|
+
'UGX': 0, // Ugandan Shilling
|
|
60
|
+
'VUV': 0, // Vanuatu Vatu
|
|
61
|
+
|
|
62
|
+
// Three decimal currencies
|
|
63
|
+
'BHD': 3, // Bahraini Dinar
|
|
64
|
+
'IQD': 3, // Iraqi Dinar
|
|
65
|
+
'JOD': 3, // Jordanian Dinar
|
|
66
|
+
'KWD': 3, // Kuwaiti Dinar
|
|
67
|
+
'LYD': 3, // Libyan Dinar
|
|
68
|
+
'OMR': 3, // Omani Rial
|
|
69
|
+
'TND': 3, // Tunisian Dinar
|
|
70
|
+
|
|
71
|
+
// Four decimal currencies
|
|
72
|
+
'CLF': 4, // Chilean Unit of Account (UF)
|
|
73
|
+
'UYW': 4, // Unidad Previsional (Uruguay)
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rounding modes for currency calculations
|
|
78
|
+
*/
|
|
79
|
+
export enum RoundingMode {
|
|
80
|
+
HALF_UP = 'HALF_UP', // Round half values up (0.5 → 1, -0.5 → -1)
|
|
81
|
+
HALF_DOWN = 'HALF_DOWN', // Round half values down (0.5 → 0, -0.5 → 0)
|
|
82
|
+
HALF_EVEN = 'HALF_EVEN', // Banker's rounding (0.5 → 0, 1.5 → 2)
|
|
83
|
+
UP = 'UP', // Always round up
|
|
84
|
+
DOWN = 'DOWN', // Always round down (truncate)
|
|
85
|
+
CEILING = 'CEILING', // Round toward positive infinity
|
|
86
|
+
FLOOR = 'FLOOR' // Round toward negative infinity
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Currency configuration for calculations
|
|
91
|
+
*/
|
|
92
|
+
export interface CurrencyConfig {
|
|
93
|
+
code: string;
|
|
94
|
+
minorUnits: number;
|
|
95
|
+
roundingMode: RoundingMode;
|
|
96
|
+
tolerance?: number; // Override default tolerance if needed
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get minor units (decimal places) for a currency
|
|
101
|
+
*/
|
|
102
|
+
export function getCurrencyMinorUnits(currencyCode: string): number {
|
|
103
|
+
const code = currencyCode.toUpperCase();
|
|
104
|
+
return ISO4217MinorUnits[code] ?? 2; // Default to 2 if unknown
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Round a value according to currency rules
|
|
109
|
+
*/
|
|
110
|
+
export function roundToCurrency(
|
|
111
|
+
value: number,
|
|
112
|
+
currencyCode: string,
|
|
113
|
+
mode: RoundingMode = RoundingMode.HALF_UP
|
|
114
|
+
): number {
|
|
115
|
+
const minorUnits = getCurrencyMinorUnits(currencyCode);
|
|
116
|
+
|
|
117
|
+
if (minorUnits === 0) {
|
|
118
|
+
// For zero decimal currencies, round to integer
|
|
119
|
+
return Math.round(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const multiplier = Math.pow(10, minorUnits);
|
|
123
|
+
const scaled = value * multiplier;
|
|
124
|
+
let rounded: number;
|
|
125
|
+
|
|
126
|
+
switch (mode) {
|
|
127
|
+
case RoundingMode.HALF_UP:
|
|
128
|
+
// Round half values away from zero
|
|
129
|
+
if (scaled >= 0) {
|
|
130
|
+
rounded = Math.floor(scaled + 0.5);
|
|
131
|
+
} else {
|
|
132
|
+
rounded = Math.ceil(scaled - 0.5);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case RoundingMode.HALF_DOWN:
|
|
136
|
+
// Round half values toward zero
|
|
137
|
+
const fraction = Math.abs(scaled % 1);
|
|
138
|
+
if (fraction === 0.5) {
|
|
139
|
+
// Exactly 0.5 - round toward zero
|
|
140
|
+
rounded = scaled >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
|
|
141
|
+
} else {
|
|
142
|
+
// Not exactly 0.5 - use normal rounding
|
|
143
|
+
rounded = Math.round(scaled);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case RoundingMode.HALF_EVEN:
|
|
147
|
+
// Banker's rounding
|
|
148
|
+
const isHalf = Math.abs(scaled % 1) === 0.5;
|
|
149
|
+
if (isHalf) {
|
|
150
|
+
const floor = Math.floor(scaled);
|
|
151
|
+
rounded = floor % 2 === 0 ? floor : Math.ceil(scaled);
|
|
152
|
+
} else {
|
|
153
|
+
rounded = Math.round(scaled);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case RoundingMode.UP:
|
|
157
|
+
rounded = scaled >= 0 ? Math.ceil(scaled) : Math.floor(scaled);
|
|
158
|
+
break;
|
|
159
|
+
case RoundingMode.DOWN:
|
|
160
|
+
rounded = Math.trunc(scaled);
|
|
161
|
+
break;
|
|
162
|
+
case RoundingMode.CEILING:
|
|
163
|
+
rounded = Math.ceil(scaled);
|
|
164
|
+
break;
|
|
165
|
+
case RoundingMode.FLOOR:
|
|
166
|
+
rounded = Math.floor(scaled);
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
rounded = Math.round(scaled);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return rounded / multiplier;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get tolerance for currency comparison
|
|
177
|
+
* Based on the smallest representable unit for the currency
|
|
178
|
+
*/
|
|
179
|
+
export function getCurrencyTolerance(currencyCode: string): number {
|
|
180
|
+
const minorUnits = getCurrencyMinorUnits(currencyCode);
|
|
181
|
+
// Tolerance is half of the smallest unit
|
|
182
|
+
return 0.5 * Math.pow(10, -minorUnits);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compare two monetary values with currency-aware tolerance
|
|
187
|
+
*/
|
|
188
|
+
export function areMonetaryValuesEqual(
|
|
189
|
+
value1: number,
|
|
190
|
+
value2: number,
|
|
191
|
+
currencyCode: string
|
|
192
|
+
): boolean {
|
|
193
|
+
const tolerance = getCurrencyTolerance(currencyCode);
|
|
194
|
+
return Math.abs(value1 - value2) <= tolerance;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Format a value according to currency decimal places
|
|
199
|
+
*/
|
|
200
|
+
export function formatCurrencyValue(
|
|
201
|
+
value: number,
|
|
202
|
+
currencyCode: string
|
|
203
|
+
): string {
|
|
204
|
+
const minorUnits = getCurrencyMinorUnits(currencyCode);
|
|
205
|
+
return value.toFixed(minorUnits);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate if a value has correct decimal places for a currency
|
|
210
|
+
*/
|
|
211
|
+
export function hasValidDecimalPlaces(
|
|
212
|
+
value: number,
|
|
213
|
+
currencyCode: string
|
|
214
|
+
): boolean {
|
|
215
|
+
const minorUnits = getCurrencyMinorUnits(currencyCode);
|
|
216
|
+
const multiplier = Math.pow(10, minorUnits);
|
|
217
|
+
const scaled = Math.round(value * multiplier);
|
|
218
|
+
const reconstructed = scaled / multiplier;
|
|
219
|
+
return Math.abs(value - reconstructed) < Number.EPSILON;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Currency calculation context for EN16931 compliance
|
|
224
|
+
*/
|
|
225
|
+
export class CurrencyCalculator {
|
|
226
|
+
private currency: string;
|
|
227
|
+
private minorUnits: number;
|
|
228
|
+
private roundingMode: RoundingMode;
|
|
229
|
+
|
|
230
|
+
constructor(config: CurrencyConfig | string) {
|
|
231
|
+
if (typeof config === 'string') {
|
|
232
|
+
this.currency = config;
|
|
233
|
+
this.minorUnits = getCurrencyMinorUnits(config);
|
|
234
|
+
this.roundingMode = RoundingMode.HALF_UP;
|
|
235
|
+
} else {
|
|
236
|
+
this.currency = config.code;
|
|
237
|
+
this.minorUnits = config.minorUnits;
|
|
238
|
+
this.roundingMode = config.roundingMode;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Round a value according to configured rules
|
|
244
|
+
*/
|
|
245
|
+
round(value: number): number {
|
|
246
|
+
return roundToCurrency(value, this.currency, this.roundingMode);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Calculate line net amount with rounding
|
|
251
|
+
* EN16931: Line net = (quantity × unit price) - line discounts
|
|
252
|
+
*/
|
|
253
|
+
calculateLineNet(
|
|
254
|
+
quantity: number,
|
|
255
|
+
unitPrice: number,
|
|
256
|
+
discount: number = 0
|
|
257
|
+
): number {
|
|
258
|
+
const gross = quantity * unitPrice;
|
|
259
|
+
const net = gross - discount;
|
|
260
|
+
return this.round(net);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Calculate VAT amount with rounding
|
|
265
|
+
* EN16931: VAT amount = taxable amount × (rate / 100)
|
|
266
|
+
*/
|
|
267
|
+
calculateVAT(taxableAmount: number, rate: number): number {
|
|
268
|
+
const vat = taxableAmount * (rate / 100);
|
|
269
|
+
return this.round(vat);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Compare values with currency-aware tolerance
|
|
274
|
+
*/
|
|
275
|
+
areEqual(value1: number, value2: number): boolean {
|
|
276
|
+
return areMonetaryValuesEqual(value1, value2, this.currency);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the tolerance for comparisons
|
|
281
|
+
*/
|
|
282
|
+
getTolerance(): number {
|
|
283
|
+
return getCurrencyTolerance(this.currency);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Format value for display
|
|
288
|
+
*/
|
|
289
|
+
format(value: number): string {
|
|
290
|
+
return formatCurrencyValue(value, this.currency);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get version info for ISO 4217 data
|
|
296
|
+
*/
|
|
297
|
+
export function getISO4217Version(): string {
|
|
298
|
+
return '2015'; // Update when currency list is updated
|
|
299
|
+
}
|