@fin.cx/einvoice 5.0.3 → 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.
Files changed (116) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +4 -8
  3. package/dist_ts/einvoice.js +46 -7
  4. package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
  5. package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
  6. package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
  7. package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
  8. package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
  9. package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
  10. package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
  11. package/dist_ts/formats/semantic/semantic.validator.js +596 -0
  12. package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
  13. package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
  14. package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
  15. package/dist_ts/formats/utils/currency.utils.js +250 -0
  16. package/dist_ts/formats/utils/decimal.d.ts +151 -0
  17. package/dist_ts/formats/utils/decimal.js +435 -0
  18. package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
  19. package/dist_ts/formats/validation/codelist.validator.js +188 -0
  20. package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
  21. package/dist_ts/formats/validation/conformance.harness.js +471 -0
  22. package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
  23. package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
  24. package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
  25. package/dist_ts/formats/validation/facturx.validator.js +522 -0
  26. package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
  27. package/dist_ts/formats/validation/integrated.validator.js +311 -0
  28. package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
  29. package/dist_ts/formats/validation/peppol.validator.js +529 -0
  30. package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
  31. package/dist_ts/formats/validation/schematron.downloader.js +254 -0
  32. package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
  33. package/dist_ts/formats/validation/schematron.integration.js +230 -0
  34. package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
  35. package/dist_ts/formats/validation/schematron.validator.js +288 -0
  36. package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
  37. package/dist_ts/formats/validation/schematron.worker.js +185 -0
  38. package/dist_ts/formats/validation/validation.types.d.ts +163 -0
  39. package/dist_ts/formats/validation/validation.types.js +181 -0
  40. package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
  41. package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
  42. package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
  43. package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
  44. package/dist_ts/interfaces/common.d.ts +1 -0
  45. package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
  46. package/dist_ts/interfaces/en16931-metadata.js +6 -0
  47. package/dist_ts/plugins.d.ts +3 -1
  48. package/dist_ts/plugins.js +7 -2
  49. package/dist_ts_install/download-schematron.d.ts +6 -0
  50. package/dist_ts_install/download-schematron.js +62 -0
  51. package/dist_ts_install/download-test-samples.d.ts +19 -0
  52. package/dist_ts_install/download-test-samples.js +169 -0
  53. package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
  54. package/dist_ts_install/download-xrechnung-rules.js +146 -0
  55. package/dist_ts_install/index.d.ts +8 -0
  56. package/dist_ts_install/index.js +246 -0
  57. package/package.json +9 -3
  58. package/readme.md +255 -810
  59. package/ts/00_commitinfo_data.ts +1 -1
  60. package/ts/einvoice.ts +63 -14
  61. package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
  62. package/ts/formats/semantic/bt-bg.model.ts +524 -0
  63. package/ts/formats/semantic/semantic.adapter.ts +600 -0
  64. package/ts/formats/semantic/semantic.validator.ts +654 -0
  65. package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
  66. package/ts/formats/utils/currency.utils.ts +299 -0
  67. package/ts/formats/utils/decimal.ts +509 -0
  68. package/ts/formats/validation/codelist.validator.ts +317 -0
  69. package/ts/formats/validation/conformance.harness.ts +591 -0
  70. package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
  71. package/ts/formats/validation/facturx.validator.ts +579 -0
  72. package/ts/formats/validation/integrated.validator.ts +405 -0
  73. package/ts/formats/validation/peppol.validator.ts +589 -0
  74. package/ts/formats/validation/schematron.downloader.ts +304 -0
  75. package/ts/formats/validation/schematron.integration.ts +285 -0
  76. package/ts/formats/validation/schematron.validator.ts +348 -0
  77. package/ts/formats/validation/schematron.worker.ts +221 -0
  78. package/ts/formats/validation/validation.types.ts +274 -0
  79. package/ts/formats/validation/vat-categories.validator.ts +845 -0
  80. package/ts/formats/validation/xrechnung.validator.ts +494 -0
  81. package/ts/interfaces/common.ts +1 -0
  82. package/ts/interfaces/en16931-metadata.ts +104 -0
  83. package/ts/plugins.ts +8 -0
  84. package/ts/tspublish.json +3 -0
  85. package/dist_ts/classes.decoder.d.ts +0 -40
  86. package/dist_ts/classes.decoder.js +0 -320
  87. package/dist_ts/classes.encoder.d.ts +0 -51
  88. package/dist_ts/classes.encoder.js +0 -293
  89. package/dist_ts/classes.xinvoice.d.ts +0 -136
  90. package/dist_ts/classes.xinvoice.js +0 -352
  91. package/dist_ts/formats/base.decoder.d.ts +0 -19
  92. package/dist_ts/formats/base.decoder.js +0 -130
  93. package/dist_ts/formats/base.validator.d.ts +0 -44
  94. package/dist_ts/formats/base.validator.js +0 -39
  95. package/dist_ts/formats/decoder.factory.d.ts +0 -15
  96. package/dist_ts/formats/decoder.factory.js +0 -46
  97. package/dist_ts/formats/factorx.decoder.d.ts +0 -18
  98. package/dist_ts/formats/factorx.decoder.js +0 -178
  99. package/dist_ts/formats/factorx.encoder.d.ts +0 -51
  100. package/dist_ts/formats/factorx.encoder.js +0 -293
  101. package/dist_ts/formats/facturx.decoder.d.ts +0 -18
  102. package/dist_ts/formats/facturx.decoder.js +0 -210
  103. package/dist_ts/formats/facturx.encoder.d.ts +0 -51
  104. package/dist_ts/formats/facturx.encoder.js +0 -293
  105. package/dist_ts/formats/facturx.validator.d.ts +0 -67
  106. package/dist_ts/formats/facturx.validator.js +0 -266
  107. package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
  108. package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
  109. package/dist_ts/formats/ubl.validator.d.ts +0 -82
  110. package/dist_ts/formats/ubl.validator.js +0 -306
  111. package/dist_ts/formats/validator.factory.d.ts +0 -18
  112. package/dist_ts/formats/validator.factory.js +0 -78
  113. package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
  114. package/dist_ts/formats/xinvoice.decoder.js +0 -332
  115. package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
  116. 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
+ }