@amboras-dev/money 0.0.0-stage → 0.2.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.
- package/README.md +401 -2
- package/dist/ai.cjs +185 -0
- package/dist/ai.d.cts +97 -0
- package/dist/ai.d.ts +97 -0
- package/dist/ai.js +175 -0
- package/dist/allocate-CZRg4TOP.cjs +222 -0
- package/dist/allocate-X7_jAtOj.js +199 -0
- package/dist/amount-BV0O-aN8.cjs +241 -0
- package/dist/amount-BoYBMgK9.js +200 -0
- package/dist/arithmetic-BhNwisjb.js +132 -0
- package/dist/arithmetic-DjT4Kzgt.cjs +233 -0
- package/dist/billing.cjs +184 -0
- package/dist/billing.d.cts +81 -0
- package/dist/billing.d.ts +81 -0
- package/dist/billing.js +175 -0
- package/dist/commerce.cjs +536 -0
- package/dist/commerce.d.cts +293 -0
- package/dist/commerce.d.ts +293 -0
- package/dist/commerce.js +516 -0
- package/dist/format-BdPnijvn.js +99 -0
- package/dist/format-DLhno7eX.cjs +128 -0
- package/dist/fx.cjs +407 -0
- package/dist/fx.d.cts +157 -0
- package/dist/fx.d.ts +157 -0
- package/dist/fx.js +374 -0
- package/dist/index.cjs +352 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +276 -0
- package/dist/input.cjs +163 -0
- package/dist/input.d.cts +203 -0
- package/dist/input.d.ts +203 -0
- package/dist/input.js +153 -0
- package/dist/iso-minor-CqkWDxHS.js +48 -0
- package/dist/iso-minor-wSwJN57T.cjs +65 -0
- package/dist/medusa-4K8N1pUc.d.cts +87 -0
- package/dist/medusa-DBXJXXvF.d.ts +87 -0
- package/dist/medusa-DqJY1qpj.js +116 -0
- package/dist/medusa-lRY9xlsf.cjs +175 -0
- package/dist/money-B-3aFjNJ.d.cts +248 -0
- package/dist/money-B-3aFjNJ.d.ts +248 -0
- package/dist/money-B3wH_Z4S.cjs +3990 -0
- package/dist/money-WhnGiI00.js +3583 -0
- package/dist/number-text-BWu67jUq.cjs +145 -0
- package/dist/number-text-DE-q4jO5.js +134 -0
- package/dist/own-D7544D3k.js +4 -0
- package/dist/own-oP9DXFWv.cjs +9 -0
- package/dist/percent-C0InC2t2.cjs +43 -0
- package/dist/percent-D1VB9xXG.js +20 -0
- package/dist/rate-CVeMii4a.cjs +217 -0
- package/dist/rate-DBPiYgT1.d.ts +63 -0
- package/dist/rate-DicStBue.js +152 -0
- package/dist/rate-tqK8ijG0.d.cts +63 -0
- package/dist/reporting-CG-ai2sM.d.cts +31 -0
- package/dist/reporting-tpGJbqYp.d.ts +31 -0
- package/dist/retail-1mu888J1.js +69 -0
- package/dist/retail-C_BPLvMj.d.cts +31 -0
- package/dist/retail-Cml8YSKg.cjs +86 -0
- package/dist/retail-DRUTTQoJ.d.ts +31 -0
- package/dist/rounding-DHBtNCHG.cjs +79 -0
- package/dist/rounding-g9rvye0k.js +50 -0
- package/dist/sql.cjs +290 -0
- package/dist/sql.d.cts +147 -0
- package/dist/sql.d.ts +147 -0
- package/dist/sql.js +271 -0
- package/dist/testing.cjs +61995 -0
- package/dist/testing.d.cts +125 -0
- package/dist/testing.d.ts +125 -0
- package/dist/testing.js +61986 -0
- package/dist/wire-Bhuut8cW.cjs +2097 -0
- package/dist/wire-CaUN9Vf_.d.ts +309 -0
- package/dist/wire-sW0yzqth.js +1888 -0
- package/dist/wire-xjYXv4go.d.cts +309 -0
- package/dist/wire.cjs +37 -0
- package/dist/wire.d.cts +2 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +2 -0
- package/package.json +158 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const require_money = require("./money-B3wH_Z4S.cjs");
|
|
2
|
+
const require_rounding = require("./rounding-DHBtNCHG.cjs");
|
|
3
|
+
//#region src/format.ts
|
|
4
|
+
function intlDisplay(options) {
|
|
5
|
+
const display = options.currencyDisplay ?? "symbol";
|
|
6
|
+
return display === "symbol+code" ? "symbol" : display;
|
|
7
|
+
}
|
|
8
|
+
function finish(text, code, options) {
|
|
9
|
+
const withCode = options.currencyDisplay === "symbol+code" && !text.includes(code) ? `${text} ${code}` : text;
|
|
10
|
+
return options.minusSign === "−" ? withCode.replace("-", "−") : withCode;
|
|
11
|
+
}
|
|
12
|
+
const asIntlValue = (text) => text;
|
|
13
|
+
/**
|
|
14
|
+
* Money for people: `'$1,234.50'`, `'¥1,200'`, `'KWD 1.005'`, `'-€5.00'`. Locale, currency
|
|
15
|
+
* display, digits and sign are explicit options; the active app locale is the caller's to
|
|
16
|
+
* pass. If Intl throws, `'<CODE> <decimal>'`.
|
|
17
|
+
*/
|
|
18
|
+
function formatMoney(m, options = {}) {
|
|
19
|
+
const { info, value } = require_money.storedDecimal(m);
|
|
20
|
+
const whole = require_money.fractionDigitsOf(value) === 0;
|
|
21
|
+
const digits = options.digits === "widen" ? Math.max(info.displayDigits, require_money.fractionDigitsOf(value)) : options.digits === "trim-zero" && whole ? 0 : info.displayDigits;
|
|
22
|
+
const text = require_money.formatDecimal(require_money.roundToScale(value, digits, "half-up"));
|
|
23
|
+
try {
|
|
24
|
+
return finish(require_money.numberFormat(require_money.resolveLocale(options.locale), {
|
|
25
|
+
style: "currency",
|
|
26
|
+
currency: info.code,
|
|
27
|
+
currencyDisplay: intlDisplay(options),
|
|
28
|
+
minimumFractionDigits: digits,
|
|
29
|
+
maximumFractionDigits: digits,
|
|
30
|
+
signDisplay: options.signDisplay ?? "auto"
|
|
31
|
+
}).format(asIntlValue(text)), info.code, options);
|
|
32
|
+
} catch {
|
|
33
|
+
return finish(`${info.code} ${text}`, info.code, options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A stored amount that may be fractional (a Medusa total, 107.8175; a percentage promotion) for
|
|
38
|
+
* people: rounded half-up to a whole storage unit (`roundToUnit`), then `formatMoney` with `options`.
|
|
39
|
+
* A number must be finite; text is plain decimal; an unknown currency throws `currency_invalid`.
|
|
40
|
+
*/
|
|
41
|
+
function formatStored(amount, currency, options = {}) {
|
|
42
|
+
return formatMoney(require_rounding.roundToUnit(amount, currency), options);
|
|
43
|
+
}
|
|
44
|
+
function compact(currency, major, options, significant) {
|
|
45
|
+
return require_money.numberFormat(require_money.resolveLocale(options.locale), {
|
|
46
|
+
style: "currency",
|
|
47
|
+
currency,
|
|
48
|
+
currencyDisplay: intlDisplay(options),
|
|
49
|
+
notation: "compact",
|
|
50
|
+
compactDisplay: "short",
|
|
51
|
+
maximumSignificantDigits: significant,
|
|
52
|
+
signDisplay: options.signDisplay ?? "auto"
|
|
53
|
+
}).format(asIntlValue(major));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Like `formatMoney` below `threshold` major units (default 10000, by magnitude); at or above
|
|
57
|
+
* it, compact short notation with `maximumSignificantDigits` (default 3): `'$12.3K'`.
|
|
58
|
+
*/
|
|
59
|
+
function formatMoneyCompact(m, options = {}) {
|
|
60
|
+
const { info, value } = require_money.storedDecimal(m);
|
|
61
|
+
const threshold = require_money.decimalOf(options.threshold ?? 1e4, false, "threshold");
|
|
62
|
+
const magnitude = {
|
|
63
|
+
units: value.units < 0n ? -value.units : value.units,
|
|
64
|
+
scale: value.scale
|
|
65
|
+
};
|
|
66
|
+
if (require_money.compareDecimal(magnitude, threshold) < 0) return formatMoney(m, options);
|
|
67
|
+
try {
|
|
68
|
+
return finish(compact(info.code, require_money.formatDecimal(value), options, options.maximumSignificantDigits ?? 3), info.code, options);
|
|
69
|
+
} catch {
|
|
70
|
+
return formatMoney(m, options);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** `'$10.00 – $20.00'` (en dash between spaces), `'$10.00+'` when `high` is null; zero amounts show `zeroLabel` when given. */
|
|
74
|
+
function formatMoneyRange(low, high, options = {}) {
|
|
75
|
+
const lowCode = require_money.storedDecimal(low).info.code;
|
|
76
|
+
const show = (m) => m.amount === 0 && options.zeroLabel !== void 0 ? options.zeroLabel : formatMoney(m, options);
|
|
77
|
+
if (high === null) return `${show(low)}+`;
|
|
78
|
+
const highCode = require_money.storedDecimal(high).info.code;
|
|
79
|
+
if (highCode !== lowCode) throw new require_money.MoneyError("currency_mismatch", `Cannot format a range from ${lowCode} to ${highCode}`);
|
|
80
|
+
return `${show(low)} – ${show(high)}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A chart tick formatter for values that already are major units: always compact, with the
|
|
84
|
+
* same rounding as `formatMoneyCompact` (`1234567` gives `'$1.23M'`).
|
|
85
|
+
*/
|
|
86
|
+
function moneyAxisFormatter(currency, options = {}) {
|
|
87
|
+
const { code } = require_money.resolveCurrency(currency, { allowHistoric: true });
|
|
88
|
+
return (major) => {
|
|
89
|
+
if (!Number.isFinite(major)) return `${code} ${major}`;
|
|
90
|
+
const text = require_money.formatDecimal(require_money.decimalFromNumber(major));
|
|
91
|
+
try {
|
|
92
|
+
return finish(compact(code, text, options, 3), code, options);
|
|
93
|
+
} catch {
|
|
94
|
+
return `${code} ${text}`;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
Object.defineProperty(exports, "formatMoney", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
get: function() {
|
|
102
|
+
return formatMoney;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(exports, "formatMoneyCompact", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
get: function() {
|
|
108
|
+
return formatMoneyCompact;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
Object.defineProperty(exports, "formatMoneyRange", {
|
|
112
|
+
enumerable: true,
|
|
113
|
+
get: function() {
|
|
114
|
+
return formatMoneyRange;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
Object.defineProperty(exports, "formatStored", {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
get: function() {
|
|
120
|
+
return formatStored;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
Object.defineProperty(exports, "moneyAxisFormatter", {
|
|
124
|
+
enumerable: true,
|
|
125
|
+
get: function() {
|
|
126
|
+
return moneyAxisFormatter;
|
|
127
|
+
}
|
|
128
|
+
});
|
package/dist/fx.cjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_money = require("./money-B3wH_Z4S.cjs");
|
|
3
|
+
const require_rounding = require("./rounding-DHBtNCHG.cjs");
|
|
4
|
+
const require_own = require("./own-oP9DXFWv.cjs");
|
|
5
|
+
const require_medusa = require("./medusa-lRY9xlsf.cjs");
|
|
6
|
+
const require_rate = require("./rate-CVeMii4a.cjs");
|
|
7
|
+
const require_retail = require("./retail-Cml8YSKg.cjs");
|
|
8
|
+
//#region src/fx/convert.ts
|
|
9
|
+
const infoOf = (code) => require_money.resolveCurrency(code, { allowHistoric: true });
|
|
10
|
+
/**
|
|
11
|
+
* The factor taking a stored `from` amount to a stored `to` amount for the major rate `r`
|
|
12
|
+
* (1 from = r to): `{ r.n x storageScale(to), r.d x storageScale(from) }`. Replaces Medusa
|
|
13
|
+
* `storedRateFactor`, `regionSwitchFactor`, `conversionFactor`.
|
|
14
|
+
*/
|
|
15
|
+
function convertFactor(from, to, r) {
|
|
16
|
+
const { numerator, denominator } = require_rate.checkRate(r);
|
|
17
|
+
return {
|
|
18
|
+
numerator: numerator * BigInt(infoOf(to).storageScale),
|
|
19
|
+
denominator: denominator * BigInt(infoOf(from).storageScale)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `m` in `to` at the major rate `r` (1 m.currency = r to): exact `m x convertFactor`, rounded
|
|
24
|
+
* once to `unit`, then `retail`. The same currency returns `m` unchanged.
|
|
25
|
+
*/
|
|
26
|
+
function convert(m, to, r, options = {}) {
|
|
27
|
+
const { info: source } = require_money.storedDecimal(m);
|
|
28
|
+
const target = infoOf(to);
|
|
29
|
+
if (source.code === target.code) return m;
|
|
30
|
+
const { numerator, denominator } = convertFactor(source.code, target.code, r);
|
|
31
|
+
const units = require_rounding.mulDivToUnit(BigInt(m.amount), numerator, denominator, target, options.unit ?? "currency", options.rounding);
|
|
32
|
+
return require_money.boundedMoney(options.retail === void 0 ? units : require_retail.retailUnits(units, 1n, target, options.retail), target);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The major rate two prices imply, `major(b) / major(a)`, exactly and in lowest terms (JPY
|
|
36
|
+
* 150000 against USD 1000 gives 150). Both must be positive, else `rate_invalid`.
|
|
37
|
+
*/
|
|
38
|
+
function impliedRate(a, b) {
|
|
39
|
+
const { info: left } = require_money.storedDecimal(a);
|
|
40
|
+
const { info: right } = require_money.storedDecimal(b);
|
|
41
|
+
if (a.amount <= 0 || b.amount <= 0) throw new require_money.MoneyError("rate_invalid", `An implied rate needs two positive amounts, got ${a.amount} and ${b.amount}`);
|
|
42
|
+
const numerator = BigInt(b.amount) * BigInt(left.storageScale);
|
|
43
|
+
const denominator = BigInt(a.amount) * BigInt(right.storageScale);
|
|
44
|
+
const divisor = require_money.gcd(numerator, denominator);
|
|
45
|
+
return {
|
|
46
|
+
numerator: numerator / divisor,
|
|
47
|
+
denominator: denominator / divisor
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/fx/format-rate.ts
|
|
52
|
+
/** `floor(log10(n / d))` of a positive ratio, exactly. */
|
|
53
|
+
function exponentOf(n, d) {
|
|
54
|
+
let e = n.toString().length - d.toString().length;
|
|
55
|
+
const at = (k) => k >= 0 ? require_money.compareRatio(n, d, require_money.pow10(k), 1n) : require_money.compareRatio(n, d, 1n, require_money.pow10(-k));
|
|
56
|
+
if (at(e) < 0) e -= 1;
|
|
57
|
+
return e;
|
|
58
|
+
}
|
|
59
|
+
const asIntlValue = (text) => text;
|
|
60
|
+
/**
|
|
61
|
+
* `'1 USD = 83.42 INR'`. `decimals: 'auto'` (default) shows 4 significant digits with at
|
|
62
|
+
* least 2 and at most 8 fraction digits; a number fixes them. Rounded half-up exactly, then
|
|
63
|
+
* grouped for `locale` (default `'en-US'`).
|
|
64
|
+
*/
|
|
65
|
+
function formatRate(r, from, to, options = {}) {
|
|
66
|
+
const { numerator, denominator } = require_rate.checkRate(r);
|
|
67
|
+
const fromCode = require_money.resolveCurrency(from, { allowHistoric: true }).code;
|
|
68
|
+
const toCode = require_money.resolveCurrency(to, { allowHistoric: true }).code;
|
|
69
|
+
const requested = options.decimals ?? "auto";
|
|
70
|
+
let digits;
|
|
71
|
+
if (requested === "auto") digits = Math.min(8, Math.max(2, 3 - exponentOf(numerator, denominator)));
|
|
72
|
+
else if (Number.isSafeInteger(requested) && requested >= 0 && requested <= 20) digits = requested;
|
|
73
|
+
else throw new require_money.MoneyError("amount_invalid", `decimals must be 'auto' or an integer from 0 to 20, got ${require_money.describeValue(requested)}`);
|
|
74
|
+
const value = require_money.ratioToDecimal(numerator, denominator, digits);
|
|
75
|
+
return `1 ${fromCode} = ${require_money.numberFormat(require_money.resolveLocale(options.locale), {
|
|
76
|
+
minimumFractionDigits: digits,
|
|
77
|
+
maximumFractionDigits: digits
|
|
78
|
+
}).format(asIntlValue(require_money.formatDecimal(value)))} ${toCode}`;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/fx/plausibility.ts
|
|
82
|
+
/** Medusa PRICE_SEED_RATE_MAX_DEVIATION's default. */
|
|
83
|
+
const DEFAULT_RATE_MAX_DEVIATION = 2;
|
|
84
|
+
/** chooseFactor's tolerance between the conventional factor and the observed median. */
|
|
85
|
+
const FACTOR_TOLERANCE = .02;
|
|
86
|
+
/** How far apart two positive rates are, as a factor >= 1 (2 = one is double the other). */
|
|
87
|
+
function deviationFactor(stored, reference) {
|
|
88
|
+
const ratio = stored / reference;
|
|
89
|
+
return ratio >= 1 ? ratio : 1 / ratio;
|
|
90
|
+
}
|
|
91
|
+
const isPositiveFinite$1 = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
92
|
+
/** A positive finite number as an exact Decimal; anything else throws `amount_invalid` naming `name`. */
|
|
93
|
+
function positiveDecimal(n, name) {
|
|
94
|
+
if (!isPositiveFinite$1(n)) throw new require_money.MoneyError("amount_invalid", `${name} must be a positive finite number, got ${require_money.describeValue(n)}`);
|
|
95
|
+
return require_money.decimalFromNumber(n);
|
|
96
|
+
}
|
|
97
|
+
function validRate(r) {
|
|
98
|
+
try {
|
|
99
|
+
return require_rate.checkRate(r);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* One locked rate against the reference, in Medusa `classifyRates` order: invalid (null or not
|
|
106
|
+
* a rate) is refused `invalid`; no reference is unverifiable `no_reference`; no positive
|
|
107
|
+
* reference for the currency is unverifiable `no_pair`; a deviation strictly above
|
|
108
|
+
* `maxDeviation` (default 2) is refused `implausible`; else plausible.
|
|
109
|
+
*/
|
|
110
|
+
function classifyRate(r, code, reference, options = {}) {
|
|
111
|
+
const currency = code.toLowerCase();
|
|
112
|
+
const max = positiveDecimal(options.maxDeviation ?? 2, "maxDeviation");
|
|
113
|
+
const valid = validRate(r);
|
|
114
|
+
if (!valid) return {
|
|
115
|
+
status: "refused",
|
|
116
|
+
reason: "invalid",
|
|
117
|
+
currency,
|
|
118
|
+
stored: NaN
|
|
119
|
+
};
|
|
120
|
+
const stored = require_rate.rateToNumber(valid, { decimals: "exact" });
|
|
121
|
+
if (!reference) return {
|
|
122
|
+
status: "unverifiable",
|
|
123
|
+
reason: "no_reference",
|
|
124
|
+
currency,
|
|
125
|
+
stored
|
|
126
|
+
};
|
|
127
|
+
const ref = require_own.hasOwnKey(reference.rates, currency) ? reference.rates[currency] : void 0;
|
|
128
|
+
if (!isPositiveFinite$1(ref)) return {
|
|
129
|
+
status: "unverifiable",
|
|
130
|
+
reason: "no_pair",
|
|
131
|
+
currency,
|
|
132
|
+
stored
|
|
133
|
+
};
|
|
134
|
+
const refValue = require_money.decimalFromNumber(ref);
|
|
135
|
+
const top = valid.numerator * require_money.pow10(refValue.scale);
|
|
136
|
+
const bottom = valid.denominator * refValue.units;
|
|
137
|
+
const maxD = require_money.pow10(max.scale);
|
|
138
|
+
if (require_money.compareRatio(top, bottom, max.units, maxD) > 0 || require_money.compareRatio(bottom, top, max.units, maxD) > 0) return {
|
|
139
|
+
status: "refused",
|
|
140
|
+
reason: "implausible",
|
|
141
|
+
currency,
|
|
142
|
+
stored,
|
|
143
|
+
reference: ref,
|
|
144
|
+
factor: deviationFactor(stored, ref),
|
|
145
|
+
source: reference.source
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
status: "plausible",
|
|
149
|
+
currency,
|
|
150
|
+
stored
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** `|value - c| / c <= tolerance`, exactly (`c` positive). */
|
|
154
|
+
function closeTo(value, c, tolerance) {
|
|
155
|
+
const scaledC = c.numerator * require_money.pow10(value.scale);
|
|
156
|
+
const gap = value.units * c.denominator - scaledC;
|
|
157
|
+
return require_money.compareRatio((gap < 0n ? -gap : gap) * require_money.pow10(tolerance.scale), 1n, tolerance.units * scaledC, 1n) <= 0;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The storage factor confirmed by the store's own prices (Medusa `chooseFactor`): the
|
|
161
|
+
* candidates `[c, c / 100, c x 100]` against the upper median of the finite positive observed
|
|
162
|
+
* ratios; the first within `tolerance` (default 0.02) wins, none gives null. No observations
|
|
163
|
+
* trusts `conventional`.
|
|
164
|
+
*/
|
|
165
|
+
function confirmStoredFactor(conventional, observedRatios, options = {}) {
|
|
166
|
+
const c = require_rate.checkRate(conventional);
|
|
167
|
+
const tolerance = require_money.decimalFromNumber(nonNegative(options.tolerance ?? .02));
|
|
168
|
+
const ratios = observedRatios.filter(isPositiveFinite$1).sort((a, b) => a - b);
|
|
169
|
+
if (ratios.length === 0) return c;
|
|
170
|
+
const median = require_money.decimalFromNumber(ratios[Math.floor(ratios.length / 2)]);
|
|
171
|
+
return [
|
|
172
|
+
c,
|
|
173
|
+
{
|
|
174
|
+
numerator: c.numerator,
|
|
175
|
+
denominator: c.denominator * 100n
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
numerator: c.numerator * 100n,
|
|
179
|
+
denominator: c.denominator
|
|
180
|
+
}
|
|
181
|
+
].find((candidate) => closeTo(median, candidate, tolerance)) ?? null;
|
|
182
|
+
}
|
|
183
|
+
function nonNegative(n) {
|
|
184
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < 0) throw new require_money.MoneyError("amount_invalid", `tolerance must be a non-negative finite number, got ${require_money.describeValue(n)}`);
|
|
185
|
+
return n;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The upper median of `rates` and which of them lie within `tolerance` (default 0.01) of it:
|
|
189
|
+
* accepted iff `|r / median - 1| <= tolerance`, exactly. No rates: `{ median: null, accepted: [] }`.
|
|
190
|
+
*/
|
|
191
|
+
function rateConsensus(rates, options = {}) {
|
|
192
|
+
const valid = rates.map(require_rate.checkRate);
|
|
193
|
+
const tolerance = require_money.decimalFromNumber(nonNegative(options.tolerance ?? .01));
|
|
194
|
+
if (valid.length === 0) return {
|
|
195
|
+
median: null,
|
|
196
|
+
accepted: []
|
|
197
|
+
};
|
|
198
|
+
const sorted = [...valid].sort((a, b) => require_money.compareRatio(a.numerator, a.denominator, b.numerator, b.denominator));
|
|
199
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
200
|
+
return {
|
|
201
|
+
median,
|
|
202
|
+
accepted: valid.map((r) => {
|
|
203
|
+
const gap = r.numerator * median.denominator - median.numerator * r.denominator;
|
|
204
|
+
const scale = require_money.pow10(tolerance.scale);
|
|
205
|
+
return require_money.compareRatio((gap < 0n ? -gap : gap) * scale, 1n, tolerance.units * median.numerator * r.denominator, 1n) <= 0;
|
|
206
|
+
})
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/fx/reporting.ts
|
|
211
|
+
const FACTOR_DIGITS = 20;
|
|
212
|
+
/**
|
|
213
|
+
* The reporting factors of a store whose locked rates say `1 default = n/d <code>`:
|
|
214
|
+
* `factor(code) = storageScale(default) / storageScale(code) x d / n`, rounded half-up to 20
|
|
215
|
+
* fraction digits; the default currency is `'1'`. Rows for the default currency, unknown
|
|
216
|
+
* codes or invalid rates (see `rateFromRow`) are skipped, so those currencies have no factor.
|
|
217
|
+
* Port of Medusa `storeTotalFactors`, exact (it measured the factor at a 1e9 reference).
|
|
218
|
+
*/
|
|
219
|
+
function storeFactors(defaultCurrency, rows) {
|
|
220
|
+
const base = require_money.resolveCurrency(defaultCurrency, { allowHistoric: true });
|
|
221
|
+
const factors = { [base.code.toLowerCase()]: "1" };
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
const info = require_money.findCurrency(row?.currency_code, { allowHistoric: true });
|
|
224
|
+
const r = require_rate.rateFromRow(row);
|
|
225
|
+
if (!info || !r || info.code === base.code) continue;
|
|
226
|
+
const units = require_money.mulDiv(BigInt(base.storageScale) * r.denominator, require_money.pow10(FACTOR_DIGITS), BigInt(info.storageScale) * r.numerator, "half-up");
|
|
227
|
+
factors[info.code.toLowerCase()] = require_money.toDecimalText({
|
|
228
|
+
units,
|
|
229
|
+
scale: FACTOR_DIGITS
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
base: base.code,
|
|
234
|
+
factors: Object.freeze(factors)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The sum of `items` in `table.base`: each converted with its currency's factor and rounded
|
|
239
|
+
* half-up to a storage unit (per item, as customer-stats rounds per order), then summed.
|
|
240
|
+
* Items whose currency has no factor are excluded and reported in `missing`.
|
|
241
|
+
*/
|
|
242
|
+
function toReporting(items, table) {
|
|
243
|
+
const base = require_money.resolveCurrency(table.base, { allowHistoric: true });
|
|
244
|
+
const missing = /* @__PURE__ */ new Set();
|
|
245
|
+
let total = 0n;
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
const { info } = require_money.storedDecimal(item);
|
|
248
|
+
const key = info.code.toLowerCase();
|
|
249
|
+
const text = require_own.hasOwnKey(table.factors, key) ? table.factors[key] : void 0;
|
|
250
|
+
if (text === void 0) {
|
|
251
|
+
missing.add(info.code);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const factor = require_money.decimalOf(text, false, `factors.${key}`);
|
|
255
|
+
total += require_money.mulDiv(BigInt(item.amount), factor.units, require_money.pow10(factor.scale), "half-up");
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
total: require_money.boundedMoney(total, base),
|
|
259
|
+
complete: missing.size === 0,
|
|
260
|
+
missing: [...missing].sort()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/fx/settings.ts
|
|
265
|
+
const RATE_MODES = ["manual", "dynamic"];
|
|
266
|
+
const PRICE_ROUNDINGS = ["none", "nearest"];
|
|
267
|
+
/** `store_currency_rate.rate_mode` as readers coerce it: `'manual'` exactly, else `'dynamic'`. */
|
|
268
|
+
function parseRateMode(value) {
|
|
269
|
+
return value === "manual" ? "manual" : "dynamic";
|
|
270
|
+
}
|
|
271
|
+
/** `store_currency_rate.rounding` as readers coerce it: `'nearest'` exactly, else `'none'`. */
|
|
272
|
+
function parsePriceRounding(value) {
|
|
273
|
+
return value === "nearest" ? "nearest" : "none";
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The amount a per-currency setting holds for `currency`: the listed entry, else the explicit
|
|
277
|
+
* fallback: `'refuse'` gives null; `'major'` is that many major units of `currency` itself
|
|
278
|
+
* (the store-credit cap `{ kind: 'major', amount: '15000' }`); `'convert'` converts the
|
|
279
|
+
* `from` entry at `rate` (null when there is no `from` entry). Never a silent default.
|
|
280
|
+
*/
|
|
281
|
+
function resolveMoneyByCurrency(setting, currency, options) {
|
|
282
|
+
const info = require_money.resolveCurrency(currency, { allowHistoric: true });
|
|
283
|
+
const listed = require_medusa.moneyFor(setting, info.code);
|
|
284
|
+
if (listed) return {
|
|
285
|
+
money: listed,
|
|
286
|
+
source: "listed"
|
|
287
|
+
};
|
|
288
|
+
const fallback = options.fallback;
|
|
289
|
+
if (fallback.kind === "major") return {
|
|
290
|
+
money: require_money.fromMajor(fallback.amount, info.code),
|
|
291
|
+
source: "fallback"
|
|
292
|
+
};
|
|
293
|
+
if (fallback.kind === "convert") {
|
|
294
|
+
const base = require_medusa.moneyFor(setting, fallback.from);
|
|
295
|
+
return base ? {
|
|
296
|
+
money: convert(base, info.code, fallback.rate, { retail: fallback.retail }),
|
|
297
|
+
source: "fallback"
|
|
298
|
+
} : null;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/fx/tables.ts
|
|
304
|
+
/** 1 USD = rate units of the currency, exact (see data/pegs.ts for the sources). Upper-case keys. */
|
|
305
|
+
const USD_PEGS = Object.freeze(Object.fromEntries(Object.entries({
|
|
306
|
+
AED: [36725n, 10000n],
|
|
307
|
+
SAR: [375n, 100n],
|
|
308
|
+
QAR: [364n, 100n],
|
|
309
|
+
BHD: [376n, 1000n],
|
|
310
|
+
OMR: [3845n, 10000n]
|
|
311
|
+
}).map(([code, [numerator, denominator]]) => [code, Object.freeze({
|
|
312
|
+
numerator,
|
|
313
|
+
denominator
|
|
314
|
+
})])));
|
|
315
|
+
const isPositiveFinite = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
316
|
+
/** A feed's rate table with lower-case keys and only finite positive numbers kept (Medusa `lowerKeys`). Never throws. */
|
|
317
|
+
function sanitizeRateTable(raw) {
|
|
318
|
+
const out = {};
|
|
319
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
|
|
320
|
+
for (const [key, value] of Object.entries(raw)) if (isPositiveFinite(value)) out[key.toLowerCase()] = value;
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/** `{ ...fallback, ...primary }`: the primary feed (ECB) wins every currency it publishes. */
|
|
324
|
+
function mergeRateTables(primary, fallback) {
|
|
325
|
+
return {
|
|
326
|
+
...fallback,
|
|
327
|
+
...primary
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function own(table, code) {
|
|
331
|
+
for (const key of [code.toUpperCase(), code.toLowerCase()]) if (require_own.hasOwnKey(table, key)) return table[key];
|
|
332
|
+
}
|
|
333
|
+
/** Units of `code` per 1 USD: the feed's finite positive value, else its peg, else undefined. */
|
|
334
|
+
function perUsd(code, ratesPerUsd, pegs) {
|
|
335
|
+
const live = own(ratesPerUsd, code);
|
|
336
|
+
if (isPositiveFinite(live)) {
|
|
337
|
+
const d = require_money.decimalFromNumber(live);
|
|
338
|
+
return {
|
|
339
|
+
numerator: d.units,
|
|
340
|
+
denominator: require_money.pow10(d.scale)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const peg = own(pegs, code);
|
|
344
|
+
return peg === void 0 ? void 0 : require_rate.checkRate(peg);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The rate 1 `from` = ? `to` when the live pair is unavailable, through the USD pegs
|
|
348
|
+
* (orch `resolveRateMajor`): a pegged `to` gives the peg from USD, else `(from -> USD) x peg`
|
|
349
|
+
* with `from -> USD` read from `ratesPerUsd` (units per 1 USD, any key case) or from `from`'s
|
|
350
|
+
* own peg; a pegged `from` into USD gives the inverse peg (new, symmetric). The same code
|
|
351
|
+
* gives 1/1; anything else null.
|
|
352
|
+
*/
|
|
353
|
+
function pegFallback(from, to, ratesPerUsd, pegs = USD_PEGS) {
|
|
354
|
+
const f = from.toUpperCase();
|
|
355
|
+
const t = to.toUpperCase();
|
|
356
|
+
if (f === t) return {
|
|
357
|
+
numerator: 1n,
|
|
358
|
+
denominator: 1n
|
|
359
|
+
};
|
|
360
|
+
const toPeg = own(pegs, t);
|
|
361
|
+
if (toPeg !== void 0) {
|
|
362
|
+
const peg = require_rate.checkRate(toPeg);
|
|
363
|
+
if (f === "USD") return peg;
|
|
364
|
+
const source = perUsd(f, ratesPerUsd, pegs);
|
|
365
|
+
if (!source) return null;
|
|
366
|
+
return {
|
|
367
|
+
numerator: peg.numerator * source.denominator,
|
|
368
|
+
denominator: peg.denominator * source.numerator
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const fromPeg = own(pegs, f);
|
|
372
|
+
return fromPeg !== void 0 && t === "USD" ? require_rate.invertRate(fromPeg) : null;
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
exports.DEFAULT_RATE_MAX_DEVIATION = DEFAULT_RATE_MAX_DEVIATION;
|
|
376
|
+
exports.FACTOR_TOLERANCE = FACTOR_TOLERANCE;
|
|
377
|
+
exports.PRICE_ROUNDINGS = PRICE_ROUNDINGS;
|
|
378
|
+
exports.RATE_BOUNDS = require_rate.RATE_BOUNDS;
|
|
379
|
+
exports.RATE_MODES = RATE_MODES;
|
|
380
|
+
exports.RATE_REF = require_rate.RATE_REF;
|
|
381
|
+
exports.USD_PEGS = USD_PEGS;
|
|
382
|
+
exports.classifyRate = classifyRate;
|
|
383
|
+
exports.confirmStoredFactor = confirmStoredFactor;
|
|
384
|
+
exports.convert = convert;
|
|
385
|
+
exports.convertFactor = convertFactor;
|
|
386
|
+
exports.crossRate = require_rate.crossRate;
|
|
387
|
+
exports.deviationFactor = deviationFactor;
|
|
388
|
+
exports.formatRate = formatRate;
|
|
389
|
+
exports.impliedRate = impliedRate;
|
|
390
|
+
exports.invertRate = require_rate.invertRate;
|
|
391
|
+
exports.mergeRateTables = mergeRateTables;
|
|
392
|
+
exports.parsePriceRounding = parsePriceRounding;
|
|
393
|
+
exports.parseRateMode = parseRateMode;
|
|
394
|
+
exports.pegFallback = pegFallback;
|
|
395
|
+
exports.priceRoundingParams = require_retail.priceRoundingParams;
|
|
396
|
+
exports.rate = require_rate.rate;
|
|
397
|
+
exports.rateConsensus = rateConsensus;
|
|
398
|
+
exports.rateEquals = require_rate.rateEquals;
|
|
399
|
+
exports.rateFromDecimal = require_rate.rateFromDecimal;
|
|
400
|
+
exports.rateFromRow = require_rate.rateFromRow;
|
|
401
|
+
exports.rateToDecimal = require_rate.rateToDecimal;
|
|
402
|
+
exports.rateToNumber = require_rate.rateToNumber;
|
|
403
|
+
exports.resolveMoneyByCurrency = resolveMoneyByCurrency;
|
|
404
|
+
exports.roundRetail = require_retail.roundRetail;
|
|
405
|
+
exports.sanitizeRateTable = sanitizeRateTable;
|
|
406
|
+
exports.storeFactors = storeFactors;
|
|
407
|
+
exports.toReporting = toReporting;
|