@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.
Files changed (78) hide show
  1. package/README.md +401 -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-CZRg4TOP.cjs +222 -0
  7. package/dist/allocate-X7_jAtOj.js +199 -0
  8. package/dist/amount-BV0O-aN8.cjs +241 -0
  9. package/dist/amount-BoYBMgK9.js +200 -0
  10. package/dist/arithmetic-BhNwisjb.js +132 -0
  11. package/dist/arithmetic-DjT4Kzgt.cjs +233 -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-BdPnijvn.js +99 -0
  21. package/dist/format-DLhno7eX.cjs +128 -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 +352 -0
  27. package/dist/index.d.cts +300 -0
  28. package/dist/index.d.ts +300 -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-CqkWDxHS.js +48 -0
  35. package/dist/iso-minor-wSwJN57T.cjs +65 -0
  36. package/dist/medusa-4K8N1pUc.d.cts +87 -0
  37. package/dist/medusa-DBXJXXvF.d.ts +87 -0
  38. package/dist/medusa-DqJY1qpj.js +116 -0
  39. package/dist/medusa-lRY9xlsf.cjs +175 -0
  40. package/dist/money-B-3aFjNJ.d.cts +248 -0
  41. package/dist/money-B-3aFjNJ.d.ts +248 -0
  42. package/dist/money-B3wH_Z4S.cjs +3990 -0
  43. package/dist/money-WhnGiI00.js +3583 -0
  44. package/dist/number-text-BWu67jUq.cjs +145 -0
  45. package/dist/number-text-DE-q4jO5.js +134 -0
  46. package/dist/own-D7544D3k.js +4 -0
  47. package/dist/own-oP9DXFWv.cjs +9 -0
  48. package/dist/percent-C0InC2t2.cjs +43 -0
  49. package/dist/percent-D1VB9xXG.js +20 -0
  50. package/dist/rate-CVeMii4a.cjs +217 -0
  51. package/dist/rate-DBPiYgT1.d.ts +63 -0
  52. package/dist/rate-DicStBue.js +152 -0
  53. package/dist/rate-tqK8ijG0.d.cts +63 -0
  54. package/dist/reporting-CG-ai2sM.d.cts +31 -0
  55. package/dist/reporting-tpGJbqYp.d.ts +31 -0
  56. package/dist/retail-1mu888J1.js +69 -0
  57. package/dist/retail-C_BPLvMj.d.cts +31 -0
  58. package/dist/retail-Cml8YSKg.cjs +86 -0
  59. package/dist/retail-DRUTTQoJ.d.ts +31 -0
  60. package/dist/rounding-DHBtNCHG.cjs +79 -0
  61. package/dist/rounding-g9rvye0k.js +50 -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 +61995 -0
  67. package/dist/testing.d.cts +125 -0
  68. package/dist/testing.d.ts +125 -0
  69. package/dist/testing.js +61986 -0
  70. package/dist/wire-Bhuut8cW.cjs +2097 -0
  71. package/dist/wire-CaUN9Vf_.d.ts +309 -0
  72. package/dist/wire-sW0yzqth.js +1888 -0
  73. package/dist/wire-xjYXv4go.d.cts +309 -0
  74. package/dist/wire.cjs +37 -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
@@ -0,0 +1,516 @@
1
+ import { $ as describeValue, B as decimalOf, E as resolveCurrency, Q as MoneyError, X as mulDiv, Z as ratioToDecimal, dt as roundToScale, et as addDecimal, i as medusaAmountDecimal, pt as toDecimalText, rt as divRound, s as storedDecimal, st as multiplyDecimal, t as boundedMoney, ut as pow10 } from "./money-WhnGiI00.js";
2
+ import { a as unitSize, r as roundStoredDecimal, t as mulDivToUnit } from "./rounding-g9rvye0k.js";
3
+ import { n as allocateUnits, t as allocate } from "./allocate-X7_jAtOj.js";
4
+ import { i as percentRatio, n as hundredPercent, t as basisPointsOf } from "./percent-D1VB9xXG.js";
5
+ import { t as formatMoney } from "./format-BdPnijvn.js";
6
+ import { a as readStoredDecimal, c as storedFieldValue } from "./medusa-DqJY1qpj.js";
7
+ import { n as retailUnits } from "./retail-1mu888J1.js";
8
+ //#region src/commerce/convention.ts
9
+ /**
10
+ * Currencies with storage digits: step 1, `'x100'`. Zero-decimal currencies: step 100 when
11
+ * there is a positive sample and every positive sample is a multiple of 100 (the platform
12
+ * convention), else step 1 `'whole-unit'` (never truncate a whole-unit store to hundreds).
13
+ */
14
+ function storageConventionOf(currency, samples) {
15
+ const info = resolveCurrency(currency, { allowHistoric: true });
16
+ for (const s of samples) storedDecimal(s);
17
+ if (info.storageDigits !== 0) return {
18
+ step: 1,
19
+ convention: "x100"
20
+ };
21
+ const step = unitSize(info, "currency");
22
+ const positive = samples.filter((s) => s.amount > 0);
23
+ return positive.length > 0 && positive.every((s) => BigInt(s.amount) % step === 0n) ? {
24
+ step: 100,
25
+ convention: "x100"
26
+ } : {
27
+ step: 1,
28
+ convention: "whole-unit"
29
+ };
30
+ }
31
+ //#endregion
32
+ //#region src/commerce/shared.ts
33
+ /** The one currency of `amounts` (the first defined one decides) and their stored integers. Throws `currency_mismatch`. */
34
+ function oneCurrency(amounts, verb) {
35
+ let info;
36
+ const units = amounts.map((m) => {
37
+ const own = storedDecimal(m).info;
38
+ if (info && own.code !== info.code) throw new MoneyError("currency_mismatch", `Cannot ${verb} ${info.code} and ${own.code}`);
39
+ info = info ?? own;
40
+ return BigInt(m.amount);
41
+ });
42
+ return {
43
+ info,
44
+ units
45
+ };
46
+ }
47
+ /** A percent (or any plain decimal) as an exact Decimal; `parse` / `amount_invalid` as `decimalOf`. */
48
+ function percentValue(value, field) {
49
+ return decimalOf(value, true, field);
50
+ }
51
+ /** `value` clamped to [0, 100] percent. */
52
+ function clampPercent(value) {
53
+ if (value.units < 0n) return {
54
+ units: 0n,
55
+ scale: 0
56
+ };
57
+ return value.units > hundredPercent(value.scale) ? {
58
+ units: 100n,
59
+ scale: 0
60
+ } : value;
61
+ }
62
+ /** A positive safe integer, else `amount_invalid`. */
63
+ function positiveInteger(value, name) {
64
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) throw new MoneyError("amount_invalid", `${name} must be a positive integer, got ${describeValue(value)}`);
65
+ return BigInt(value);
66
+ }
67
+ /** A non-negative safe integer, else `amount_invalid`. */
68
+ function nonNegativeInteger(value, name) {
69
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new MoneyError("amount_invalid", `${name} must be a non-negative integer, got ${describeValue(value)}`);
70
+ return BigInt(value);
71
+ }
72
+ /** `decimals` as a digit count from 0 to 20, else `amount_invalid`. */
73
+ function digitsOf(decimals, name = "decimals") {
74
+ if (typeof decimals !== "number" || !Number.isSafeInteger(decimals) || decimals < 0 || decimals > 20) throw new MoneyError("amount_invalid", `${name} must be an integer from 0 to 20, got ${describeValue(decimals)}`);
75
+ return decimals;
76
+ }
77
+ //#endregion
78
+ //#region src/commerce/cost.ts
79
+ const scaleOf = (scale) => digitsOf(scale ?? 4, "scale");
80
+ function quantityOf(value, name) {
81
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) throw new MoneyError("amount_invalid", `${name} must be an integer, got ${describeValue(value)}`);
82
+ return BigInt(value);
83
+ }
84
+ /**
85
+ * Extra costs (freight, duties) spread over receipt lines by quantity, exactly: `extras`
86
+ * (rounded half-up to `scale`, default 4) is allocated with the last line absorbing the
87
+ * remainder; each line's landed unit cost is `max(0, round(unitCost + share / quantity))` at
88
+ * `scale`. Non-positive extras or quantities spread nothing.
89
+ */
90
+ function landedUnitCosts(lines, extras, options = {}) {
91
+ const scale = scaleOf(options.scale);
92
+ const quantities = lines.map((l, i) => quantityOf(l.quantity, `lines[${i}].quantity`));
93
+ const pot = roundToScale(decimalOf(extras, true, "extras"), scale, "half-up");
94
+ const shares = pot.units > 0n && quantities.every((q) => q >= 0n) && quantities.some((q) => q > 0n) ? allocate(toDecimalText(pot), quantities.map((q) => Number(q)), {
95
+ strategy: "last",
96
+ scale
97
+ }) : lines.map(() => "0");
98
+ return lines.map((line, i) => {
99
+ const unit = decimalOf(line.unitCost, true, `lines[${i}].unitCost`);
100
+ const share = roundToScale(decimalOf(shares[i]), scale, "down").units;
101
+ const q = quantities[i];
102
+ const total = addDecimal(multiplyDecimal(unit, {
103
+ units: q,
104
+ scale: 0
105
+ }), {
106
+ units: share,
107
+ scale
108
+ });
109
+ const landed = q > 0n ? ratioToDecimal(total.units, pow10(total.scale) * q, scale) : roundToScale(unit, scale, "half-up");
110
+ return {
111
+ share: shares[i],
112
+ landedUnitCost: toDecimalText({
113
+ units: landed.units > 0n ? landed.units : 0n,
114
+ scale
115
+ })
116
+ };
117
+ });
118
+ }
119
+ /**
120
+ * The weighted-average unit cost after a receipt: the receipt's cost (half-up to `scale`,
121
+ * default 4) when there is no average, nothing on hand or nothing received; else
122
+ * `(onHand x average + quantity x unitCost) / (onHand + quantity)` half-up to `scale`.
123
+ */
124
+ function weightedAverageCost(current, receipt, options = {}) {
125
+ const scale = scaleOf(options.scale);
126
+ const onHand = quantityOf(current.onHand, "onHand");
127
+ const quantity = quantityOf(receipt.quantity, "quantity");
128
+ const cost = decimalOf(receipt.unitCost, true, "unitCost");
129
+ if (current.average === null || onHand <= 0n || quantity <= 0n) return toDecimalText(roundToScale(cost, scale, "half-up"));
130
+ const average = decimalOf(current.average, true, "average");
131
+ const held = multiplyDecimal({
132
+ units: onHand,
133
+ scale: 0
134
+ }, average);
135
+ const sum = addDecimal(held, multiplyDecimal({
136
+ units: quantity,
137
+ scale: 0
138
+ }, cost));
139
+ return toDecimalText(ratioToDecimal(sum.units, pow10(sum.scale) * (onHand + quantity), scale));
140
+ }
141
+ //#endregion
142
+ //#region src/commerce/discount.ts
143
+ const isMoney = (value) => typeof value === "object" && value !== null;
144
+ /**
145
+ * The discount `spec` takes off `base` (a line or order total), clamped to `[0, base]`; a
146
+ * non-positive base gives 0. A percentage of the total rounds once (`per: 'total'`) or per
147
+ * unit then times the quantity (`per: 'unit'`: 10% of 2 x 18.99 truncated is 2 x 1.89); a
148
+ * fixed amount is Money in base's currency, per unit times the quantity with `per: 'unit'`.
149
+ */
150
+ function discountAmount(spec, base, options = {}) {
151
+ const { info } = storedDecimal(base);
152
+ const total = BigInt(base.amount);
153
+ const quantity = options.per === "unit" ? positiveInteger(options.quantity, "quantity") : 1n;
154
+ let amount;
155
+ if (spec?.type === "percentage") {
156
+ if (isMoney(spec.value)) throw new MoneyError("amount_invalid", "A percentage discount takes a percent, not Money");
157
+ const pct = clampPercent(percentValue(spec.value, "value"));
158
+ const denominator = hundredPercent(pct.scale) * quantity;
159
+ amount = mulDivToUnit(total, pct.units, denominator, info, options.unit ?? "currency", options.rounding) * quantity;
160
+ } else if (spec?.type === "fixed") {
161
+ if (!isMoney(spec.value)) throw new MoneyError("amount_invalid", `A fixed discount takes Money, got ${describeValue(spec.value)}`);
162
+ amount = oneCurrency([base, spec.value], "discount").units[1] * quantity;
163
+ } else throw new MoneyError("amount_invalid", `Unknown discount type: ${describeValue(spec?.type)}`);
164
+ if (total <= 0n || amount <= 0n) return boundedMoney(0n, info);
165
+ return boundedMoney(amount < total ? amount : total, info);
166
+ }
167
+ /**
168
+ * The reward of a buy X get Y offer on one line: `min(quantity, maxQuantity)` discounted
169
+ * units, each `percent` of the unit price (rounded once on the total to the currency unit) or
170
+ * a fixed amount off each, never more than the unit price (promotion `fixBuyGetFixedRewards`).
171
+ * The units come from the input, never recovered from an amount.
172
+ */
173
+ function buyGetReward(input) {
174
+ const { info } = storedDecimal(input.unitPrice);
175
+ const price = BigInt(input.unitPrice.amount);
176
+ const quantity = nonNegativeInteger(input.quantity, "quantity");
177
+ const cap = input.maxQuantity === void 0 ? quantity : nonNegativeInteger(input.maxQuantity, "maxQuantity");
178
+ const units = quantity < cap ? quantity : cap;
179
+ let amount;
180
+ if (input.reward?.kind === "percent") {
181
+ const pct = clampPercent(percentValue(input.reward.value, "reward.value"));
182
+ amount = mulDivToUnit(price * units, pct.units, hundredPercent(pct.scale), info, "currency");
183
+ } else if (input.reward?.kind === "fixed-each") {
184
+ const off = oneCurrency([input.unitPrice, input.reward.value], "reward").units[1];
185
+ amount = (off < price ? off : price) * units;
186
+ } else throw new MoneyError("amount_invalid", `Unknown reward kind: ${describeValue(input.reward?.kind)}`);
187
+ return {
188
+ amount: boundedMoney(amount > 0n ? amount : 0n, info),
189
+ discountedUnits: Number(units)
190
+ };
191
+ }
192
+ /** `'10%'`, `'12.5%'` (up to 2 decimals, half-up) or the fixed amount via `formatMoney` (its currency must be `currency`). */
193
+ function formatDiscount(spec, currency, options = {}) {
194
+ const info = resolveCurrency(currency, { allowHistoric: true });
195
+ if (spec?.type === "percentage" && !isMoney(spec.value)) {
196
+ const pct = percentValue(spec.value, "value");
197
+ return `${toDecimalText(roundToScale(pct, 2, "half-up"))}%`;
198
+ }
199
+ if (spec?.type === "fixed" && isMoney(spec.value)) {
200
+ const own = storedDecimal(spec.value).info;
201
+ if (own.code !== info.code) throw new MoneyError("currency_mismatch", `Discount in ${own.code}, shown in ${info.code}`);
202
+ return formatMoney(spec.value, { locale: options.locale });
203
+ }
204
+ throw new MoneyError("amount_invalid", `Not a discount: ${describeValue(spec)}`);
205
+ }
206
+ //#endregion
207
+ //#region src/commerce/order-value.ts
208
+ const negate = (a) => ({
209
+ units: -a.units,
210
+ scale: a.scale
211
+ });
212
+ const ZERO = {
213
+ units: 0n,
214
+ scale: 0
215
+ };
216
+ /** Sum of lines `max(0, unit_price x quantity - sum(adjustments))`; null when any part is unreadable. */
217
+ function itemsValue(items) {
218
+ let total = ZERO;
219
+ for (const item of items) {
220
+ const price = medusaAmountDecimal(item?.unit_price);
221
+ const quantity = medusaAmountDecimal(item?.quantity);
222
+ if (!price || !quantity) return null;
223
+ let line = multiplyDecimal(price, quantity);
224
+ for (const adjustment of item.adjustments ?? []) {
225
+ const amount = medusaAmountDecimal(adjustment?.amount);
226
+ if (!amount) return null;
227
+ line = addDecimal(line, negate(amount));
228
+ }
229
+ if (line.units > 0n) total = addDecimal(total, line);
230
+ }
231
+ return total;
232
+ }
233
+ /** An optional total: absent (null/undefined) is 0, unreadable is null. */
234
+ const optional = (raw) => raw === null || raw === void 0 ? ZERO : medusaAmountDecimal(raw);
235
+ /**
236
+ * The value an order reports to ad platforms, and where it came from: the graph total when
237
+ * trusted and positive, else the summary total (a genuine 0 or a negative is kept), else the
238
+ * items netted of adjustments when positive, else null. The result is rounded half-up to a
239
+ * storage unit. Skipping a send when the value is not positive (GA4) stays an app policy.
240
+ */
241
+ function resolveOrderValue(input) {
242
+ const info = resolveCurrency(input.currency, { allowHistoric: true });
243
+ const toMoney = (d) => roundStoredDecimal(d, info);
244
+ const net = (total) => {
245
+ if (input.basis !== "net-of-tax-shipping") return total;
246
+ const tax = optional(input.taxTotal);
247
+ const shipping = optional(input.shippingTotal);
248
+ return tax && shipping ? addDecimal(total, negate(addDecimal(tax, shipping))) : null;
249
+ };
250
+ const graph = input.trustGraph ? medusaAmountDecimal(input.graphTotal) : null;
251
+ if (graph && graph.units > 0n) {
252
+ const value = net(graph);
253
+ return value ? {
254
+ value: toMoney(value),
255
+ source: "graph"
256
+ } : null;
257
+ }
258
+ const summary = medusaAmountDecimal(input.summaryTotal);
259
+ if (summary) {
260
+ const value = net(summary);
261
+ return value ? {
262
+ value: toMoney(value),
263
+ source: "summary"
264
+ } : null;
265
+ }
266
+ const items = input.items ? itemsValue(input.items) : null;
267
+ return items && items.units > 0n ? {
268
+ value: toMoney(items),
269
+ source: "items"
270
+ } : null;
271
+ }
272
+ //#endregion
273
+ //#region src/commerce/payable.ts
274
+ /**
275
+ * `false` when the cart can complete with no payment (`total <= 0` and no negative credit-line
276
+ * balance: `credit_line_total >= 0 && total <= 0`), `true` when it must be paid, `undefined`
277
+ * when the total is not a finite number (still loading: fail closed, never "free"). As in the
278
+ * starter, only a number counts as a total, and an absent credit-line total counts as none.
279
+ */
280
+ function requiresPayment(input, currency) {
281
+ resolveCurrency(currency, { allowHistoric: true });
282
+ const total = input?.total;
283
+ if (typeof total !== "number" || !Number.isFinite(total)) return void 0;
284
+ const credit = input.creditLineTotal;
285
+ return !(total <= 0 && (credit == null || credit >= 0));
286
+ }
287
+ //#endregion
288
+ //#region src/commerce/pricing.ts
289
+ /**
290
+ * `m` with a markup: exact percent and flat in `order`, one rounding to a storage unit, then
291
+ * clamped to `[floor, ceiling]`, then to `>= 0`. A non-positive `m` (a failed quote) is
292
+ * returned unchanged.
293
+ */
294
+ function applyMarkup(m, options) {
295
+ const { info } = oneCurrency([m, ...[
296
+ options.flat,
297
+ options.floor,
298
+ options.ceiling
299
+ ].filter((x) => x !== void 0)], "mark up");
300
+ const amount = BigInt(m.amount);
301
+ if (amount <= 0n) return m;
302
+ const pct = percentValue(options.percent ?? 0, "percent");
303
+ const scale = hundredPercent(pct.scale);
304
+ const factor = scale + pct.units;
305
+ const flat = options.flat ? BigInt(options.flat.amount) : 0n;
306
+ const rounding = options.rounding ?? "half-up";
307
+ let value = options.order === "flat-then-percent" ? mulDiv(amount + flat, factor, scale, rounding) : divRound(amount * factor + flat * scale, scale, rounding);
308
+ if (options.floor && value < BigInt(options.floor.amount)) value = BigInt(options.floor.amount);
309
+ if (options.ceiling && value > BigInt(options.ceiling.amount)) value = BigInt(options.ceiling.amount);
310
+ return boundedMoney(value > 0n ? value : 0n, info);
311
+ }
312
+ /**
313
+ * The one retail price:
314
+ * - AliExpress (`markup: { percent }`): `cost x (1 + percent / 100)`, exact, then `rounding`
315
+ * once (`'none'`: half-up to the currency unit; `'.99'`, `'.95'`, `'whole'`: up to the ending).
316
+ * - Domains (`markup: { bps, capTiers }`, integer cents): `markup = min(round(cost x bps / 1e4),
317
+ * cap)` with the cap of the first tier whose `below` the cost is under (or that has none);
318
+ * `subtotal = cost + markup + extras`; `fee = ceil(subtotal x feeBps / 1e4) + fixed`; then
319
+ * `max(roundRetail(subtotal + fee, rounding), floor)` (`'up-to-99'` for domains).
320
+ */
321
+ function retailPrice(input) {
322
+ const tiers = "bps" in input.markup ? input.markup.capTiers ?? [] : [];
323
+ const optional = [
324
+ input.extras,
325
+ input.fee?.fixed,
326
+ input.floor,
327
+ ...tiers.flatMap((t) => [t.cap, t.below])
328
+ ];
329
+ const { info } = oneCurrency([input.cost, ...optional.filter((x) => x !== void 0)], "price");
330
+ const cost = BigInt(input.cost.amount);
331
+ let p;
332
+ let q;
333
+ if ("percent" in input.markup) {
334
+ const pct = percentValue(input.markup.percent, "markup.percent");
335
+ q = hundredPercent(pct.scale);
336
+ p = cost * (q + pct.units);
337
+ } else if ("bps" in input.markup) {
338
+ const bps = nonNegativeInteger(input.markup.bps, "markup.bps");
339
+ let markup = basisPointsOf(cost, bps, "half-up");
340
+ const tier = tiers.find((t) => t.below === void 0 || cost < BigInt(t.below.amount));
341
+ if (tier && markup > BigInt(tier.cap.amount)) markup = BigInt(tier.cap.amount);
342
+ p = cost + markup;
343
+ q = 1n;
344
+ } else throw new MoneyError("amount_invalid", `markup needs percent or bps, got ${describeValue(input.markup)}`);
345
+ if (input.extras) p += BigInt(input.extras.amount) * q;
346
+ if (input.fee) {
347
+ const feeBps = nonNegativeInteger(input.fee.bps, "fee.bps");
348
+ const fee = basisPointsOf(p, feeBps, "ceil", q) + BigInt(input.fee.fixed.amount);
349
+ p += fee * q;
350
+ }
351
+ let price = retailUnits(p, q, info, input.rounding ?? "none");
352
+ if (input.floor && price < BigInt(input.floor.amount)) price = BigInt(input.floor.amount);
353
+ return boundedMoney(price, info);
354
+ }
355
+ /** `(price - cost) / price` in percent, half-up to `decimals` (default 1); a non-positive price gives 0. */
356
+ function marginPercent(price, cost, options = {}) {
357
+ const [p, c] = oneCurrency([price, cost], "compare").units;
358
+ const decimals = digitsOf(options.decimals ?? 1);
359
+ if (p <= 0n) return 0;
360
+ return Number(toDecimalText(percentRatio(p - c, p, decimals)));
361
+ }
362
+ /**
363
+ * The change from `from` to `to` in percent, half-up to `decimals` (default 1): `'change'`
364
+ * (default) is `(to - from) / from`, `'off'` the discount `(from - to) / from` (promotion
365
+ * percent off, subscription `discount_percentage`). A non-positive `from` gives null.
366
+ */
367
+ function percentChange(from, to, options = {}) {
368
+ const [f, t] = oneCurrency([from, to], "compare").units;
369
+ const decimals = digitsOf(options.decimals ?? 1);
370
+ if (f <= 0n) return null;
371
+ const delta = options.as === "off" ? f - t : t - f;
372
+ return Number(toDecimalText(percentRatio(delta, f, decimals)));
373
+ }
374
+ /** What is kept of a charge after a card fee: `m - round(m x bps / 1e4) - fixed`. */
375
+ function netOfFee(m, fee) {
376
+ const { info, units } = oneCurrency([m, fee.fixed], "net");
377
+ const bps = nonNegativeInteger(fee.bps, "fee.bps");
378
+ const [amount, fixed] = units;
379
+ return boundedMoney(amount - basisPointsOf(amount, bps, "half-up") - fixed, info);
380
+ }
381
+ //#endregion
382
+ //#region src/commerce/tax.ts
383
+ /**
384
+ * The tax rate in percent that `tax` on `net` implies, snapped half-up to a multiple of `snap`
385
+ * (default 0.5, every real EU VAT rate; 0 = no snap, 10 decimals). `net <= 0` gives 0.
386
+ * sevDesk `inferTaxRatePercent`, exact.
387
+ */
388
+ function inferTaxRatePercent(net, tax, options = {}) {
389
+ const { units } = oneCurrency([net, tax], "infer a tax rate from");
390
+ const [n, t] = units;
391
+ const snap = percentValue(options.snap ?? .5, "snap");
392
+ if (snap.units < 0n) throw new MoneyError("amount_invalid", `snap must not be negative, got ${describeValue(options.snap)}`);
393
+ if (n <= 0n) return 0;
394
+ if (snap.units === 0n) return Number(toDecimalText(percentRatio(t, n, 10)));
395
+ const steps = percentRatio(t, n * snap.units, snap.scale).units;
396
+ return Number(toDecimalText({
397
+ units: steps * snap.units,
398
+ scale: snap.scale
399
+ }));
400
+ }
401
+ /**
402
+ * The rate in percent behind `tax` on `base` (Shopify archive `taxRateFor`): exclusive
403
+ * `100 tax / base`, inclusive `100 tax / (base - tax)`, half-up to `decimals` (default 10),
404
+ * trailing zeros dropped. Null when tax or base is not positive, or the denominator is not.
405
+ */
406
+ function solveTaxRate(input) {
407
+ const { units } = oneCurrency([input.tax, input.base], "solve a tax rate from");
408
+ const [tax, base] = units;
409
+ const decimals = digitsOf(input.decimals ?? 10);
410
+ if (tax <= 0n || base <= 0n) return null;
411
+ const denominator = input.inclusive ? base - tax : base;
412
+ if (denominator <= 0n) return null;
413
+ return toDecimalText(percentRatio(tax, denominator, decimals));
414
+ }
415
+ /**
416
+ * Whether prices include tax (Shopify archive `inferTaxesIncluded`): `explicit` wins; a null
417
+ * total or a non-positive tax is exclusive; else inclusive iff
418
+ * `|sub + ship - total| < |sub + ship + tax - total|`.
419
+ */
420
+ function inferTaxesIncluded(input) {
421
+ if (typeof input.explicit === "boolean") return input.explicit;
422
+ const { units } = oneCurrency([
423
+ input.subtotal,
424
+ input.shipping,
425
+ input.tax,
426
+ ...input.total ? [input.total] : []
427
+ ], "compare");
428
+ const [sub, ship, tax] = units;
429
+ if (!input.total || tax <= 0n) return false;
430
+ const total = units[3];
431
+ const abs = (x) => x < 0n ? -x : x;
432
+ return abs(sub + ship - total) < abs(sub + ship + tax - total);
433
+ }
434
+ /** `gross x 100 / (100 + rate)` rounded once to a storage unit. */
435
+ function netOf(gross, ratePercent, rounding) {
436
+ const rate = percentValue(ratePercent, "ratePercent");
437
+ const whole = hundredPercent(rate.scale);
438
+ const denominator = whole + rate.units;
439
+ if (denominator <= 0n) throw new MoneyError("amount_invalid", `A tax rate must be above -100%, got ${describeValue(ratePercent)}`);
440
+ return mulDiv(BigInt(gross.amount), whole, denominator, rounding);
441
+ }
442
+ /** `gross` split into `net = round(gross x 100 / (100 + rate))` and `tax = gross - net` (net + tax == gross). */
443
+ function splitGross(gross, ratePercent, options = {}) {
444
+ const { info } = storedDecimal(gross);
445
+ const net = netOf(gross, ratePercent, options.rounding ?? "half-up");
446
+ return {
447
+ net: boundedMoney(net, info),
448
+ tax: boundedMoney(BigInt(gross.amount) - net, info)
449
+ };
450
+ }
451
+ /** A unit price without its tax when prices include tax (`round(unit x 100 / (100 + rate))`), else `unit`. */
452
+ function unitNetOfTax(unit, options) {
453
+ const { info } = storedDecimal(unit);
454
+ if (!options.taxInclusive) return unit;
455
+ return boundedMoney(netOf(unit, options.ratePercent, "half-up"), info);
456
+ }
457
+ /**
458
+ * `refund` spread over positions (each a net and its tax): capped at the gross total, split by
459
+ * gross with the largest remainder, then each gross share split into net and tax in the
460
+ * position's proportion (largest remainder). Per row `net + tax == gross`, and the grosses
461
+ * sum to the (capped) refund. Negative amounts throw `amount_invalid`.
462
+ */
463
+ function allocateWithTax(refund, positions) {
464
+ const { info, units } = oneCurrency([refund, ...positions.flatMap((p) => [p.net, p.tax])], "allocate");
465
+ const [total, ...parts] = units;
466
+ if (total < 0n || parts.some((p) => p < 0n)) throw new MoneyError("amount_invalid", "A refund and its positions must not be negative");
467
+ const grosses = positions.map((_, i) => parts[2 * i] + parts[2 * i + 1]);
468
+ const grossSum = grosses.reduce((a, b) => a + b, 0n);
469
+ return allocateUnits(total < grossSum ? total : grossSum, grosses).map((share, i) => {
470
+ const [net, tax] = allocateUnits(share, [parts[2 * i], parts[2 * i + 1]]);
471
+ return {
472
+ net: boundedMoney(net, info),
473
+ tax: boundedMoney(tax, info),
474
+ gross: boundedMoney(share, info)
475
+ };
476
+ });
477
+ }
478
+ const INCLUSIVE_KEYS = [
479
+ "original_item_total",
480
+ "discount_total",
481
+ "original_shipping_total",
482
+ "tax_total",
483
+ "total"
484
+ ];
485
+ const EXCLUSIVE_KEYS = [
486
+ "item_subtotal",
487
+ "discount_subtotal",
488
+ "shipping_subtotal",
489
+ "tax_total",
490
+ "total"
491
+ ];
492
+ /**
493
+ * The totals a customer-facing summary shows, one selection for every email: tax-inclusive
494
+ * stores show the gross item and shipping totals with the tax "included", exclusive stores the
495
+ * net subtotals with the tax "added". Each field reads `raw_<key>` first, like
496
+ * `readStoredField`; Medusa's fractional totals round half-up to a storage unit; a missing or
497
+ * unreadable field is null, never 0.
498
+ */
499
+ function displayTotals(totals, currency, options) {
500
+ const info = resolveCurrency(currency, { allowHistoric: true });
501
+ const read = (key) => {
502
+ const exact = readStoredDecimal(storedFieldValue(totals, key), { mode: "lenient" });
503
+ return exact === null ? null : roundStoredDecimal(decimalOf(exact), info);
504
+ };
505
+ const [subtotal, discount, shipping, tax, total] = (options.taxInclusive ? INCLUSIVE_KEYS : EXCLUSIVE_KEYS).map(read);
506
+ return {
507
+ subtotal,
508
+ discount,
509
+ shipping,
510
+ tax,
511
+ total,
512
+ taxIncluded: options.taxInclusive
513
+ };
514
+ }
515
+ //#endregion
516
+ export { allocateWithTax, applyMarkup, buyGetReward, discountAmount, displayTotals, formatDiscount, inferTaxRatePercent, inferTaxesIncluded, landedUnitCosts, marginPercent, netOfFee, percentChange, requiresPayment, resolveOrderValue, retailPrice, solveTaxRate, splitGross, storageConventionOf, unitNetOfTax, weightedAverageCost };
@@ -0,0 +1,99 @@
1
+ import { B as decimalOf, E as resolveCurrency, M as resolveLocale, Q as MoneyError, at as fractionDigitsOf, dt as roundToScale, it as formatDecimal, j as numberFormat, nt as decimalFromNumber, s as storedDecimal, tt as compareDecimal } from "./money-WhnGiI00.js";
2
+ import { i as roundToUnit } from "./rounding-g9rvye0k.js";
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 } = storedDecimal(m);
20
+ const whole = fractionDigitsOf(value) === 0;
21
+ const digits = options.digits === "widen" ? Math.max(info.displayDigits, fractionDigitsOf(value)) : options.digits === "trim-zero" && whole ? 0 : info.displayDigits;
22
+ const text = formatDecimal(roundToScale(value, digits, "half-up"));
23
+ try {
24
+ return finish(numberFormat(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(roundToUnit(amount, currency), options);
43
+ }
44
+ function compact(currency, major, options, significant) {
45
+ return numberFormat(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 } = storedDecimal(m);
61
+ const threshold = 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 (compareDecimal(magnitude, threshold) < 0) return formatMoney(m, options);
67
+ try {
68
+ return finish(compact(info.code, 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 = 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 = storedDecimal(high).info.code;
79
+ if (highCode !== lowCode) throw new 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 } = resolveCurrency(currency, { allowHistoric: true });
88
+ return (major) => {
89
+ if (!Number.isFinite(major)) return `${code} ${major}`;
90
+ const text = formatDecimal(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
+ export { moneyAxisFormatter as a, formatStored as i, formatMoneyCompact as n, formatMoneyRange as r, formatMoney as t };