@amboras-dev/money 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +393 -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-CdnhvAda.js +199 -0
- package/dist/allocate-D7tm_t_o.cjs +222 -0
- package/dist/amount-D7dj76nH.js +200 -0
- package/dist/amount-DHtvZSSm.cjs +241 -0
- package/dist/arithmetic-Bq-8mSal.cjs +233 -0
- package/dist/arithmetic-C-Q_aI49.js +132 -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-BdB7LEIp.js +90 -0
- package/dist/format-BnA1aLJk.cjs +113 -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 +349 -0
- package/dist/index.d.cts +288 -0
- package/dist/index.d.ts +288 -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--gQWI1f2.cjs +65 -0
- package/dist/iso-minor-CtaiIifs.js +48 -0
- package/dist/medusa-CmxCni-G.js +101 -0
- package/dist/medusa-DuQ3-KoD.d.cts +78 -0
- package/dist/medusa-OJ2TJOxt.cjs +154 -0
- package/dist/medusa-RwqxaDGq.d.ts +78 -0
- package/dist/money-CIgELekG.js +3571 -0
- package/dist/money-CbhsT0fB.d.cts +232 -0
- package/dist/money-CbhsT0fB.d.ts +232 -0
- package/dist/money-D5-vlv0h.cjs +3960 -0
- package/dist/number-text-BpvN6NAo.cjs +145 -0
- package/dist/number-text-C82oh6JB.js +134 -0
- package/dist/own-D7544D3k.js +4 -0
- package/dist/own-oP9DXFWv.cjs +9 -0
- package/dist/percent-CKVI_c-m.js +20 -0
- package/dist/percent-Dt8vNIAW.cjs +43 -0
- package/dist/rate-CmeTGNex.d.ts +63 -0
- package/dist/rate-DGPfB_jc.cjs +217 -0
- package/dist/rate-lLPViBuM.js +152 -0
- package/dist/rate-toyS3EMf.d.cts +63 -0
- package/dist/reporting-5xby-Zas.d.ts +31 -0
- package/dist/reporting-DEM97DhQ.d.cts +31 -0
- package/dist/retail-BsvpNv1F.js +69 -0
- package/dist/retail-BwcFJcVN.d.ts +31 -0
- package/dist/retail-DTk2XAMM.cjs +86 -0
- package/dist/retail-Mqd-sPMF.d.cts +31 -0
- package/dist/rounding-CVAPEfq_.js +50 -0
- package/dist/rounding-oZ832yTD.cjs +79 -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 +28518 -0
- package/dist/testing.d.cts +97 -0
- package/dist/testing.d.ts +97 -0
- package/dist/testing.js +28510 -0
- package/dist/wire-CI3Sm1iC.js +1675 -0
- package/dist/wire-Cd4Q1Jmm.d.ts +269 -0
- package/dist/wire-DQZ6fA3c.cjs +1872 -0
- package/dist/wire-hWuixv3w.d.cts +269 -0
- package/dist/wire.cjs +35 -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,233 @@
|
|
|
1
|
+
const require_money = require("./money-D5-vlv0h.cjs");
|
|
2
|
+
const require_rounding = require("./rounding-oZ832yTD.cjs");
|
|
3
|
+
const require_percent = require("./percent-Dt8vNIAW.cjs");
|
|
4
|
+
//#region src/arithmetic.ts
|
|
5
|
+
/** Internal: the shared currency of `a` and `b` and their stored integers. Throws `currency_mismatch`. */
|
|
6
|
+
function pair(a, b, verb) {
|
|
7
|
+
const left = require_money.storedDecimal(a).info;
|
|
8
|
+
const right = require_money.storedDecimal(b).info;
|
|
9
|
+
if (left.code !== right.code) throw new require_money.MoneyError("currency_mismatch", `Cannot ${verb} ${left.code} and ${right.code}`);
|
|
10
|
+
return {
|
|
11
|
+
info: left,
|
|
12
|
+
x: BigInt(a.amount),
|
|
13
|
+
y: BigInt(b.amount)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const sign = (n) => n < 0n ? -1 : n > 0n ? 1 : 0;
|
|
17
|
+
/** The validated stored integer of `m`. */
|
|
18
|
+
const unitsOf = (m) => require_money.storedDecimal(m).value.units;
|
|
19
|
+
function add(a, b) {
|
|
20
|
+
const { info, x, y } = pair(a, b, "add");
|
|
21
|
+
return require_money.boundedMoney(x + y, info);
|
|
22
|
+
}
|
|
23
|
+
function subtract(a, b) {
|
|
24
|
+
const { info, x, y } = pair(a, b, "subtract");
|
|
25
|
+
return require_money.boundedMoney(x - y, info);
|
|
26
|
+
}
|
|
27
|
+
/** The exact total of `items`, every one in `currency` (required, so an empty list has one). */
|
|
28
|
+
function sum(items, currency) {
|
|
29
|
+
const info = require_money.resolveCurrency(currency, { allowHistoric: true });
|
|
30
|
+
let total = 0n;
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
const own = require_money.storedDecimal(item).info;
|
|
33
|
+
if (own.code !== info.code) throw new require_money.MoneyError("currency_mismatch", `Cannot sum ${own.code} into ${info.code}`);
|
|
34
|
+
total += BigInt(item.amount);
|
|
35
|
+
}
|
|
36
|
+
return require_money.boundedMoney(total, info);
|
|
37
|
+
}
|
|
38
|
+
function integerOf(value, name) {
|
|
39
|
+
if (typeof value === "bigint") return value;
|
|
40
|
+
if (Number.isSafeInteger(value)) return BigInt(value);
|
|
41
|
+
throw new require_money.MoneyError("amount_invalid", `Ratio ${name} must be a safe integer or bigint, got ${require_money.describeValue(value)}`);
|
|
42
|
+
}
|
|
43
|
+
/** `m x numerator / denominator`, rounded once to a whole `unit`. */
|
|
44
|
+
function scaled(m, numerator, denominator, options) {
|
|
45
|
+
const { info } = require_money.storedDecimal(m);
|
|
46
|
+
if (denominator === 0n) throw new require_money.MoneyError("amount_invalid", "Division by zero");
|
|
47
|
+
const units = require_rounding.mulDivToUnit(BigInt(m.amount), numerator, denominator, info, options.unit ?? "storage", options.rounding);
|
|
48
|
+
return require_money.boundedMoney(units, info);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `m x factor`, rounded once to a whole `unit` (default `'storage'`) with `rounding`
|
|
52
|
+
* (default half-up). A number is taken by its shortest decimal text (as JSON carries it);
|
|
53
|
+
* text is exact decimal; a Ratio is exact. Integer factors are exact. A non-finite factor or
|
|
54
|
+
* a zero denominator throws `amount_invalid`.
|
|
55
|
+
*/
|
|
56
|
+
function multiply(m, factor, options = {}) {
|
|
57
|
+
if (typeof factor === "object" && factor !== null) return scaled(m, integerOf(factor.numerator, "numerator"), integerOf(factor.denominator, "denominator"), options);
|
|
58
|
+
const value = require_money.decimalOf(factor);
|
|
59
|
+
return scaled(m, value.units, require_money.pow10(value.scale), options);
|
|
60
|
+
}
|
|
61
|
+
/** `m / divisor`, rounded once like `multiply`. Divisor 0 throws `amount_invalid`. */
|
|
62
|
+
function divide(m, divisor, options = {}) {
|
|
63
|
+
const value = require_money.decimalOf(divisor);
|
|
64
|
+
return scaled(m, require_money.pow10(value.scale), value.units, options);
|
|
65
|
+
}
|
|
66
|
+
/** `percent` percent of `m` (12.5 is an eighth), rounded once like `multiply` (default unit `'storage'`). */
|
|
67
|
+
function percentOf(m, percent, options = {}) {
|
|
68
|
+
const value = require_money.decimalOf(percent);
|
|
69
|
+
return scaled(m, value.units, require_percent.hundredPercent(value.scale), options);
|
|
70
|
+
}
|
|
71
|
+
/** `-1`, `0` or `1`. Different currencies throw `currency_mismatch`. */
|
|
72
|
+
function compare(a, b) {
|
|
73
|
+
const { x, y } = pair(a, b, "compare");
|
|
74
|
+
return sign(x - y);
|
|
75
|
+
}
|
|
76
|
+
/** Same currency and same amount. Different currencies are `false`, never a throw. */
|
|
77
|
+
function equals(a, b) {
|
|
78
|
+
return require_money.storedDecimal(a).info.code === require_money.storedDecimal(b).info.code && a.amount === b.amount;
|
|
79
|
+
}
|
|
80
|
+
function isZero(m) {
|
|
81
|
+
return unitsOf(m) === 0n;
|
|
82
|
+
}
|
|
83
|
+
function isPositive(m) {
|
|
84
|
+
return require_money.isPositiveDecimal(require_money.storedDecimal(m).value);
|
|
85
|
+
}
|
|
86
|
+
function isNegative(m) {
|
|
87
|
+
return unitsOf(m) < 0n;
|
|
88
|
+
}
|
|
89
|
+
/** The smallest of the amounts, all in one currency (the first of equals). */
|
|
90
|
+
function min(first, ...rest) {
|
|
91
|
+
require_money.storedDecimal(first);
|
|
92
|
+
return rest.reduce((low, m) => compare(m, low) < 0 ? m : low, first);
|
|
93
|
+
}
|
|
94
|
+
/** The largest of the amounts, all in one currency (the first of equals). */
|
|
95
|
+
function max(first, ...rest) {
|
|
96
|
+
require_money.storedDecimal(first);
|
|
97
|
+
return rest.reduce((high, m) => compare(m, high) > 0 ? m : high, first);
|
|
98
|
+
}
|
|
99
|
+
/** `m` limited to `[min, max]` (either optional, same currency). `min` above `max` throws `amount_invalid`. */
|
|
100
|
+
function clamp(m, bounds) {
|
|
101
|
+
if (bounds.min && bounds.max && compare(bounds.min, bounds.max) > 0) throw new require_money.MoneyError("amount_invalid", `Clamp bounds are reversed: ${bounds.min.amount} > ${bounds.max.amount}`);
|
|
102
|
+
if (bounds.min && compare(m, bounds.min) < 0) return bounds.min;
|
|
103
|
+
if (bounds.max && compare(m, bounds.max) > 0) return bounds.max;
|
|
104
|
+
return m;
|
|
105
|
+
}
|
|
106
|
+
/** What is left: `max(0, total - used)`. */
|
|
107
|
+
function remaining(total, used) {
|
|
108
|
+
const { info, x, y } = pair(total, used, "subtract");
|
|
109
|
+
return require_money.boundedMoney(x > y ? x - y : 0n, info);
|
|
110
|
+
}
|
|
111
|
+
function checkTolerance(tolerance, info) {
|
|
112
|
+
const own = require_money.storedDecimal(tolerance).info;
|
|
113
|
+
if (own.code !== info.code) throw new require_money.MoneyError("currency_mismatch", `Tolerance in ${own.code}, amounts in ${info.code}`);
|
|
114
|
+
if (tolerance.amount < 0) throw new require_money.MoneyError("amount_invalid", `Tolerance must not be negative, got ${tolerance.amount}`);
|
|
115
|
+
return BigInt(tolerance.amount);
|
|
116
|
+
}
|
|
117
|
+
/** `|a - b| <= tolerance` (`<` with `inclusive: false`), all one currency. A negative tolerance throws `amount_invalid`. */
|
|
118
|
+
function within(a, b, tolerance, options = {}) {
|
|
119
|
+
const { info, x, y } = pair(a, b, "compare");
|
|
120
|
+
const limit = checkTolerance(tolerance, info);
|
|
121
|
+
const gap = x > y ? x - y : y - x;
|
|
122
|
+
return options.inclusive === false ? gap < limit : gap <= limit;
|
|
123
|
+
}
|
|
124
|
+
/** `d = a - b`; `d` below `tolerance` counts as rounding and gives 0, else `d` (Stripe `refundSyncDelta`). */
|
|
125
|
+
function excessOver(a, b, tolerance) {
|
|
126
|
+
const { info, x, y } = pair(a, b, "subtract");
|
|
127
|
+
const limit = checkTolerance(tolerance, info);
|
|
128
|
+
const delta = x - y;
|
|
129
|
+
return require_money.boundedMoney(delta < limit ? 0n : delta, info);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
Object.defineProperty(exports, "add", {
|
|
133
|
+
enumerable: true,
|
|
134
|
+
get: function() {
|
|
135
|
+
return add;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
Object.defineProperty(exports, "clamp", {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
get: function() {
|
|
141
|
+
return clamp;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
Object.defineProperty(exports, "compare", {
|
|
145
|
+
enumerable: true,
|
|
146
|
+
get: function() {
|
|
147
|
+
return compare;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
Object.defineProperty(exports, "divide", {
|
|
151
|
+
enumerable: true,
|
|
152
|
+
get: function() {
|
|
153
|
+
return divide;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
Object.defineProperty(exports, "equals", {
|
|
157
|
+
enumerable: true,
|
|
158
|
+
get: function() {
|
|
159
|
+
return equals;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
Object.defineProperty(exports, "excessOver", {
|
|
163
|
+
enumerable: true,
|
|
164
|
+
get: function() {
|
|
165
|
+
return excessOver;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
Object.defineProperty(exports, "isNegative", {
|
|
169
|
+
enumerable: true,
|
|
170
|
+
get: function() {
|
|
171
|
+
return isNegative;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
Object.defineProperty(exports, "isPositive", {
|
|
175
|
+
enumerable: true,
|
|
176
|
+
get: function() {
|
|
177
|
+
return isPositive;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
Object.defineProperty(exports, "isZero", {
|
|
181
|
+
enumerable: true,
|
|
182
|
+
get: function() {
|
|
183
|
+
return isZero;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
Object.defineProperty(exports, "max", {
|
|
187
|
+
enumerable: true,
|
|
188
|
+
get: function() {
|
|
189
|
+
return max;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
Object.defineProperty(exports, "min", {
|
|
193
|
+
enumerable: true,
|
|
194
|
+
get: function() {
|
|
195
|
+
return min;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
Object.defineProperty(exports, "multiply", {
|
|
199
|
+
enumerable: true,
|
|
200
|
+
get: function() {
|
|
201
|
+
return multiply;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
Object.defineProperty(exports, "percentOf", {
|
|
205
|
+
enumerable: true,
|
|
206
|
+
get: function() {
|
|
207
|
+
return percentOf;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
Object.defineProperty(exports, "remaining", {
|
|
211
|
+
enumerable: true,
|
|
212
|
+
get: function() {
|
|
213
|
+
return remaining;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
Object.defineProperty(exports, "subtract", {
|
|
217
|
+
enumerable: true,
|
|
218
|
+
get: function() {
|
|
219
|
+
return subtract;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
Object.defineProperty(exports, "sum", {
|
|
223
|
+
enumerable: true,
|
|
224
|
+
get: function() {
|
|
225
|
+
return sum;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
Object.defineProperty(exports, "within", {
|
|
229
|
+
enumerable: true,
|
|
230
|
+
get: function() {
|
|
231
|
+
return within;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { E as resolveCurrency, Q as describeValue, Z as MoneyError, at as isPositiveDecimal, ct as pow10, s as storedDecimal, t as boundedMoney, z as decimalOf } from "./money-CIgELekG.js";
|
|
2
|
+
import { t as mulDivToUnit } from "./rounding-CVAPEfq_.js";
|
|
3
|
+
import { n as hundredPercent } from "./percent-CKVI_c-m.js";
|
|
4
|
+
//#region src/arithmetic.ts
|
|
5
|
+
/** Internal: the shared currency of `a` and `b` and their stored integers. Throws `currency_mismatch`. */
|
|
6
|
+
function pair(a, b, verb) {
|
|
7
|
+
const left = storedDecimal(a).info;
|
|
8
|
+
const right = storedDecimal(b).info;
|
|
9
|
+
if (left.code !== right.code) throw new MoneyError("currency_mismatch", `Cannot ${verb} ${left.code} and ${right.code}`);
|
|
10
|
+
return {
|
|
11
|
+
info: left,
|
|
12
|
+
x: BigInt(a.amount),
|
|
13
|
+
y: BigInt(b.amount)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const sign = (n) => n < 0n ? -1 : n > 0n ? 1 : 0;
|
|
17
|
+
/** The validated stored integer of `m`. */
|
|
18
|
+
const unitsOf = (m) => storedDecimal(m).value.units;
|
|
19
|
+
function add(a, b) {
|
|
20
|
+
const { info, x, y } = pair(a, b, "add");
|
|
21
|
+
return boundedMoney(x + y, info);
|
|
22
|
+
}
|
|
23
|
+
function subtract(a, b) {
|
|
24
|
+
const { info, x, y } = pair(a, b, "subtract");
|
|
25
|
+
return boundedMoney(x - y, info);
|
|
26
|
+
}
|
|
27
|
+
/** The exact total of `items`, every one in `currency` (required, so an empty list has one). */
|
|
28
|
+
function sum(items, currency) {
|
|
29
|
+
const info = resolveCurrency(currency, { allowHistoric: true });
|
|
30
|
+
let total = 0n;
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
const own = storedDecimal(item).info;
|
|
33
|
+
if (own.code !== info.code) throw new MoneyError("currency_mismatch", `Cannot sum ${own.code} into ${info.code}`);
|
|
34
|
+
total += BigInt(item.amount);
|
|
35
|
+
}
|
|
36
|
+
return boundedMoney(total, info);
|
|
37
|
+
}
|
|
38
|
+
function integerOf(value, name) {
|
|
39
|
+
if (typeof value === "bigint") return value;
|
|
40
|
+
if (Number.isSafeInteger(value)) return BigInt(value);
|
|
41
|
+
throw new MoneyError("amount_invalid", `Ratio ${name} must be a safe integer or bigint, got ${describeValue(value)}`);
|
|
42
|
+
}
|
|
43
|
+
/** `m x numerator / denominator`, rounded once to a whole `unit`. */
|
|
44
|
+
function scaled(m, numerator, denominator, options) {
|
|
45
|
+
const { info } = storedDecimal(m);
|
|
46
|
+
if (denominator === 0n) throw new MoneyError("amount_invalid", "Division by zero");
|
|
47
|
+
const units = mulDivToUnit(BigInt(m.amount), numerator, denominator, info, options.unit ?? "storage", options.rounding);
|
|
48
|
+
return boundedMoney(units, info);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `m x factor`, rounded once to a whole `unit` (default `'storage'`) with `rounding`
|
|
52
|
+
* (default half-up). A number is taken by its shortest decimal text (as JSON carries it);
|
|
53
|
+
* text is exact decimal; a Ratio is exact. Integer factors are exact. A non-finite factor or
|
|
54
|
+
* a zero denominator throws `amount_invalid`.
|
|
55
|
+
*/
|
|
56
|
+
function multiply(m, factor, options = {}) {
|
|
57
|
+
if (typeof factor === "object" && factor !== null) return scaled(m, integerOf(factor.numerator, "numerator"), integerOf(factor.denominator, "denominator"), options);
|
|
58
|
+
const value = decimalOf(factor);
|
|
59
|
+
return scaled(m, value.units, pow10(value.scale), options);
|
|
60
|
+
}
|
|
61
|
+
/** `m / divisor`, rounded once like `multiply`. Divisor 0 throws `amount_invalid`. */
|
|
62
|
+
function divide(m, divisor, options = {}) {
|
|
63
|
+
const value = decimalOf(divisor);
|
|
64
|
+
return scaled(m, pow10(value.scale), value.units, options);
|
|
65
|
+
}
|
|
66
|
+
/** `percent` percent of `m` (12.5 is an eighth), rounded once like `multiply` (default unit `'storage'`). */
|
|
67
|
+
function percentOf(m, percent, options = {}) {
|
|
68
|
+
const value = decimalOf(percent);
|
|
69
|
+
return scaled(m, value.units, hundredPercent(value.scale), options);
|
|
70
|
+
}
|
|
71
|
+
/** `-1`, `0` or `1`. Different currencies throw `currency_mismatch`. */
|
|
72
|
+
function compare(a, b) {
|
|
73
|
+
const { x, y } = pair(a, b, "compare");
|
|
74
|
+
return sign(x - y);
|
|
75
|
+
}
|
|
76
|
+
/** Same currency and same amount. Different currencies are `false`, never a throw. */
|
|
77
|
+
function equals(a, b) {
|
|
78
|
+
return storedDecimal(a).info.code === storedDecimal(b).info.code && a.amount === b.amount;
|
|
79
|
+
}
|
|
80
|
+
function isZero(m) {
|
|
81
|
+
return unitsOf(m) === 0n;
|
|
82
|
+
}
|
|
83
|
+
function isPositive(m) {
|
|
84
|
+
return isPositiveDecimal(storedDecimal(m).value);
|
|
85
|
+
}
|
|
86
|
+
function isNegative(m) {
|
|
87
|
+
return unitsOf(m) < 0n;
|
|
88
|
+
}
|
|
89
|
+
/** The smallest of the amounts, all in one currency (the first of equals). */
|
|
90
|
+
function min(first, ...rest) {
|
|
91
|
+
storedDecimal(first);
|
|
92
|
+
return rest.reduce((low, m) => compare(m, low) < 0 ? m : low, first);
|
|
93
|
+
}
|
|
94
|
+
/** The largest of the amounts, all in one currency (the first of equals). */
|
|
95
|
+
function max(first, ...rest) {
|
|
96
|
+
storedDecimal(first);
|
|
97
|
+
return rest.reduce((high, m) => compare(m, high) > 0 ? m : high, first);
|
|
98
|
+
}
|
|
99
|
+
/** `m` limited to `[min, max]` (either optional, same currency). `min` above `max` throws `amount_invalid`. */
|
|
100
|
+
function clamp(m, bounds) {
|
|
101
|
+
if (bounds.min && bounds.max && compare(bounds.min, bounds.max) > 0) throw new MoneyError("amount_invalid", `Clamp bounds are reversed: ${bounds.min.amount} > ${bounds.max.amount}`);
|
|
102
|
+
if (bounds.min && compare(m, bounds.min) < 0) return bounds.min;
|
|
103
|
+
if (bounds.max && compare(m, bounds.max) > 0) return bounds.max;
|
|
104
|
+
return m;
|
|
105
|
+
}
|
|
106
|
+
/** What is left: `max(0, total - used)`. */
|
|
107
|
+
function remaining(total, used) {
|
|
108
|
+
const { info, x, y } = pair(total, used, "subtract");
|
|
109
|
+
return boundedMoney(x > y ? x - y : 0n, info);
|
|
110
|
+
}
|
|
111
|
+
function checkTolerance(tolerance, info) {
|
|
112
|
+
const own = storedDecimal(tolerance).info;
|
|
113
|
+
if (own.code !== info.code) throw new MoneyError("currency_mismatch", `Tolerance in ${own.code}, amounts in ${info.code}`);
|
|
114
|
+
if (tolerance.amount < 0) throw new MoneyError("amount_invalid", `Tolerance must not be negative, got ${tolerance.amount}`);
|
|
115
|
+
return BigInt(tolerance.amount);
|
|
116
|
+
}
|
|
117
|
+
/** `|a - b| <= tolerance` (`<` with `inclusive: false`), all one currency. A negative tolerance throws `amount_invalid`. */
|
|
118
|
+
function within(a, b, tolerance, options = {}) {
|
|
119
|
+
const { info, x, y } = pair(a, b, "compare");
|
|
120
|
+
const limit = checkTolerance(tolerance, info);
|
|
121
|
+
const gap = x > y ? x - y : y - x;
|
|
122
|
+
return options.inclusive === false ? gap < limit : gap <= limit;
|
|
123
|
+
}
|
|
124
|
+
/** `d = a - b`; `d` below `tolerance` counts as rounding and gives 0, else `d` (Stripe `refundSyncDelta`). */
|
|
125
|
+
function excessOver(a, b, tolerance) {
|
|
126
|
+
const { info, x, y } = pair(a, b, "subtract");
|
|
127
|
+
const limit = checkTolerance(tolerance, info);
|
|
128
|
+
const delta = x - y;
|
|
129
|
+
return boundedMoney(delta < limit ? 0n : delta, info);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
export { within as _, equals as a, isPositive as c, min as d, multiply as f, sum as g, subtract as h, divide as i, isZero as l, remaining as m, clamp as n, excessOver as o, percentOf as p, compare as r, isNegative as s, add as t, max as u };
|
package/dist/billing.cjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_money = require("./money-D5-vlv0h.cjs");
|
|
3
|
+
const require_percent = require("./percent-Dt8vNIAW.cjs");
|
|
4
|
+
//#region src/billing/credits.ts
|
|
5
|
+
function wholeNumber(value, name, min) {
|
|
6
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) throw new require_money.MoneyError("amount_invalid", `${name} must be an integer >= ${min}, got ${require_money.describeValue(value)}`);
|
|
7
|
+
return BigInt(value);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fraction digits a float USD cost is held to before the ceil: 1e-8 USD, the granularity of
|
|
11
|
+
* orch `costUsdToCredits`' 1e-6-credit drift epsilon. Coarse enough to absorb float noise
|
|
12
|
+
* (`0.07 * 3` is 0.21000000000000002), fine enough to keep sub-micro costs (1e-7 gives 1).
|
|
13
|
+
*/
|
|
14
|
+
const FLOAT_USD_DIGITS = 8;
|
|
15
|
+
/**
|
|
16
|
+
* Credits for a USD cost: `ceil(usd x 100)` (0.07 gives 7, 1.1 gives 110, 0.0049 and 1e-7
|
|
17
|
+
* give 1). Text and Money are exact. A number is first rounded half-even to 8 decimals, so
|
|
18
|
+
* float noise never adds a credit (`0.07 * 3` gives 21, `1.1 * 1.1` gives 121). USD Money is
|
|
19
|
+
* cents already. A negative cost throws `amount_invalid`.
|
|
20
|
+
*/
|
|
21
|
+
function usdToCredits(usd) {
|
|
22
|
+
let cents;
|
|
23
|
+
if (typeof usd === "object" && usd !== null) {
|
|
24
|
+
const { info } = require_money.storedDecimal(usd);
|
|
25
|
+
if (info.code !== "USD") throw new require_money.MoneyError("currency_mismatch", `Credits are USD cents, got ${info.code}`);
|
|
26
|
+
cents = usd.amount;
|
|
27
|
+
} else {
|
|
28
|
+
const parsed = require_money.decimalOf(usd, true, "usd");
|
|
29
|
+
const exact = require_money.formatDecimal(typeof usd === "number" ? require_money.roundToScale(parsed, FLOAT_USD_DIGITS, "half-even") : parsed);
|
|
30
|
+
cents = require_money.fromMajor(exact, "USD", {
|
|
31
|
+
precision: "round",
|
|
32
|
+
rounding: "ceil",
|
|
33
|
+
field: "usd"
|
|
34
|
+
}).amount;
|
|
35
|
+
}
|
|
36
|
+
if (cents < 0) throw new require_money.MoneyError("amount_invalid", `A USD cost must not be negative, got ${require_money.describeValue(usd)}`);
|
|
37
|
+
return cents;
|
|
38
|
+
}
|
|
39
|
+
/** The USD a number of credits is worth: `credits / 100`, exact. */
|
|
40
|
+
function creditsToUsd(credits) {
|
|
41
|
+
return require_money.toDecimalText({
|
|
42
|
+
units: wholeNumber(credits, "credits", Number.MIN_SAFE_INTEGER),
|
|
43
|
+
scale: 2
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The credits charged for a share (0..1) of a quoted run: `min(quoted, ceil(quoted x share))`,
|
|
48
|
+
* with the product held to 6 decimals (half-up) before the ceil, as the quote does, so a float
|
|
49
|
+
* share such as 0.28000000000000003 never adds a credit.
|
|
50
|
+
*/
|
|
51
|
+
function prorateCredits(quoted, share) {
|
|
52
|
+
const q = wholeNumber(quoted, "quoted", 0);
|
|
53
|
+
const s = require_money.decimalOf(share, false, "share");
|
|
54
|
+
const product = require_money.roundToScale({
|
|
55
|
+
units: q * s.units,
|
|
56
|
+
scale: s.scale
|
|
57
|
+
}, 6, "half-up");
|
|
58
|
+
const charged = require_money.roundToScale(product, 0, "ceil").units;
|
|
59
|
+
return Number(charged < q ? charged : q);
|
|
60
|
+
}
|
|
61
|
+
/** Credits rounded up to a whole `step` (default 50) for a quote: `ceil(credits / step) x step`. */
|
|
62
|
+
function bucketCredits(credits, step = 50) {
|
|
63
|
+
const c = wholeNumber(credits, "credits", Number.MIN_SAFE_INTEGER);
|
|
64
|
+
const s = wholeNumber(step, "step", 1);
|
|
65
|
+
return Number(require_money.divRound(c, s, "ceil") * s);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/billing/plans.ts
|
|
69
|
+
function checkSame(m, other, verb) {
|
|
70
|
+
const info = require_money.storedDecimal(m).info;
|
|
71
|
+
const own = require_money.storedDecimal(other).info;
|
|
72
|
+
if (own.code !== info.code) throw new require_money.MoneyError("currency_mismatch", `Cannot ${verb} ${own.code} with ${info.code}`);
|
|
73
|
+
return info;
|
|
74
|
+
}
|
|
75
|
+
/** The yearly price: `yearly` when set, else `monthly x 12`. */
|
|
76
|
+
function annualPrice(input) {
|
|
77
|
+
const info = require_money.storedDecimal(input.monthly).info;
|
|
78
|
+
if (input.yearly !== null && input.yearly !== void 0) {
|
|
79
|
+
checkSame(input.monthly, input.yearly, "price");
|
|
80
|
+
return input.yearly;
|
|
81
|
+
}
|
|
82
|
+
return require_money.boundedMoney(BigInt(input.monthly.amount) * 12n, info);
|
|
83
|
+
}
|
|
84
|
+
/** `yearly / 12` half-up to a storage unit (a cent); whole-dollar headlines use `roundToUnit(..., { unit: 'major' })`. */
|
|
85
|
+
function monthlyEquivalent(yearly) {
|
|
86
|
+
const { info } = require_money.storedDecimal(yearly);
|
|
87
|
+
return require_money.boundedMoney(require_money.divRound(BigInt(yearly.amount), 12n, "half-up"), info);
|
|
88
|
+
}
|
|
89
|
+
/** A percent in 0..100, else `amount_invalid`. */
|
|
90
|
+
function couponPercent(value, name) {
|
|
91
|
+
const pct = require_money.decimalOf(value, false, name);
|
|
92
|
+
if (pct.units > require_percent.hundredPercent(pct.scale)) throw new require_money.MoneyError("amount_invalid", `${name} must be at most 100, got ${require_money.describeValue(value)}`);
|
|
93
|
+
return pct;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `m` after `coupons`. `'sequential'` (default, Stripe): in order, each on what the previous
|
|
97
|
+
* left, a percent taking `round(amount x pct / 100)` half-up, clamped at 0 after each.
|
|
98
|
+
* `'additive'`: the percents summed and taken in one rounding, plus the amounts, clamped at 0.
|
|
99
|
+
* An amount-off coupon in another currency throws `currency_mismatch`.
|
|
100
|
+
*/
|
|
101
|
+
function applyCoupons(m, coupons, options = {}) {
|
|
102
|
+
const { info } = require_money.storedDecimal(m);
|
|
103
|
+
let amount = BigInt(m.amount);
|
|
104
|
+
const percentCut = (base, value) => require_percent.percentOfUnits(base, couponPercent(value, "percentOff"), "half-up");
|
|
105
|
+
if (options.mode === "additive") {
|
|
106
|
+
let percents = {
|
|
107
|
+
units: 0n,
|
|
108
|
+
scale: 0
|
|
109
|
+
};
|
|
110
|
+
let off = 0n;
|
|
111
|
+
for (const c of coupons) if ("amountOff" in c) {
|
|
112
|
+
checkSame(m, c.amountOff, "apply a coupon in");
|
|
113
|
+
off += BigInt(c.amountOff.amount);
|
|
114
|
+
} else percents = require_money.addDecimal(percents, couponPercent(c.percentOff, "percentOff"));
|
|
115
|
+
const cut = require_percent.percentOfUnits(amount, percents, "half-up") + off;
|
|
116
|
+
return require_money.boundedMoney(amount > cut ? amount - cut : 0n, info);
|
|
117
|
+
}
|
|
118
|
+
for (const c of coupons) {
|
|
119
|
+
if ("amountOff" in c) {
|
|
120
|
+
checkSame(m, c.amountOff, "apply a coupon in");
|
|
121
|
+
amount -= BigInt(c.amountOff.amount);
|
|
122
|
+
} else amount -= percentCut(amount, c.percentOff);
|
|
123
|
+
if (amount < 0n) amount = 0n;
|
|
124
|
+
}
|
|
125
|
+
return require_money.boundedMoney(amount, info);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The line amount that is still `m` after the subscription's percent coupons take their cut
|
|
129
|
+
* (Stripe discounts a negative invoice line too). A verbatim port of orch `grossUpCredit`,
|
|
130
|
+
* float arithmetic included, because a credit already granted must not change by a cent:
|
|
131
|
+
* `Math.round(m / prod(1 - p / 100))` with the product taken in float64, so a tie is resolved
|
|
132
|
+
* by the float product (9 cents at [10, 20] gives 12, since 0.9 x 0.8 is 0.7200000000000001);
|
|
133
|
+
* when the kept share is not positive (100% off), `m` unchanged.
|
|
134
|
+
* It inverts `applyCoupons` exactly for one percent coupon. For stacked percents it can miss
|
|
135
|
+
* by a cent (about 4% of amounts at [10, 20] or [30, 15]): Stripe rounds each coupon's cut,
|
|
136
|
+
* while this divides by the unrounded product (11 cents at [10, 20] grosses up to 15, which
|
|
137
|
+
* the coupons take to 10).
|
|
138
|
+
*/
|
|
139
|
+
function grossUpForPercentOffs(m, percents) {
|
|
140
|
+
const { info } = require_money.storedDecimal(m);
|
|
141
|
+
const kept = percents.reduce((share, value) => {
|
|
142
|
+
return share * (1 - Number(require_money.formatDecimal(require_money.decimalOf(value, true, "percent"))) / 100);
|
|
143
|
+
}, 1);
|
|
144
|
+
if (!(kept > 0)) return m;
|
|
145
|
+
return require_money.boundedMoney(BigInt(Math.round(m.amount / kept)), info);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/billing/usage.ts
|
|
149
|
+
const USD_DIGITS = 6;
|
|
150
|
+
/**
|
|
151
|
+
* The exact sum of `quantity x rate x multiplier` over `lines`, rounded half-up once to 6
|
|
152
|
+
* decimals. Replaces the token-cost formulas (`computeCost`, the unpriced-model fallback,
|
|
153
|
+
* the cloner's `estimateCost`). Each line needs exactly one of `usdPerMillion` / `usdPerUnit`.
|
|
154
|
+
*/
|
|
155
|
+
function usdCost(lines) {
|
|
156
|
+
let total = {
|
|
157
|
+
units: 0n,
|
|
158
|
+
scale: 0
|
|
159
|
+
};
|
|
160
|
+
for (let i = 0; i < lines.length; i++) {
|
|
161
|
+
const line = lines[i];
|
|
162
|
+
const perMillion = line?.usdPerMillion !== void 0;
|
|
163
|
+
if (perMillion === (line?.usdPerUnit !== void 0)) throw new require_money.MoneyError("amount_invalid", `lines[${i}] needs exactly one of usdPerMillion and usdPerUnit, got ${require_money.describeValue(line)}`);
|
|
164
|
+
const quantity = require_money.decimalOf(line.quantity, true, `lines[${i}].quantity`);
|
|
165
|
+
const rate = require_money.decimalOf(perMillion ? line.usdPerMillion : line.usdPerUnit, true, `lines[${i}].rate`);
|
|
166
|
+
const multiplier = require_money.decimalOf(line.multiplier ?? 1, true, `lines[${i}].multiplier`);
|
|
167
|
+
const cost = require_money.multiplyDecimal(require_money.multiplyDecimal(quantity, rate), multiplier);
|
|
168
|
+
total = require_money.addDecimal(total, perMillion ? {
|
|
169
|
+
units: cost.units,
|
|
170
|
+
scale: cost.scale + 6
|
|
171
|
+
} : cost);
|
|
172
|
+
}
|
|
173
|
+
return require_money.toDecimalText(require_money.roundToScale(total, USD_DIGITS, "half-up"));
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
exports.annualPrice = annualPrice;
|
|
177
|
+
exports.applyCoupons = applyCoupons;
|
|
178
|
+
exports.bucketCredits = bucketCredits;
|
|
179
|
+
exports.creditsToUsd = creditsToUsd;
|
|
180
|
+
exports.grossUpForPercentOffs = grossUpForPercentOffs;
|
|
181
|
+
exports.monthlyEquivalent = monthlyEquivalent;
|
|
182
|
+
exports.prorateCredits = prorateCredits;
|
|
183
|
+
exports.usdCost = usdCost;
|
|
184
|
+
exports.usdToCredits = usdToCredits;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { L as DecimalText, r as Money } from "./money-CbhsT0fB.cjs";
|
|
2
|
+
//#region src/billing/usage.d.ts
|
|
3
|
+
/** USD with sub-cent precision, as canonical decimal text (`'0.001234'`). Never merchant money. */
|
|
4
|
+
type UsdAmount = DecimalText;
|
|
5
|
+
interface UsdCostLine {
|
|
6
|
+
/** Tokens, seconds, images...; negative for an adjustment. */
|
|
7
|
+
quantity: number;
|
|
8
|
+
/** USD per million units (token prices). Give this or `usdPerUnit`. */
|
|
9
|
+
usdPerMillion?: number | DecimalText;
|
|
10
|
+
/** USD per unit. */
|
|
11
|
+
usdPerUnit?: number | DecimalText;
|
|
12
|
+
/** A surcharge multiplier (long-context 2x, 1.5x). Default 1. */
|
|
13
|
+
multiplier?: number | DecimalText;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The exact sum of `quantity x rate x multiplier` over `lines`, rounded half-up once to 6
|
|
17
|
+
* decimals. Replaces the token-cost formulas (`computeCost`, the unpriced-model fallback,
|
|
18
|
+
* the cloner's `estimateCost`). Each line needs exactly one of `usdPerMillion` / `usdPerUnit`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function usdCost(lines: readonly UsdCostLine[]): UsdAmount;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/billing/credits.d.ts
|
|
23
|
+
/**
|
|
24
|
+
* Credits for a USD cost: `ceil(usd x 100)` (0.07 gives 7, 1.1 gives 110, 0.0049 and 1e-7
|
|
25
|
+
* give 1). Text and Money are exact. A number is first rounded half-even to 8 decimals, so
|
|
26
|
+
* float noise never adds a credit (`0.07 * 3` gives 21, `1.1 * 1.1` gives 121). USD Money is
|
|
27
|
+
* cents already. A negative cost throws `amount_invalid`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function usdToCredits(usd: UsdAmount | number | Money): number;
|
|
30
|
+
/** The USD a number of credits is worth: `credits / 100`, exact. */
|
|
31
|
+
export declare function creditsToUsd(credits: number): UsdAmount;
|
|
32
|
+
/**
|
|
33
|
+
* The credits charged for a share (0..1) of a quoted run: `min(quoted, ceil(quoted x share))`,
|
|
34
|
+
* with the product held to 6 decimals (half-up) before the ceil, as the quote does, so a float
|
|
35
|
+
* share such as 0.28000000000000003 never adds a credit.
|
|
36
|
+
*/
|
|
37
|
+
export declare function prorateCredits(quoted: number, share: number | DecimalText): number;
|
|
38
|
+
/** Credits rounded up to a whole `step` (default 50) for a quote: `ceil(credits / step) x step`. */
|
|
39
|
+
export declare function bucketCredits(credits: number, step?: number): number;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/billing/plans.d.ts
|
|
42
|
+
/** The yearly price: `yearly` when set, else `monthly x 12`. */
|
|
43
|
+
export declare function annualPrice(input: {
|
|
44
|
+
monthly: Money;
|
|
45
|
+
yearly?: Money | null;
|
|
46
|
+
}): Money;
|
|
47
|
+
/** `yearly / 12` half-up to a storage unit (a cent); whole-dollar headlines use `roundToUnit(..., { unit: 'major' })`. */
|
|
48
|
+
export declare function monthlyEquivalent(yearly: Money): Money;
|
|
49
|
+
type Coupon = {
|
|
50
|
+
readonly percentOff: number | DecimalText;
|
|
51
|
+
} | {
|
|
52
|
+
readonly amountOff: Money;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* `m` after `coupons`. `'sequential'` (default, Stripe): in order, each on what the previous
|
|
56
|
+
* left, a percent taking `round(amount x pct / 100)` half-up, clamped at 0 after each.
|
|
57
|
+
* `'additive'`: the percents summed and taken in one rounding, plus the amounts, clamped at 0.
|
|
58
|
+
* An amount-off coupon in another currency throws `currency_mismatch`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyCoupons(m: Money, coupons: readonly Coupon[], options?: {
|
|
61
|
+
mode?: 'sequential' | 'additive';
|
|
62
|
+
}): Money;
|
|
63
|
+
/**
|
|
64
|
+
* The line amount that is still `m` after the subscription's percent coupons take their cut
|
|
65
|
+
* (Stripe discounts a negative invoice line too). A verbatim port of orch `grossUpCredit`,
|
|
66
|
+
* float arithmetic included, because a credit already granted must not change by a cent:
|
|
67
|
+
* `Math.round(m / prod(1 - p / 100))` with the product taken in float64, so a tie is resolved
|
|
68
|
+
* by the float product (9 cents at [10, 20] gives 12, since 0.9 x 0.8 is 0.7200000000000001);
|
|
69
|
+
* when the kept share is not positive (100% off), `m` unchanged.
|
|
70
|
+
* It inverts `applyCoupons` exactly for one percent coupon. For stacked percents it can miss
|
|
71
|
+
* by a cent (about 4% of amounts at [10, 20] or [30, 15]): Stripe rounds each coupon's cut,
|
|
72
|
+
* while this divides by the unrounded product (11 cents at [10, 20] grosses up to 15, which
|
|
73
|
+
* the coupons take to 10).
|
|
74
|
+
*/
|
|
75
|
+
export declare function grossUpForPercentOffs(m: Money, percents: readonly (number | DecimalText)[]): Money;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/billing.d.ts
|
|
78
|
+
/** USD cents: Money in USD (USD stored amounts are cents). */
|
|
79
|
+
export type UsdCents = Money;
|
|
80
|
+
//#endregion
|
|
81
|
+
export type { Coupon, UsdAmount, UsdCostLine };
|