@amboras-dev/money 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +393 -2
  2. package/dist/ai.cjs +185 -0
  3. package/dist/ai.d.cts +97 -0
  4. package/dist/ai.d.ts +97 -0
  5. package/dist/ai.js +175 -0
  6. package/dist/allocate-CdnhvAda.js +199 -0
  7. package/dist/allocate-D7tm_t_o.cjs +222 -0
  8. package/dist/amount-D7dj76nH.js +200 -0
  9. package/dist/amount-DHtvZSSm.cjs +241 -0
  10. package/dist/arithmetic-Bq-8mSal.cjs +233 -0
  11. package/dist/arithmetic-C-Q_aI49.js +132 -0
  12. package/dist/billing.cjs +184 -0
  13. package/dist/billing.d.cts +81 -0
  14. package/dist/billing.d.ts +81 -0
  15. package/dist/billing.js +175 -0
  16. package/dist/commerce.cjs +536 -0
  17. package/dist/commerce.d.cts +293 -0
  18. package/dist/commerce.d.ts +293 -0
  19. package/dist/commerce.js +516 -0
  20. package/dist/format-BdB7LEIp.js +90 -0
  21. package/dist/format-BnA1aLJk.cjs +113 -0
  22. package/dist/fx.cjs +407 -0
  23. package/dist/fx.d.cts +157 -0
  24. package/dist/fx.d.ts +157 -0
  25. package/dist/fx.js +374 -0
  26. package/dist/index.cjs +349 -0
  27. package/dist/index.d.cts +288 -0
  28. package/dist/index.d.ts +288 -0
  29. package/dist/index.js +276 -0
  30. package/dist/input.cjs +163 -0
  31. package/dist/input.d.cts +203 -0
  32. package/dist/input.d.ts +203 -0
  33. package/dist/input.js +153 -0
  34. package/dist/iso-minor--gQWI1f2.cjs +65 -0
  35. package/dist/iso-minor-CtaiIifs.js +48 -0
  36. package/dist/medusa-CmxCni-G.js +101 -0
  37. package/dist/medusa-DuQ3-KoD.d.cts +78 -0
  38. package/dist/medusa-OJ2TJOxt.cjs +154 -0
  39. package/dist/medusa-RwqxaDGq.d.ts +78 -0
  40. package/dist/money-CIgELekG.js +3571 -0
  41. package/dist/money-CbhsT0fB.d.cts +232 -0
  42. package/dist/money-CbhsT0fB.d.ts +232 -0
  43. package/dist/money-D5-vlv0h.cjs +3960 -0
  44. package/dist/number-text-BpvN6NAo.cjs +145 -0
  45. package/dist/number-text-C82oh6JB.js +134 -0
  46. package/dist/own-D7544D3k.js +4 -0
  47. package/dist/own-oP9DXFWv.cjs +9 -0
  48. package/dist/percent-CKVI_c-m.js +20 -0
  49. package/dist/percent-Dt8vNIAW.cjs +43 -0
  50. package/dist/rate-CmeTGNex.d.ts +63 -0
  51. package/dist/rate-DGPfB_jc.cjs +217 -0
  52. package/dist/rate-lLPViBuM.js +152 -0
  53. package/dist/rate-toyS3EMf.d.cts +63 -0
  54. package/dist/reporting-5xby-Zas.d.ts +31 -0
  55. package/dist/reporting-DEM97DhQ.d.cts +31 -0
  56. package/dist/retail-BsvpNv1F.js +69 -0
  57. package/dist/retail-BwcFJcVN.d.ts +31 -0
  58. package/dist/retail-DTk2XAMM.cjs +86 -0
  59. package/dist/retail-Mqd-sPMF.d.cts +31 -0
  60. package/dist/rounding-CVAPEfq_.js +50 -0
  61. package/dist/rounding-oZ832yTD.cjs +79 -0
  62. package/dist/sql.cjs +290 -0
  63. package/dist/sql.d.cts +147 -0
  64. package/dist/sql.d.ts +147 -0
  65. package/dist/sql.js +271 -0
  66. package/dist/testing.cjs +28518 -0
  67. package/dist/testing.d.cts +97 -0
  68. package/dist/testing.d.ts +97 -0
  69. package/dist/testing.js +28510 -0
  70. package/dist/wire-CI3Sm1iC.js +1675 -0
  71. package/dist/wire-Cd4Q1Jmm.d.ts +269 -0
  72. package/dist/wire-DQZ6fA3c.cjs +1872 -0
  73. package/dist/wire-hWuixv3w.d.cts +269 -0
  74. package/dist/wire.cjs +35 -0
  75. package/dist/wire.d.cts +2 -0
  76. package/dist/wire.d.ts +2 -0
  77. package/dist/wire.js +2 -0
  78. package/package.json +158 -3
package/dist/fx.d.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { L as DecimalText, R as RoundingMode, r as Money } from "./money-CbhsT0fB.js";
2
+ import { n as priceRoundingParams, r as roundRetail, t as RetailRounding } from "./retail-BwcFJcVN.js";
3
+ import { a as RateRow, c as rate, d as rateFromRow, f as rateToDecimal, i as RateFromDecimalOptions, l as rateEquals, n as RATE_REF, o as crossRate, p as rateToNumber, r as Rate, s as invertRate, t as RATE_BOUNDS, u as rateFromDecimal } from "./rate-CmeTGNex.js";
4
+ import { i as toReporting, n as ReportingTotal, r as storeFactors, t as FactorTable } from "./reporting-5xby-Zas.js";
5
+ import { n as MoneyByCurrency } from "./medusa-RwqxaDGq.js";
6
+ //#region src/fx/convert.d.ts
7
+ /**
8
+ * The factor taking a stored `from` amount to a stored `to` amount for the major rate `r`
9
+ * (1 from = r to): `{ r.n x storageScale(to), r.d x storageScale(from) }`. Replaces Medusa
10
+ * `storedRateFactor`, `regionSwitchFactor`, `conversionFactor`.
11
+ */
12
+ export declare function convertFactor(from: string, to: string, r: Rate): Rate;
13
+ interface ConvertOptions {
14
+ /** Default `'currency'` (whole yen for JPY, a cent for USD): the one rounding (D6). */
15
+ unit?: 'currency' | 'storage';
16
+ /** Default `'half-up'`. */
17
+ rounding?: RoundingMode;
18
+ /**
19
+ * Applied after the unit rounding, only when given (so `unit: 'storage'` keeps a sub-unit
20
+ * JPY amount; an explicit `'none'` rounds a zero-decimal currency to a whole unit, as Medusa).
21
+ */
22
+ retail?: RetailRounding;
23
+ }
24
+ /**
25
+ * `m` in `to` at the major rate `r` (1 m.currency = r to): exact `m x convertFactor`, rounded
26
+ * once to `unit`, then `retail`. The same currency returns `m` unchanged.
27
+ */
28
+ export declare function convert(m: Money, to: string, r: Rate, options?: ConvertOptions): Money;
29
+ /**
30
+ * The major rate two prices imply, `major(b) / major(a)`, exactly and in lowest terms (JPY
31
+ * 150000 against USD 1000 gives 150). Both must be positive, else `rate_invalid`.
32
+ */
33
+ export declare function impliedRate(a: Money, b: Money): Rate;
34
+ //#endregion
35
+ //#region src/fx/format-rate.d.ts
36
+ /**
37
+ * `'1 USD = 83.42 INR'`. `decimals: 'auto'` (default) shows 4 significant digits with at
38
+ * least 2 and at most 8 fraction digits; a number fixes them. Rounded half-up exactly, then
39
+ * grouped for `locale` (default `'en-US'`).
40
+ */
41
+ export declare function formatRate(r: Rate, from: string, to: string, options?: {
42
+ decimals?: 'auto' | number;
43
+ locale?: string;
44
+ }): string;
45
+ //#endregion
46
+ //#region src/fx/plausibility.d.ts
47
+ /** Medusa PRICE_SEED_RATE_MAX_DEVIATION's default. */
48
+ export declare const DEFAULT_RATE_MAX_DEVIATION = 2;
49
+ /** chooseFactor's tolerance between the conventional factor and the observed median. */
50
+ export declare const FACTOR_TOLERANCE = 0.02;
51
+ /** How far apart two positive rates are, as a factor >= 1 (2 = one is double the other). */
52
+ export declare function deviationFactor(stored: number, reference: number): number;
53
+ interface RateReference {
54
+ /** Units of quote per one unit of the default currency, lower-case codes. */
55
+ readonly rates: Readonly<Record<string, number>>;
56
+ readonly source: string;
57
+ }
58
+ type RateClassification = {
59
+ readonly status: 'plausible';
60
+ readonly currency: string;
61
+ readonly stored: number;
62
+ } | {
63
+ readonly status: 'unverifiable';
64
+ readonly reason: 'no_reference' | 'no_pair';
65
+ readonly currency: string;
66
+ readonly stored: number;
67
+ } | {
68
+ readonly status: 'refused';
69
+ readonly reason: 'invalid';
70
+ readonly currency: string;
71
+ readonly stored: number;
72
+ } | {
73
+ readonly status: 'refused';
74
+ readonly reason: 'implausible';
75
+ readonly currency: string;
76
+ readonly stored: number;
77
+ readonly reference: number;
78
+ readonly factor: number;
79
+ readonly source: string;
80
+ };
81
+ /**
82
+ * One locked rate against the reference, in Medusa `classifyRates` order: invalid (null or not
83
+ * a rate) is refused `invalid`; no reference is unverifiable `no_reference`; no positive
84
+ * reference for the currency is unverifiable `no_pair`; a deviation strictly above
85
+ * `maxDeviation` (default 2) is refused `implausible`; else plausible.
86
+ */
87
+ export declare function classifyRate(r: Rate | null, code: string, reference: RateReference | null, options?: {
88
+ maxDeviation?: number;
89
+ }): RateClassification;
90
+ /**
91
+ * The storage factor confirmed by the store's own prices (Medusa `chooseFactor`): the
92
+ * candidates `[c, c / 100, c x 100]` against the upper median of the finite positive observed
93
+ * ratios; the first within `tolerance` (default 0.02) wins, none gives null. No observations
94
+ * trusts `conventional`.
95
+ */
96
+ export declare function confirmStoredFactor(conventional: Rate, observedRatios: readonly number[], options?: {
97
+ tolerance?: number;
98
+ }): Rate | null;
99
+ /**
100
+ * The upper median of `rates` and which of them lie within `tolerance` (default 0.01) of it:
101
+ * accepted iff `|r / median - 1| <= tolerance`, exactly. No rates: `{ median: null, accepted: [] }`.
102
+ */
103
+ export declare function rateConsensus(rates: readonly Rate[], options?: {
104
+ tolerance?: number;
105
+ }): {
106
+ median: Rate | null;
107
+ accepted: boolean[];
108
+ };
109
+ //#endregion
110
+ //#region src/fx/settings.d.ts
111
+ export declare const RATE_MODES: readonly ["manual", "dynamic"];
112
+ export declare const PRICE_ROUNDINGS: readonly ["none", "nearest"];
113
+ /** `store_currency_rate.rate_mode` as readers coerce it: `'manual'` exactly, else `'dynamic'`. */
114
+ export declare function parseRateMode(value: unknown): (typeof RATE_MODES)[number];
115
+ /** `store_currency_rate.rounding` as readers coerce it: `'nearest'` exactly, else `'none'`. */
116
+ export declare function parsePriceRounding(value: unknown): (typeof PRICE_ROUNDINGS)[number];
117
+ type MoneyByCurrencyFallback = {
118
+ readonly kind: 'refuse';
119
+ } | {
120
+ readonly kind: 'major';
121
+ readonly amount: DecimalText | number;
122
+ } | {
123
+ readonly kind: 'convert';
124
+ readonly from: string;
125
+ readonly rate: Rate;
126
+ readonly retail?: RetailRounding;
127
+ };
128
+ /**
129
+ * The amount a per-currency setting holds for `currency`: the listed entry, else the explicit
130
+ * fallback: `'refuse'` gives null; `'major'` is that many major units of `currency` itself
131
+ * (the store-credit cap `{ kind: 'major', amount: '15000' }`); `'convert'` converts the
132
+ * `from` entry at `rate` (null when there is no `from` entry). Never a silent default.
133
+ */
134
+ export declare function resolveMoneyByCurrency(setting: MoneyByCurrency, currency: string, options: {
135
+ fallback: MoneyByCurrencyFallback;
136
+ }): {
137
+ money: Money;
138
+ source: 'listed' | 'fallback';
139
+ } | null;
140
+ //#endregion
141
+ //#region src/fx/tables.d.ts
142
+ /** 1 USD = rate units of the currency, exact (see data/pegs.ts for the sources). Upper-case keys. */
143
+ export declare const USD_PEGS: Readonly<Record<string, Rate>>;
144
+ /** A feed's rate table with lower-case keys and only finite positive numbers kept (Medusa `lowerKeys`). Never throws. */
145
+ export declare function sanitizeRateTable(raw: unknown): Record<string, number>;
146
+ /** `{ ...fallback, ...primary }`: the primary feed (ECB) wins every currency it publishes. */
147
+ export declare function mergeRateTables(primary: Readonly<Record<string, number>>, fallback: Readonly<Record<string, number>>): Record<string, number>;
148
+ /**
149
+ * The rate 1 `from` = ? `to` when the live pair is unavailable, through the USD pegs
150
+ * (orch `resolveRateMajor`): a pegged `to` gives the peg from USD, else `(from -> USD) x peg`
151
+ * with `from -> USD` read from `ratesPerUsd` (units per 1 USD, any key case) or from `from`'s
152
+ * own peg; a pegged `from` into USD gives the inverse peg (new, symmetric). The same code
153
+ * gives 1/1; anything else null.
154
+ */
155
+ export declare function pegFallback(from: string, to: string, ratesPerUsd: Readonly<Record<string, number>>, pegs?: Readonly<Record<string, Rate>>): Rate | null;
156
+ //#endregion
157
+ export { type ConvertOptions, type FactorTable, type MoneyByCurrencyFallback, RATE_BOUNDS, RATE_REF, type Rate, type RateClassification, type RateFromDecimalOptions, type RateReference, type RateRow, type ReportingTotal, type RetailRounding, crossRate, invertRate, priceRoundingParams, rate, rateEquals, rateFromDecimal, rateFromRow, rateToDecimal, rateToNumber, roundRetail, storeFactors, toReporting };
package/dist/fx.js ADDED
@@ -0,0 +1,374 @@
1
+ import { E as resolveCurrency, J as gcd, M as resolveLocale, Q as describeValue, X as ratioToDecimal, Y as mulDiv, Z as MoneyError, ct as pow10, dt as toDecimalText, j as numberFormat, n as fromMajor, q as compareRatio, rt as formatDecimal, s as storedDecimal, t as boundedMoney, tt as decimalFromNumber, y as findCurrency, z as decimalOf } from "./money-CIgELekG.js";
2
+ import { t as mulDivToUnit } from "./rounding-CVAPEfq_.js";
3
+ import { t as hasOwnKey } from "./own-D7544D3k.js";
4
+ import { r as moneyFor } from "./medusa-CmxCni-G.js";
5
+ import { a as invertRate, c as rateFromDecimal, d as rateToNumber, i as crossRate, l as rateFromRow, n as RATE_REF, o as rate, r as checkRate, s as rateEquals, t as RATE_BOUNDS, u as rateToDecimal } from "./rate-lLPViBuM.js";
6
+ import { n as retailUnits, r as roundRetail, t as priceRoundingParams } from "./retail-BsvpNv1F.js";
7
+ //#region src/fx/convert.ts
8
+ const infoOf = (code) => resolveCurrency(code, { allowHistoric: true });
9
+ /**
10
+ * The factor taking a stored `from` amount to a stored `to` amount for the major rate `r`
11
+ * (1 from = r to): `{ r.n x storageScale(to), r.d x storageScale(from) }`. Replaces Medusa
12
+ * `storedRateFactor`, `regionSwitchFactor`, `conversionFactor`.
13
+ */
14
+ function convertFactor(from, to, r) {
15
+ const { numerator, denominator } = checkRate(r);
16
+ return {
17
+ numerator: numerator * BigInt(infoOf(to).storageScale),
18
+ denominator: denominator * BigInt(infoOf(from).storageScale)
19
+ };
20
+ }
21
+ /**
22
+ * `m` in `to` at the major rate `r` (1 m.currency = r to): exact `m x convertFactor`, rounded
23
+ * once to `unit`, then `retail`. The same currency returns `m` unchanged.
24
+ */
25
+ function convert(m, to, r, options = {}) {
26
+ const { info: source } = storedDecimal(m);
27
+ const target = infoOf(to);
28
+ if (source.code === target.code) return m;
29
+ const { numerator, denominator } = convertFactor(source.code, target.code, r);
30
+ const units = mulDivToUnit(BigInt(m.amount), numerator, denominator, target, options.unit ?? "currency", options.rounding);
31
+ return boundedMoney(options.retail === void 0 ? units : retailUnits(units, 1n, target, options.retail), target);
32
+ }
33
+ /**
34
+ * The major rate two prices imply, `major(b) / major(a)`, exactly and in lowest terms (JPY
35
+ * 150000 against USD 1000 gives 150). Both must be positive, else `rate_invalid`.
36
+ */
37
+ function impliedRate(a, b) {
38
+ const { info: left } = storedDecimal(a);
39
+ const { info: right } = storedDecimal(b);
40
+ if (a.amount <= 0 || b.amount <= 0) throw new MoneyError("rate_invalid", `An implied rate needs two positive amounts, got ${a.amount} and ${b.amount}`);
41
+ const numerator = BigInt(b.amount) * BigInt(left.storageScale);
42
+ const denominator = BigInt(a.amount) * BigInt(right.storageScale);
43
+ const divisor = gcd(numerator, denominator);
44
+ return {
45
+ numerator: numerator / divisor,
46
+ denominator: denominator / divisor
47
+ };
48
+ }
49
+ //#endregion
50
+ //#region src/fx/format-rate.ts
51
+ /** `floor(log10(n / d))` of a positive ratio, exactly. */
52
+ function exponentOf(n, d) {
53
+ let e = n.toString().length - d.toString().length;
54
+ const at = (k) => k >= 0 ? compareRatio(n, d, pow10(k), 1n) : compareRatio(n, d, 1n, pow10(-k));
55
+ if (at(e) < 0) e -= 1;
56
+ return e;
57
+ }
58
+ const asIntlValue = (text) => text;
59
+ /**
60
+ * `'1 USD = 83.42 INR'`. `decimals: 'auto'` (default) shows 4 significant digits with at
61
+ * least 2 and at most 8 fraction digits; a number fixes them. Rounded half-up exactly, then
62
+ * grouped for `locale` (default `'en-US'`).
63
+ */
64
+ function formatRate(r, from, to, options = {}) {
65
+ const { numerator, denominator } = checkRate(r);
66
+ const fromCode = resolveCurrency(from, { allowHistoric: true }).code;
67
+ const toCode = resolveCurrency(to, { allowHistoric: true }).code;
68
+ const requested = options.decimals ?? "auto";
69
+ let digits;
70
+ if (requested === "auto") digits = Math.min(8, Math.max(2, 3 - exponentOf(numerator, denominator)));
71
+ else if (Number.isSafeInteger(requested) && requested >= 0 && requested <= 20) digits = requested;
72
+ else throw new MoneyError("amount_invalid", `decimals must be 'auto' or an integer from 0 to 20, got ${describeValue(requested)}`);
73
+ const value = ratioToDecimal(numerator, denominator, digits);
74
+ return `1 ${fromCode} = ${numberFormat(resolveLocale(options.locale), {
75
+ minimumFractionDigits: digits,
76
+ maximumFractionDigits: digits
77
+ }).format(asIntlValue(formatDecimal(value)))} ${toCode}`;
78
+ }
79
+ //#endregion
80
+ //#region src/fx/plausibility.ts
81
+ /** Medusa PRICE_SEED_RATE_MAX_DEVIATION's default. */
82
+ const DEFAULT_RATE_MAX_DEVIATION = 2;
83
+ /** chooseFactor's tolerance between the conventional factor and the observed median. */
84
+ const FACTOR_TOLERANCE = .02;
85
+ /** How far apart two positive rates are, as a factor >= 1 (2 = one is double the other). */
86
+ function deviationFactor(stored, reference) {
87
+ const ratio = stored / reference;
88
+ return ratio >= 1 ? ratio : 1 / ratio;
89
+ }
90
+ const isPositiveFinite$1 = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
91
+ /** A positive finite number as an exact Decimal; anything else throws `amount_invalid` naming `name`. */
92
+ function positiveDecimal(n, name) {
93
+ if (!isPositiveFinite$1(n)) throw new MoneyError("amount_invalid", `${name} must be a positive finite number, got ${describeValue(n)}`);
94
+ return decimalFromNumber(n);
95
+ }
96
+ function validRate(r) {
97
+ try {
98
+ return checkRate(r);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * One locked rate against the reference, in Medusa `classifyRates` order: invalid (null or not
105
+ * a rate) is refused `invalid`; no reference is unverifiable `no_reference`; no positive
106
+ * reference for the currency is unverifiable `no_pair`; a deviation strictly above
107
+ * `maxDeviation` (default 2) is refused `implausible`; else plausible.
108
+ */
109
+ function classifyRate(r, code, reference, options = {}) {
110
+ const currency = code.toLowerCase();
111
+ const max = positiveDecimal(options.maxDeviation ?? 2, "maxDeviation");
112
+ const valid = validRate(r);
113
+ if (!valid) return {
114
+ status: "refused",
115
+ reason: "invalid",
116
+ currency,
117
+ stored: NaN
118
+ };
119
+ const stored = rateToNumber(valid, { decimals: "exact" });
120
+ if (!reference) return {
121
+ status: "unverifiable",
122
+ reason: "no_reference",
123
+ currency,
124
+ stored
125
+ };
126
+ const ref = hasOwnKey(reference.rates, currency) ? reference.rates[currency] : void 0;
127
+ if (!isPositiveFinite$1(ref)) return {
128
+ status: "unverifiable",
129
+ reason: "no_pair",
130
+ currency,
131
+ stored
132
+ };
133
+ const refValue = decimalFromNumber(ref);
134
+ const top = valid.numerator * pow10(refValue.scale);
135
+ const bottom = valid.denominator * refValue.units;
136
+ const maxD = pow10(max.scale);
137
+ if (compareRatio(top, bottom, max.units, maxD) > 0 || compareRatio(bottom, top, max.units, maxD) > 0) return {
138
+ status: "refused",
139
+ reason: "implausible",
140
+ currency,
141
+ stored,
142
+ reference: ref,
143
+ factor: deviationFactor(stored, ref),
144
+ source: reference.source
145
+ };
146
+ return {
147
+ status: "plausible",
148
+ currency,
149
+ stored
150
+ };
151
+ }
152
+ /** `|value - c| / c <= tolerance`, exactly (`c` positive). */
153
+ function closeTo(value, c, tolerance) {
154
+ const scaledC = c.numerator * pow10(value.scale);
155
+ const gap = value.units * c.denominator - scaledC;
156
+ return compareRatio((gap < 0n ? -gap : gap) * pow10(tolerance.scale), 1n, tolerance.units * scaledC, 1n) <= 0;
157
+ }
158
+ /**
159
+ * The storage factor confirmed by the store's own prices (Medusa `chooseFactor`): the
160
+ * candidates `[c, c / 100, c x 100]` against the upper median of the finite positive observed
161
+ * ratios; the first within `tolerance` (default 0.02) wins, none gives null. No observations
162
+ * trusts `conventional`.
163
+ */
164
+ function confirmStoredFactor(conventional, observedRatios, options = {}) {
165
+ const c = checkRate(conventional);
166
+ const tolerance = decimalFromNumber(nonNegative(options.tolerance ?? .02));
167
+ const ratios = observedRatios.filter(isPositiveFinite$1).sort((a, b) => a - b);
168
+ if (ratios.length === 0) return c;
169
+ const median = decimalFromNumber(ratios[Math.floor(ratios.length / 2)]);
170
+ return [
171
+ c,
172
+ {
173
+ numerator: c.numerator,
174
+ denominator: c.denominator * 100n
175
+ },
176
+ {
177
+ numerator: c.numerator * 100n,
178
+ denominator: c.denominator
179
+ }
180
+ ].find((candidate) => closeTo(median, candidate, tolerance)) ?? null;
181
+ }
182
+ function nonNegative(n) {
183
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 0) throw new MoneyError("amount_invalid", `tolerance must be a non-negative finite number, got ${describeValue(n)}`);
184
+ return n;
185
+ }
186
+ /**
187
+ * The upper median of `rates` and which of them lie within `tolerance` (default 0.01) of it:
188
+ * accepted iff `|r / median - 1| <= tolerance`, exactly. No rates: `{ median: null, accepted: [] }`.
189
+ */
190
+ function rateConsensus(rates, options = {}) {
191
+ const valid = rates.map(checkRate);
192
+ const tolerance = decimalFromNumber(nonNegative(options.tolerance ?? .01));
193
+ if (valid.length === 0) return {
194
+ median: null,
195
+ accepted: []
196
+ };
197
+ const sorted = [...valid].sort((a, b) => compareRatio(a.numerator, a.denominator, b.numerator, b.denominator));
198
+ const median = sorted[Math.floor(sorted.length / 2)];
199
+ return {
200
+ median,
201
+ accepted: valid.map((r) => {
202
+ const gap = r.numerator * median.denominator - median.numerator * r.denominator;
203
+ const scale = pow10(tolerance.scale);
204
+ return compareRatio((gap < 0n ? -gap : gap) * scale, 1n, tolerance.units * median.numerator * r.denominator, 1n) <= 0;
205
+ })
206
+ };
207
+ }
208
+ //#endregion
209
+ //#region src/fx/reporting.ts
210
+ const FACTOR_DIGITS = 20;
211
+ /**
212
+ * The reporting factors of a store whose locked rates say `1 default = n/d <code>`:
213
+ * `factor(code) = storageScale(default) / storageScale(code) x d / n`, rounded half-up to 20
214
+ * fraction digits; the default currency is `'1'`. Rows for the default currency, unknown
215
+ * codes or invalid rates (see `rateFromRow`) are skipped, so those currencies have no factor.
216
+ * Port of Medusa `storeTotalFactors`, exact (it measured the factor at a 1e9 reference).
217
+ */
218
+ function storeFactors(defaultCurrency, rows) {
219
+ const base = resolveCurrency(defaultCurrency, { allowHistoric: true });
220
+ const factors = { [base.code.toLowerCase()]: "1" };
221
+ for (const row of rows) {
222
+ const info = findCurrency(row?.currency_code, { allowHistoric: true });
223
+ const r = rateFromRow(row);
224
+ if (!info || !r || info.code === base.code) continue;
225
+ const units = mulDiv(BigInt(base.storageScale) * r.denominator, pow10(FACTOR_DIGITS), BigInt(info.storageScale) * r.numerator, "half-up");
226
+ factors[info.code.toLowerCase()] = toDecimalText({
227
+ units,
228
+ scale: FACTOR_DIGITS
229
+ });
230
+ }
231
+ return {
232
+ base: base.code,
233
+ factors: Object.freeze(factors)
234
+ };
235
+ }
236
+ /**
237
+ * The sum of `items` in `table.base`: each converted with its currency's factor and rounded
238
+ * half-up to a storage unit (per item, as customer-stats rounds per order), then summed.
239
+ * Items whose currency has no factor are excluded and reported in `missing`.
240
+ */
241
+ function toReporting(items, table) {
242
+ const base = resolveCurrency(table.base, { allowHistoric: true });
243
+ const missing = /* @__PURE__ */ new Set();
244
+ let total = 0n;
245
+ for (const item of items) {
246
+ const { info } = storedDecimal(item);
247
+ const key = info.code.toLowerCase();
248
+ const text = hasOwnKey(table.factors, key) ? table.factors[key] : void 0;
249
+ if (text === void 0) {
250
+ missing.add(info.code);
251
+ continue;
252
+ }
253
+ const factor = decimalOf(text, false, `factors.${key}`);
254
+ total += mulDiv(BigInt(item.amount), factor.units, pow10(factor.scale), "half-up");
255
+ }
256
+ return {
257
+ total: boundedMoney(total, base),
258
+ complete: missing.size === 0,
259
+ missing: [...missing].sort()
260
+ };
261
+ }
262
+ //#endregion
263
+ //#region src/fx/settings.ts
264
+ const RATE_MODES = ["manual", "dynamic"];
265
+ const PRICE_ROUNDINGS = ["none", "nearest"];
266
+ /** `store_currency_rate.rate_mode` as readers coerce it: `'manual'` exactly, else `'dynamic'`. */
267
+ function parseRateMode(value) {
268
+ return value === "manual" ? "manual" : "dynamic";
269
+ }
270
+ /** `store_currency_rate.rounding` as readers coerce it: `'nearest'` exactly, else `'none'`. */
271
+ function parsePriceRounding(value) {
272
+ return value === "nearest" ? "nearest" : "none";
273
+ }
274
+ /**
275
+ * The amount a per-currency setting holds for `currency`: the listed entry, else the explicit
276
+ * fallback: `'refuse'` gives null; `'major'` is that many major units of `currency` itself
277
+ * (the store-credit cap `{ kind: 'major', amount: '15000' }`); `'convert'` converts the
278
+ * `from` entry at `rate` (null when there is no `from` entry). Never a silent default.
279
+ */
280
+ function resolveMoneyByCurrency(setting, currency, options) {
281
+ const info = resolveCurrency(currency, { allowHistoric: true });
282
+ const listed = moneyFor(setting, info.code);
283
+ if (listed) return {
284
+ money: listed,
285
+ source: "listed"
286
+ };
287
+ const fallback = options.fallback;
288
+ if (fallback.kind === "major") return {
289
+ money: fromMajor(fallback.amount, info.code),
290
+ source: "fallback"
291
+ };
292
+ if (fallback.kind === "convert") {
293
+ const base = moneyFor(setting, fallback.from);
294
+ return base ? {
295
+ money: convert(base, info.code, fallback.rate, { retail: fallback.retail }),
296
+ source: "fallback"
297
+ } : null;
298
+ }
299
+ return null;
300
+ }
301
+ //#endregion
302
+ //#region src/fx/tables.ts
303
+ /** 1 USD = rate units of the currency, exact (see data/pegs.ts for the sources). Upper-case keys. */
304
+ const USD_PEGS = Object.freeze(Object.fromEntries(Object.entries({
305
+ AED: [36725n, 10000n],
306
+ SAR: [375n, 100n],
307
+ QAR: [364n, 100n],
308
+ BHD: [376n, 1000n],
309
+ OMR: [3845n, 10000n]
310
+ }).map(([code, [numerator, denominator]]) => [code, Object.freeze({
311
+ numerator,
312
+ denominator
313
+ })])));
314
+ const isPositiveFinite = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
315
+ /** A feed's rate table with lower-case keys and only finite positive numbers kept (Medusa `lowerKeys`). Never throws. */
316
+ function sanitizeRateTable(raw) {
317
+ const out = {};
318
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
319
+ for (const [key, value] of Object.entries(raw)) if (isPositiveFinite(value)) out[key.toLowerCase()] = value;
320
+ return out;
321
+ }
322
+ /** `{ ...fallback, ...primary }`: the primary feed (ECB) wins every currency it publishes. */
323
+ function mergeRateTables(primary, fallback) {
324
+ return {
325
+ ...fallback,
326
+ ...primary
327
+ };
328
+ }
329
+ function own(table, code) {
330
+ for (const key of [code.toUpperCase(), code.toLowerCase()]) if (hasOwnKey(table, key)) return table[key];
331
+ }
332
+ /** Units of `code` per 1 USD: the feed's finite positive value, else its peg, else undefined. */
333
+ function perUsd(code, ratesPerUsd, pegs) {
334
+ const live = own(ratesPerUsd, code);
335
+ if (isPositiveFinite(live)) {
336
+ const d = decimalFromNumber(live);
337
+ return {
338
+ numerator: d.units,
339
+ denominator: pow10(d.scale)
340
+ };
341
+ }
342
+ const peg = own(pegs, code);
343
+ return peg === void 0 ? void 0 : checkRate(peg);
344
+ }
345
+ /**
346
+ * The rate 1 `from` = ? `to` when the live pair is unavailable, through the USD pegs
347
+ * (orch `resolveRateMajor`): a pegged `to` gives the peg from USD, else `(from -> USD) x peg`
348
+ * with `from -> USD` read from `ratesPerUsd` (units per 1 USD, any key case) or from `from`'s
349
+ * own peg; a pegged `from` into USD gives the inverse peg (new, symmetric). The same code
350
+ * gives 1/1; anything else null.
351
+ */
352
+ function pegFallback(from, to, ratesPerUsd, pegs = USD_PEGS) {
353
+ const f = from.toUpperCase();
354
+ const t = to.toUpperCase();
355
+ if (f === t) return {
356
+ numerator: 1n,
357
+ denominator: 1n
358
+ };
359
+ const toPeg = own(pegs, t);
360
+ if (toPeg !== void 0) {
361
+ const peg = checkRate(toPeg);
362
+ if (f === "USD") return peg;
363
+ const source = perUsd(f, ratesPerUsd, pegs);
364
+ if (!source) return null;
365
+ return {
366
+ numerator: peg.numerator * source.denominator,
367
+ denominator: peg.denominator * source.numerator
368
+ };
369
+ }
370
+ const fromPeg = own(pegs, f);
371
+ return fromPeg !== void 0 && t === "USD" ? invertRate(fromPeg) : null;
372
+ }
373
+ //#endregion
374
+ export { DEFAULT_RATE_MAX_DEVIATION, FACTOR_TOLERANCE, PRICE_ROUNDINGS, RATE_BOUNDS, RATE_MODES, RATE_REF, USD_PEGS, classifyRate, confirmStoredFactor, convert, convertFactor, crossRate, deviationFactor, formatRate, impliedRate, invertRate, mergeRateTables, parsePriceRounding, parseRateMode, pegFallback, priceRoundingParams, rate, rateConsensus, rateEquals, rateFromDecimal, rateFromRow, rateToDecimal, rateToNumber, resolveMoneyByCurrency, roundRetail, sanitizeRateTable, storeFactors, toReporting };