@blamejs/core 0.11.23 → 0.11.25
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/CHANGELOG.md +4 -0
- package/index.js +8 -0
- package/lib/auth/bot-challenge.js +573 -0
- package/lib/framework-error.js +6 -0
- package/lib/fsm.js +469 -0
- package/lib/guard-mail-query.js +14 -0
- package/lib/mail-agent.js +24 -10
- package/lib/mail-send-deliver.js +629 -0
- package/lib/mail-store-fts.js +394 -0
- package/lib/mail-store.js +142 -4
- package/lib/money.js +699 -0
- package/lib/webhook.js +229 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/money.js
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.money
|
|
4
|
+
* @nav Domain
|
|
5
|
+
* @title Money
|
|
6
|
+
* @order 500
|
|
7
|
+
* @slug money
|
|
8
|
+
* @featured true
|
|
9
|
+
*
|
|
10
|
+
* @intro
|
|
11
|
+
* Decimal-safe money arithmetic. The framework primitive every
|
|
12
|
+
* billing / invoicing / shop consumer reaches for so the IEEE 754
|
|
13
|
+
* double-precision `0.1 + 0.2 !== 0.3` rounding error never reaches
|
|
14
|
+
* an invoice line, a tax cell, or a ledger row.
|
|
15
|
+
*
|
|
16
|
+
* ## Why not Number
|
|
17
|
+
*
|
|
18
|
+
* JavaScript's `Number` is a binary64 (IEEE 754 double). `0.10` and
|
|
19
|
+
* `0.20` are unrepresentable in binary fraction; the closest binary
|
|
20
|
+
* approximations sum to `0.30000000000000004`. Add 10,000 such
|
|
21
|
+
* approximations into a daily revenue total and the cumulative drift
|
|
22
|
+
* is large enough to fail a SOX 404 reconciliation. The framework's
|
|
23
|
+
* defense is to refuse `Number` at the boundary: `Money` values
|
|
24
|
+
* carry BigInt minor units (cents / pence / sen / yen / fils) and a
|
|
25
|
+
* currency tag pulled from the ISO 4217 catalog. Every arithmetic
|
|
26
|
+
* operation is integer BigInt math; rounding (where it must happen --
|
|
27
|
+
* FX conversion, weighted allocation) is bankers' (half-to-even)
|
|
28
|
+
* by default and explicit when not.
|
|
29
|
+
*
|
|
30
|
+
* ## What ships
|
|
31
|
+
*
|
|
32
|
+
* - `b.money.of(amount, currency)` -- accepts BigInt minor units OR a
|
|
33
|
+
* decimal-shaped string ("12.50"). Numbers refused at the boundary.
|
|
34
|
+
* - `b.money.fromMinorUnits(bigint, currency)` -- direct construction.
|
|
35
|
+
* - `b.money.parse("12.50 USD")` -- bidirectional shape parser
|
|
36
|
+
* (`<amount> <code>` AND `<code> <amount>`).
|
|
37
|
+
* - `b.money.zero(currency)` -- convenience zero.
|
|
38
|
+
* - `b.money.convert(money, toCurrency, fxRateProvider, opts?)` --
|
|
39
|
+
* conversion through an operator-injected rate provider. Framework
|
|
40
|
+
* NEVER bakes rates in.
|
|
41
|
+
* - `b.money.CURRENCIES` -- frozen ISO 4217 catalog (code -> exponent).
|
|
42
|
+
* - `b.money.MoneyError` -- typed refusal class.
|
|
43
|
+
*
|
|
44
|
+
* ## Allocation
|
|
45
|
+
*
|
|
46
|
+
* `m.allocate([w1, w2, ...])` uses the largest-remainder method:
|
|
47
|
+
* floor each weighted share, then distribute the remainder unit-by-
|
|
48
|
+
* unit to shares with the largest fractional remainder. `$10.00 /
|
|
49
|
+
* [1, 1, 1]` returns `[$3.34, $3.33, $3.33]` (sum exact). `$100.00 /
|
|
50
|
+
* [60, 40]` returns `[$60.00, $40.00]`. Deterministic; total
|
|
51
|
+
* preserved by construction.
|
|
52
|
+
*
|
|
53
|
+
* ## Rounding
|
|
54
|
+
*
|
|
55
|
+
* FX conversion rounds half-to-even (bankers'). Opt into half-up at
|
|
56
|
+
* the call site when an operator regime demands it.
|
|
57
|
+
*
|
|
58
|
+
* ## RFC / standards
|
|
59
|
+
*
|
|
60
|
+
* - ISO 4217 -- currency code + minor-unit catalog.
|
|
61
|
+
* - BCP 47 -- locale tags consumed by `format()` via Intl.NumberFormat.
|
|
62
|
+
* - IEEE 754 binary64 -- the binary fraction representation we
|
|
63
|
+
* refuse at the API boundary. Documented to make the refusal
|
|
64
|
+
* visible to auditors.
|
|
65
|
+
*
|
|
66
|
+
* @card
|
|
67
|
+
* BigInt minor units + ISO 4217 catalog + largest-remainder
|
|
68
|
+
* allocation. Numbers refused at the boundary; FX conversion rounds
|
|
69
|
+
* half-to-even.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
var { defineClass } = require("./framework-error");
|
|
73
|
+
|
|
74
|
+
var MoneyError = defineClass("MoneyError", { alwaysPermanent: true });
|
|
75
|
+
|
|
76
|
+
// ---- ISO 4217 minor-unit catalog ---------------------------------------
|
|
77
|
+
//
|
|
78
|
+
// Sourced from the ISO 4217 maintenance agency's "current funds" table.
|
|
79
|
+
// Exponent is the number of digits after the decimal that compose one
|
|
80
|
+
// major unit (USD -> 2, JPY -> 0, KWD -> 3, CLF -> 4). The framework
|
|
81
|
+
// never embeds rates -- operators inject FX via the rateProvider
|
|
82
|
+
// contract.
|
|
83
|
+
//
|
|
84
|
+
// The catalog is intentionally narrow: the ~36 currencies operators
|
|
85
|
+
// actually price + settle in. Adding an entry is a one-line edit
|
|
86
|
+
// (code + exponent). Codes the catalog does NOT recognise are refused
|
|
87
|
+
// at construction time so operators catch typos at boot.
|
|
88
|
+
|
|
89
|
+
var _CURRENCIES = Object.freeze({
|
|
90
|
+
// exponent 2 (cents / pence / centavos / oere / etc.)
|
|
91
|
+
USD: 2, EUR: 2, GBP: 2, CHF: 2, CAD: 2, AUD: 2, NZD: 2,
|
|
92
|
+
HKD: 2, SGD: 2, CNY: 2, TWD: 2, INR: 2, BRL: 2, MXN: 2,
|
|
93
|
+
ZAR: 2, SEK: 2, NOK: 2, DKK: 2, PLN: 2, CZK: 2, HUF: 2,
|
|
94
|
+
RUB: 2, TRY: 2, AED: 2, SAR: 2, ILS: 2, THB: 2, IDR: 2,
|
|
95
|
+
PHP: 2, MYR: 2, VND: 2, ARS: 2, COP: 2, PEN: 2,
|
|
96
|
+
// exponent 0 (whole-unit minor -- yen / won / etc.)
|
|
97
|
+
JPY: 0, KRW: 0, CLP: 0,
|
|
98
|
+
// exponent 3 (fils / dinar fractions per ISO 4217)
|
|
99
|
+
KWD: 3, BHD: 3, JOD: 3, OMR: 3, TND: 3,
|
|
100
|
+
// exponent 4 (CLF -- UF unidad de fomento)
|
|
101
|
+
CLF: 4,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
var CURRENCIES = _CURRENCIES; // exported alias; same frozen object
|
|
105
|
+
|
|
106
|
+
// _pow10(n) -- BigInt 10^n, computed once per call. Avoids any raw
|
|
107
|
+
// `10n ** Xn` literal in the source and centralises the negative-n
|
|
108
|
+
// refusal in one place.
|
|
109
|
+
function _pow10(n) {
|
|
110
|
+
var p = 1n;
|
|
111
|
+
for (var i = 0; i < n; i++) p = p * 10n;
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _isCurrencyCode(code) {
|
|
116
|
+
return typeof code === "string" &&
|
|
117
|
+
Object.prototype.hasOwnProperty.call(_CURRENCIES, code);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _requireCurrency(code) {
|
|
121
|
+
if (!_isCurrencyCode(code)) {
|
|
122
|
+
throw new MoneyError("money/bad-currency",
|
|
123
|
+
"unknown ISO 4217 currency code: " + JSON.stringify(code));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _exponentOf(code) {
|
|
128
|
+
return _CURRENCIES[code];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// _parseDecimalString -- accepts strings of the form `[-]<int>[.<frac>]`
|
|
132
|
+
// and returns a BigInt minor-unit count under the requested exponent.
|
|
133
|
+
// Fractional digits beyond the exponent ARE refused (silent truncation
|
|
134
|
+
// would erase audit-relevant precision; operators round at the call
|
|
135
|
+
// site if they need to).
|
|
136
|
+
var _DECIMAL_RE = /^(-)?(\d+)(?:\.(\d+))?$/;
|
|
137
|
+
function _parseDecimalString(amount, exponent) {
|
|
138
|
+
if (typeof amount !== "string" || amount.length === 0) {
|
|
139
|
+
throw new MoneyError("money/bad-amount",
|
|
140
|
+
"amount string must be non-empty");
|
|
141
|
+
}
|
|
142
|
+
// Strict shape: optional sign, integer part, optional fractional
|
|
143
|
+
// part. No exponent notation (`1e3`), no thousands separators
|
|
144
|
+
// (locale ambiguity -> operator pre-normalises), no whitespace.
|
|
145
|
+
var match = amount.match(_DECIMAL_RE);
|
|
146
|
+
if (!match) {
|
|
147
|
+
throw new MoneyError("money/bad-amount",
|
|
148
|
+
"amount string must match /^-?\\d+(\\.\\d+)?$/, got " +
|
|
149
|
+
JSON.stringify(amount));
|
|
150
|
+
}
|
|
151
|
+
var sign = match[1] === "-" ? -1n : 1n;
|
|
152
|
+
var intPart = match[2];
|
|
153
|
+
var fracPart = match[3] || "";
|
|
154
|
+
if (fracPart.length > exponent) {
|
|
155
|
+
throw new MoneyError("money/precision-loss",
|
|
156
|
+
"amount " + JSON.stringify(amount) + " has " + fracPart.length +
|
|
157
|
+
" fractional digit(s); currency allows " + exponent);
|
|
158
|
+
}
|
|
159
|
+
// Pad fractional part to the exponent so we can read as one BigInt.
|
|
160
|
+
while (fracPart.length < exponent) fracPart = fracPart + "0";
|
|
161
|
+
var minor = BigInt(intPart + fracPart);
|
|
162
|
+
return sign * minor;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// _formatMinorUnits -- render a BigInt minor-unit count as a decimal
|
|
166
|
+
// string with the requested exponent. Used by toString() and as the
|
|
167
|
+
// fallback for format() when Intl.NumberFormat isn't a fit.
|
|
168
|
+
function _formatMinorUnits(minor, exponent) {
|
|
169
|
+
var neg = minor < 0n;
|
|
170
|
+
var abs = neg ? -minor : minor;
|
|
171
|
+
if (exponent === 0) return (neg ? "-" : "") + abs.toString();
|
|
172
|
+
var s = abs.toString();
|
|
173
|
+
while (s.length <= exponent) s = "0" + s;
|
|
174
|
+
var head = s.slice(0, s.length - exponent);
|
|
175
|
+
var tail = s.slice(s.length - exponent);
|
|
176
|
+
return (neg ? "-" : "") + head + "." + tail;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// _requireSameCurrency -- throws on cross-currency arithmetic.
|
|
180
|
+
// Preserves the invariant; operators catch the bug at the call site.
|
|
181
|
+
function _requireSameCurrency(a, b, op) {
|
|
182
|
+
if (a.currency !== b.currency) {
|
|
183
|
+
throw new MoneyError("money/currency-mismatch",
|
|
184
|
+
"cannot " + op + " " + a.currency + " and " + b.currency +
|
|
185
|
+
"; convert first via b.money.convert(...)");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function _requireMoney(value, label) {
|
|
190
|
+
if (!(value instanceof Money)) {
|
|
191
|
+
throw new MoneyError("money/bad-operand",
|
|
192
|
+
label + " must be a Money instance");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- Money value class -------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @primitive b.money.Money
|
|
200
|
+
* @signature b.money.Money(minorUnits, currency)
|
|
201
|
+
* @since 0.11.25
|
|
202
|
+
* @status stable
|
|
203
|
+
* @related b.money.of, b.money.fromMinorUnits
|
|
204
|
+
*
|
|
205
|
+
* The immutable `Money` value class. Operators rarely construct
|
|
206
|
+
* directly -- reach for `b.money.of` (string or BigInt) or
|
|
207
|
+
* `b.money.fromMinorUnits` (BigInt) instead. The class is exported
|
|
208
|
+
* so `instance instanceof b.money.Money` is a stable type check
|
|
209
|
+
* when receiving Money values across module boundaries.
|
|
210
|
+
*
|
|
211
|
+
* Instance methods: `add`, `subtract`, `multiply`, `allocate`,
|
|
212
|
+
* `negate`, `abs`, `equals`, `lessThan`, `greaterThan`,
|
|
213
|
+
* `lessThanOrEqual`, `greaterThanOrEqual`, `isZero`, `isNegative`,
|
|
214
|
+
* `isPositive`, `toMinorUnits`, `toString`, `toJSON`, `format`.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* var m = new b.money.Money(1250n, "USD");
|
|
218
|
+
* m instanceof b.money.Money;
|
|
219
|
+
*/
|
|
220
|
+
function Money(minorUnits, currency) {
|
|
221
|
+
if (typeof minorUnits !== "bigint") {
|
|
222
|
+
throw new MoneyError("money/bad-minor-units",
|
|
223
|
+
"minorUnits must be a BigInt; got " + (typeof minorUnits));
|
|
224
|
+
}
|
|
225
|
+
_requireCurrency(currency);
|
|
226
|
+
this._minor = minorUnits;
|
|
227
|
+
this.currency = currency;
|
|
228
|
+
Object.freeze(this);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Money.prototype.toMinorUnits = function () { return this._minor; };
|
|
232
|
+
|
|
233
|
+
Money.prototype.add = function (other) {
|
|
234
|
+
_requireMoney(other, "add operand");
|
|
235
|
+
_requireSameCurrency(this, other, "add");
|
|
236
|
+
return new Money(this._minor + other._minor, this.currency);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
Money.prototype.subtract = function (other) {
|
|
240
|
+
_requireMoney(other, "subtract operand");
|
|
241
|
+
_requireSameCurrency(this, other, "subtract");
|
|
242
|
+
return new Money(this._minor - other._minor, this.currency);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// multiply -- scale by a rational. Accepts either a BigInt scalar
|
|
246
|
+
// (`m.multiply(3n)`), a `[numerator, denominator]` BigInt pair, or a
|
|
247
|
+
// decimal-shaped string (`"1.085"`). Refuses Number -- same boundary
|
|
248
|
+
// discipline as construction.
|
|
249
|
+
Money.prototype.multiply = function (factor, opts) {
|
|
250
|
+
opts = opts || {};
|
|
251
|
+
var rounding = opts.rounding === "half-up" ? "half-up" : "half-even";
|
|
252
|
+
var num;
|
|
253
|
+
var den;
|
|
254
|
+
if (typeof factor === "bigint") {
|
|
255
|
+
return new Money(this._minor * factor, this.currency);
|
|
256
|
+
}
|
|
257
|
+
if (Array.isArray(factor) && factor.length === 2 &&
|
|
258
|
+
typeof factor[0] === "bigint" && typeof factor[1] === "bigint") {
|
|
259
|
+
num = factor[0];
|
|
260
|
+
den = factor[1];
|
|
261
|
+
} else if (typeof factor === "string") {
|
|
262
|
+
var parsed = _rationalFromDecimalString(factor);
|
|
263
|
+
num = parsed.num;
|
|
264
|
+
den = parsed.den;
|
|
265
|
+
} else {
|
|
266
|
+
throw new MoneyError("money/bad-factor",
|
|
267
|
+
"multiply factor must be BigInt, [num, den] BigInt pair, or " +
|
|
268
|
+
"decimal string; refuse Number to keep the no-binary-fraction " +
|
|
269
|
+
"invariant intact");
|
|
270
|
+
}
|
|
271
|
+
if (den === 0n) {
|
|
272
|
+
throw new MoneyError("money/division-by-zero",
|
|
273
|
+
"multiply denominator is zero");
|
|
274
|
+
}
|
|
275
|
+
var product = this._minor * num;
|
|
276
|
+
var quotient = _divRound(product, den, rounding);
|
|
277
|
+
return new Money(quotient, this.currency);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// _rationalFromDecimalString -- `"1.085"` -> { num: 1085n, den: 1000n }.
|
|
281
|
+
// Strict shape; same refusals as _parseDecimalString.
|
|
282
|
+
function _rationalFromDecimalString(s) {
|
|
283
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
284
|
+
throw new MoneyError("money/bad-factor",
|
|
285
|
+
"decimal factor must be a non-empty string");
|
|
286
|
+
}
|
|
287
|
+
var m = s.match(_DECIMAL_RE);
|
|
288
|
+
if (!m) {
|
|
289
|
+
throw new MoneyError("money/bad-factor",
|
|
290
|
+
"decimal factor must match /^-?\\d+(\\.\\d+)?$/, got " +
|
|
291
|
+
JSON.stringify(s));
|
|
292
|
+
}
|
|
293
|
+
var sign = m[1] === "-" ? -1n : 1n;
|
|
294
|
+
var intPart = m[2];
|
|
295
|
+
var fracPart = m[3] || "";
|
|
296
|
+
var num = sign * BigInt(intPart + fracPart);
|
|
297
|
+
var den = _pow10(fracPart.length);
|
|
298
|
+
return { num: num, den: den };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// _divRound -- integer-divide `n / d` and round per `rounding`.
|
|
302
|
+
// half-even: banker's rounding, the IEEE 754 default and the ISO 80000
|
|
303
|
+
// recommendation. half-up: rounds .5 away from zero.
|
|
304
|
+
function _divRound(n, d, rounding) {
|
|
305
|
+
if (d === 0n) {
|
|
306
|
+
throw new MoneyError("money/division-by-zero",
|
|
307
|
+
"divisor is zero in _divRound");
|
|
308
|
+
}
|
|
309
|
+
// Work with signed values; normalise sign to denominator-positive so
|
|
310
|
+
// the half-way arithmetic doesn't have to handle d < 0.
|
|
311
|
+
if (d < 0n) { n = -n; d = -d; }
|
|
312
|
+
var q = n / d;
|
|
313
|
+
var r = n - q * d;
|
|
314
|
+
if (r === 0n) return q;
|
|
315
|
+
var twiceRemAbs = r < 0n ? -r * 2n : r * 2n;
|
|
316
|
+
var cmp; // -1: below half, 0: exactly half, 1: above half
|
|
317
|
+
if (twiceRemAbs < d) cmp = -1;
|
|
318
|
+
else if (twiceRemAbs === d) cmp = 0;
|
|
319
|
+
else cmp = 1;
|
|
320
|
+
var bump;
|
|
321
|
+
if (cmp < 0) {
|
|
322
|
+
bump = 0n;
|
|
323
|
+
} else if (cmp > 0) {
|
|
324
|
+
bump = 1n;
|
|
325
|
+
} else if (rounding === "half-up") {
|
|
326
|
+
bump = 1n;
|
|
327
|
+
} else {
|
|
328
|
+
// half-even -- only bump if q is odd (after sign).
|
|
329
|
+
var qAbs = q < 0n ? -q : q;
|
|
330
|
+
bump = (qAbs % 2n === 1n) ? 1n : 0n;
|
|
331
|
+
}
|
|
332
|
+
if (bump === 0n) return q;
|
|
333
|
+
return r < 0n ? q - 1n : q + 1n;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// allocate -- split `this` into `weights.length` parts proportional to
|
|
337
|
+
// the weights, distributing every minor unit. Largest-remainder
|
|
338
|
+
// method: floor each share, then hand out the leftover units to the
|
|
339
|
+
// shares with the largest fractional remainder. Total preserved by
|
|
340
|
+
// construction; deterministic across runtimes.
|
|
341
|
+
Money.prototype.allocate = function (weights) {
|
|
342
|
+
if (!Array.isArray(weights) || weights.length === 0) {
|
|
343
|
+
throw new MoneyError("money/bad-weights",
|
|
344
|
+
"allocate requires a non-empty array of weights");
|
|
345
|
+
}
|
|
346
|
+
var sum = 0n;
|
|
347
|
+
var w = new Array(weights.length);
|
|
348
|
+
for (var i = 0; i < weights.length; i++) {
|
|
349
|
+
var wi = weights[i];
|
|
350
|
+
var wBig;
|
|
351
|
+
if (typeof wi === "bigint") wBig = wi;
|
|
352
|
+
else if (typeof wi === "number" && Number.isInteger(wi)) wBig = BigInt(wi);
|
|
353
|
+
else {
|
|
354
|
+
throw new MoneyError("money/bad-weight",
|
|
355
|
+
"weight[" + i + "] must be BigInt or integer Number; got " +
|
|
356
|
+
(typeof wi));
|
|
357
|
+
}
|
|
358
|
+
if (wBig < 0n) {
|
|
359
|
+
throw new MoneyError("money/bad-weight",
|
|
360
|
+
"weight[" + i + "] is negative; allocation refuses negative shares");
|
|
361
|
+
}
|
|
362
|
+
w[i] = wBig;
|
|
363
|
+
sum = sum + wBig;
|
|
364
|
+
}
|
|
365
|
+
if (sum === 0n) {
|
|
366
|
+
throw new MoneyError("money/bad-weights",
|
|
367
|
+
"allocate weights sum to zero");
|
|
368
|
+
}
|
|
369
|
+
var total = this._minor;
|
|
370
|
+
var shares = new Array(weights.length);
|
|
371
|
+
var remainders = new Array(weights.length);
|
|
372
|
+
var allocated = 0n;
|
|
373
|
+
for (var j = 0; j < weights.length; j++) {
|
|
374
|
+
// share = floor(total * w[j] / sum). For BigInt /, truncation is
|
|
375
|
+
// toward zero; we want floor (toward -infinity) so the leftover
|
|
376
|
+
// pass below distributes positively. Adjust when total < 0.
|
|
377
|
+
var num2 = total * w[j];
|
|
378
|
+
var q = num2 / sum;
|
|
379
|
+
var r = num2 - q * sum;
|
|
380
|
+
if (r !== 0n && total < 0n) {
|
|
381
|
+
q = q - 1n;
|
|
382
|
+
r = r + sum;
|
|
383
|
+
}
|
|
384
|
+
shares[j] = q;
|
|
385
|
+
remainders[j] = { idx: j, rem: r };
|
|
386
|
+
allocated = allocated + q;
|
|
387
|
+
}
|
|
388
|
+
var leftover = total - allocated;
|
|
389
|
+
// leftover is non-negative after the floor adjustment above (proof:
|
|
390
|
+
// for total >= 0, each share is floor toward zero so allocated <=
|
|
391
|
+
// total; for total < 0, each share is floored toward -infty so
|
|
392
|
+
// allocated <= total likewise). Distribute one unit at a time to
|
|
393
|
+
// the largest remainder. Ties broken by index (deterministic).
|
|
394
|
+
remainders.sort(function (a, b) {
|
|
395
|
+
if (a.rem === b.rem) return a.idx - b.idx;
|
|
396
|
+
return a.rem < b.rem ? 1 : -1;
|
|
397
|
+
});
|
|
398
|
+
var k = 0;
|
|
399
|
+
while (leftover > 0n) {
|
|
400
|
+
shares[remainders[k % remainders.length].idx] =
|
|
401
|
+
shares[remainders[k % remainders.length].idx] + 1n;
|
|
402
|
+
leftover = leftover - 1n;
|
|
403
|
+
k = k + 1;
|
|
404
|
+
}
|
|
405
|
+
var out = new Array(shares.length);
|
|
406
|
+
for (var s2 = 0; s2 < shares.length; s2++) {
|
|
407
|
+
out[s2] = new Money(shares[s2], this.currency);
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
Money.prototype.negate = function () {
|
|
413
|
+
return new Money(-this._minor, this.currency);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
Money.prototype.abs = function () {
|
|
417
|
+
return new Money(this._minor < 0n ? -this._minor : this._minor, this.currency);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
Money.prototype.equals = function (other) {
|
|
421
|
+
_requireMoney(other, "equals operand");
|
|
422
|
+
return this.currency === other.currency && this._minor === other._minor;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
Money.prototype.lessThan = function (other) {
|
|
426
|
+
_requireMoney(other, "lessThan operand");
|
|
427
|
+
_requireSameCurrency(this, other, "compare");
|
|
428
|
+
return this._minor < other._minor;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
Money.prototype.greaterThan = function (other) {
|
|
432
|
+
_requireMoney(other, "greaterThan operand");
|
|
433
|
+
_requireSameCurrency(this, other, "compare");
|
|
434
|
+
return this._minor > other._minor;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
Money.prototype.lessThanOrEqual = function (other) {
|
|
438
|
+
_requireMoney(other, "lessThanOrEqual operand");
|
|
439
|
+
_requireSameCurrency(this, other, "compare");
|
|
440
|
+
return this._minor <= other._minor;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
Money.prototype.greaterThanOrEqual = function (other) {
|
|
444
|
+
_requireMoney(other, "greaterThanOrEqual operand");
|
|
445
|
+
_requireSameCurrency(this, other, "compare");
|
|
446
|
+
return this._minor >= other._minor;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
Money.prototype.isZero = function () {
|
|
450
|
+
return this._minor === 0n;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
Money.prototype.isNegative = function () {
|
|
454
|
+
return this._minor < 0n;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
Money.prototype.isPositive = function () {
|
|
458
|
+
return this._minor > 0n;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// toString -- canonical `<decimal-major> <code>` shape. Always
|
|
462
|
+
// 2-decimal for USD-shaped currencies, 0-decimal for JPY, 3-decimal
|
|
463
|
+
// for KWD, 4-decimal for CLF. The shape round-trips through
|
|
464
|
+
// `b.money.parse`.
|
|
465
|
+
Money.prototype.toString = function () {
|
|
466
|
+
var exp = _exponentOf(this.currency);
|
|
467
|
+
return _formatMinorUnits(this._minor, exp) + " " + this.currency;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// toJSON -- operator-side serialisation. `minorUnits` is rendered as a
|
|
471
|
+
// decimal string (BigInt isn't JSON-native; loss-free string survives
|
|
472
|
+
// every transport). Pair with `fromJSON` for round-trip.
|
|
473
|
+
Money.prototype.toJSON = function () {
|
|
474
|
+
return { minorUnits: this._minor.toString(), currency: this.currency };
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// format -- locale-aware string via Intl.NumberFormat when a locale is
|
|
478
|
+
// supplied or when the host runtime exposes Intl (every Node ICU
|
|
479
|
+
// build does). Falls back to `toString()` on any host that lacks Intl
|
|
480
|
+
// or rejects the currency. Operators wanting strict-locale rendering
|
|
481
|
+
// reach for Intl.NumberFormat directly; this method is the convenience
|
|
482
|
+
// shape.
|
|
483
|
+
Money.prototype.format = function (locale) {
|
|
484
|
+
var exp = _exponentOf(this.currency);
|
|
485
|
+
// Compose the decimal value as a Number ONLY for the Intl call --
|
|
486
|
+
// never use it for arithmetic. JPY (exp=0) is exact; for larger
|
|
487
|
+
// exponents we pass a string-derived Number knowing the formatter
|
|
488
|
+
// re-quantises to the currency exponent via minimumFractionDigits.
|
|
489
|
+
var decStr = _formatMinorUnits(this._minor, exp);
|
|
490
|
+
var asNum = Number(decStr);
|
|
491
|
+
if (typeof Intl !== "undefined" && Intl.NumberFormat) {
|
|
492
|
+
try {
|
|
493
|
+
var fmt = new Intl.NumberFormat(locale || undefined, {
|
|
494
|
+
style: "currency",
|
|
495
|
+
currency: this.currency,
|
|
496
|
+
minimumFractionDigits: exp,
|
|
497
|
+
maximumFractionDigits: exp,
|
|
498
|
+
});
|
|
499
|
+
return fmt.format(asNum);
|
|
500
|
+
} catch (_e) {
|
|
501
|
+
// Locale or currency rejected by ICU -- fall through to canonical.
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return this.toString();
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// ---- Factories ---------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @primitive b.money.of
|
|
511
|
+
* @signature b.money.of(amount, currency)
|
|
512
|
+
* @since 0.11.25
|
|
513
|
+
* @status stable
|
|
514
|
+
* @related b.money.fromMinorUnits, b.money.parse
|
|
515
|
+
*
|
|
516
|
+
* Build a `Money` from `amount` (BigInt minor units OR decimal-shaped
|
|
517
|
+
* string) and an ISO 4217 currency code. Throws `MoneyError` on bad
|
|
518
|
+
* shape. Numbers are refused at the boundary -- the framework's
|
|
519
|
+
* defense against IEEE 754 binary-fraction drift.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* var price = b.money.of("12.50", "USD");
|
|
523
|
+
* var fee = b.money.of(250n, "USD");
|
|
524
|
+
* var tip = b.money.of("0", "JPY");
|
|
525
|
+
*/
|
|
526
|
+
function of(amount, currency) {
|
|
527
|
+
_requireCurrency(currency);
|
|
528
|
+
var exp = _exponentOf(currency);
|
|
529
|
+
if (typeof amount === "bigint") {
|
|
530
|
+
return new Money(amount, currency);
|
|
531
|
+
}
|
|
532
|
+
if (typeof amount === "string") {
|
|
533
|
+
return new Money(_parseDecimalString(amount, exp), currency);
|
|
534
|
+
}
|
|
535
|
+
if (typeof amount === "number") {
|
|
536
|
+
throw new MoneyError("money/number-refused",
|
|
537
|
+
"Number amounts refused at the API boundary -- pass BigInt minor " +
|
|
538
|
+
"units (e.g. 250n) or a decimal-shaped string (\"2.50\"). " +
|
|
539
|
+
"Number values lose precision under IEEE 754 binary fractions.");
|
|
540
|
+
}
|
|
541
|
+
throw new MoneyError("money/bad-amount",
|
|
542
|
+
"amount must be BigInt minor units or decimal-shaped string; got " +
|
|
543
|
+
(typeof amount));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* @primitive b.money.fromMinorUnits
|
|
548
|
+
* @signature b.money.fromMinorUnits(minorUnits, currency)
|
|
549
|
+
* @since 0.11.25
|
|
550
|
+
* @status stable
|
|
551
|
+
* @related b.money.of
|
|
552
|
+
*
|
|
553
|
+
* Build a `Money` directly from a BigInt minor-unit count. The
|
|
554
|
+
* lowest-overhead constructor; useful when restoring from a ledger
|
|
555
|
+
* row or a wire-shape `toJSON` payload.
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
* var due = b.money.fromMinorUnits(1250n, "USD");
|
|
559
|
+
*/
|
|
560
|
+
function fromMinorUnits(minorUnits, currency) {
|
|
561
|
+
return new Money(minorUnits, currency);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// _PARSE_AMOUNT_FIRST_RE / _PARSE_CODE_FIRST_RE -- the two accepted shapes.
|
|
565
|
+
var _PARSE_AMOUNT_FIRST_RE = /^(-?\d+(?:\.\d+)?)\s+([A-Z]{3})$/;
|
|
566
|
+
var _PARSE_CODE_FIRST_RE = /^([A-Z]{3})\s+(-?\d+(?:\.\d+)?)$/;
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* @primitive b.money.parse
|
|
570
|
+
* @signature b.money.parse(input)
|
|
571
|
+
* @since 0.11.25
|
|
572
|
+
* @status stable
|
|
573
|
+
* @related b.money.of, Money.prototype.toString
|
|
574
|
+
*
|
|
575
|
+
* Parse a string of the form `<amount> <code>` OR `<code> <amount>`
|
|
576
|
+
* into a `Money`. The two shapes round-trip with `toString()` (which
|
|
577
|
+
* emits the amount-first canonical form). Whitespace between amount
|
|
578
|
+
* and code is required; locale-formatted strings (thousands separator,
|
|
579
|
+
* `$` glyph) are refused -- operators normalise at the call site.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* b.money.parse("12.50 USD");
|
|
583
|
+
* b.money.parse("USD 12.50");
|
|
584
|
+
* b.money.parse("12 JPY");
|
|
585
|
+
* b.money.parse("12.500 KWD");
|
|
586
|
+
*/
|
|
587
|
+
function parse(input) {
|
|
588
|
+
if (typeof input !== "string") {
|
|
589
|
+
throw new MoneyError("money/bad-input",
|
|
590
|
+
"parse expects a string; got " + (typeof input));
|
|
591
|
+
}
|
|
592
|
+
var trimmed = input.trim();
|
|
593
|
+
var m1 = trimmed.match(_PARSE_AMOUNT_FIRST_RE);
|
|
594
|
+
if (m1) return of(m1[1], m1[2]);
|
|
595
|
+
var m2 = trimmed.match(_PARSE_CODE_FIRST_RE);
|
|
596
|
+
if (m2) return of(m2[2], m2[1]);
|
|
597
|
+
throw new MoneyError("money/bad-input",
|
|
598
|
+
"parse: input must match `<amount> <code>` or `<code> <amount>`, " +
|
|
599
|
+
"got " + JSON.stringify(input));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* @primitive b.money.zero
|
|
604
|
+
* @signature b.money.zero(currency)
|
|
605
|
+
* @since 0.11.25
|
|
606
|
+
* @status stable
|
|
607
|
+
* @related b.money.of
|
|
608
|
+
*
|
|
609
|
+
* Return a zero-valued `Money` in the requested currency. Convenience
|
|
610
|
+
* for fold/sum accumulators.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* var total = items.reduce(function (acc, it) { return acc.add(it.price); },
|
|
614
|
+
* b.money.zero("USD"));
|
|
615
|
+
*/
|
|
616
|
+
function zero(currency) {
|
|
617
|
+
_requireCurrency(currency);
|
|
618
|
+
return new Money(0n, currency);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* @primitive b.money.convert
|
|
623
|
+
* @signature b.money.convert(money, toCurrency, fxRateProvider, opts?)
|
|
624
|
+
* @since 0.11.25
|
|
625
|
+
* @status stable
|
|
626
|
+
* @related b.money.of, Money.prototype.multiply
|
|
627
|
+
*
|
|
628
|
+
* Convert `money` to `toCurrency` through an operator-injected rate
|
|
629
|
+
* provider. The framework NEVER bakes in rates -- operators wire a
|
|
630
|
+
* provider that pulls from an external FX feed (ECB / OANDA / their
|
|
631
|
+
* internal treasury system) and refresh on whatever cadence their
|
|
632
|
+
* regime requires.
|
|
633
|
+
*
|
|
634
|
+
* The `fxRateProvider.rate(from, to)` contract returns a decimal-
|
|
635
|
+
* shaped string (`"1.085"`) -- never a Number. Conversion math runs
|
|
636
|
+
* in BigInt with the provider rate's denominator; rounding is
|
|
637
|
+
* half-to-even by default (operator opts into half-up via
|
|
638
|
+
* `opts.rounding`).
|
|
639
|
+
*
|
|
640
|
+
* @opts
|
|
641
|
+
* rounding: "half-even" | "half-up", // default "half-even" (bankers')
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* var rates = { rate: function (from, to) { return "0.92"; } };
|
|
645
|
+
* var eur = b.money.convert(b.money.of("100.00", "USD"), "EUR", rates);
|
|
646
|
+
*/
|
|
647
|
+
function convert(money, toCurrency, fxRateProvider, opts) {
|
|
648
|
+
_requireMoney(money, "convert source");
|
|
649
|
+
_requireCurrency(toCurrency);
|
|
650
|
+
if (!fxRateProvider || typeof fxRateProvider.rate !== "function") {
|
|
651
|
+
throw new MoneyError("money/bad-fx-provider",
|
|
652
|
+
"convert requires an fxRateProvider with a .rate(from, to) method");
|
|
653
|
+
}
|
|
654
|
+
opts = opts || {};
|
|
655
|
+
if (money.currency === toCurrency) {
|
|
656
|
+
// Identity conversion still flows through so the operator's audit
|
|
657
|
+
// hook (if any) sees the call. New instance -- Money is immutable.
|
|
658
|
+
return new Money(money._minor, money.currency);
|
|
659
|
+
}
|
|
660
|
+
var rateStr = fxRateProvider.rate(money.currency, toCurrency);
|
|
661
|
+
if (typeof rateStr !== "string") {
|
|
662
|
+
throw new MoneyError("money/bad-rate",
|
|
663
|
+
"fxRateProvider.rate must return a decimal-shaped string; got " +
|
|
664
|
+
(typeof rateStr));
|
|
665
|
+
}
|
|
666
|
+
var rational = _rationalFromDecimalString(rateStr);
|
|
667
|
+
if (rational.num < 0n) {
|
|
668
|
+
throw new MoneyError("money/bad-rate",
|
|
669
|
+
"fxRateProvider.rate returned a negative rate: " +
|
|
670
|
+
JSON.stringify(rateStr));
|
|
671
|
+
}
|
|
672
|
+
var fromExp = _exponentOf(money.currency);
|
|
673
|
+
var toExp = _exponentOf(toCurrency);
|
|
674
|
+
// Scale source minor units into the destination's exponent before
|
|
675
|
+
// applying the rate, so the rounding step happens once at the end.
|
|
676
|
+
// amount_to = amount_from * (10^toExp / 10^fromExp) * (num / den)
|
|
677
|
+
var rounding = opts.rounding === "half-up" ? "half-up" : "half-even";
|
|
678
|
+
var num = money._minor * rational.num;
|
|
679
|
+
var den = rational.den;
|
|
680
|
+
if (toExp > fromExp) {
|
|
681
|
+
num = num * _pow10(toExp - fromExp);
|
|
682
|
+
} else if (fromExp > toExp) {
|
|
683
|
+
den = den * _pow10(fromExp - toExp);
|
|
684
|
+
}
|
|
685
|
+
var minor = _divRound(num, den, rounding);
|
|
686
|
+
return new Money(minor, toCurrency);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
module.exports = {
|
|
690
|
+
of: of,
|
|
691
|
+
create: of, // alias -- matches spec's "create or of"
|
|
692
|
+
fromMinorUnits: fromMinorUnits,
|
|
693
|
+
parse: parse,
|
|
694
|
+
zero: zero,
|
|
695
|
+
convert: convert,
|
|
696
|
+
CURRENCIES: CURRENCIES,
|
|
697
|
+
Money: Money,
|
|
698
|
+
MoneyError: MoneyError,
|
|
699
|
+
};
|