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