subunit-money 2.1.1 → 3.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/dist/cjs/currency.d.ts +38 -0
- package/dist/cjs/currency.js +56 -0
- package/dist/cjs/errors.d.ts +52 -0
- package/dist/cjs/errors.js +80 -0
- package/dist/cjs/exchange-rate-service.d.ts +96 -0
- package/dist/cjs/exchange-rate-service.js +174 -0
- package/dist/cjs/index.d.ts +22 -0
- package/dist/cjs/index.js +58 -0
- package/dist/cjs/money-converter.d.ts +82 -0
- package/dist/cjs/money-converter.js +172 -0
- package/dist/cjs/money.d.ts +146 -0
- package/dist/cjs/money.js +362 -0
- package/dist/currency.js +6 -14
- package/dist/errors.js +5 -13
- package/dist/exchange-rate-service.js +1 -5
- package/dist/index.js +8 -28
- package/dist/money-converter.js +11 -15
- package/dist/money.d.ts +1 -11
- package/dist/money.js +66 -88
- package/package.json +3 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Money Converter - Safe cross-currency operations.
|
|
4
|
+
*
|
|
5
|
+
* Bridges Money objects with ExchangeRateService to enable:
|
|
6
|
+
* - Currency conversion
|
|
7
|
+
* - Multi-currency arithmetic
|
|
8
|
+
* - Percentage calculations across currencies
|
|
9
|
+
*/
|
|
10
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
11
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
12
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
13
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
14
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
15
|
+
};
|
|
16
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
17
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
18
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
19
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
20
|
+
};
|
|
21
|
+
var _MoneyConverter_instances, _MoneyConverter_rateService, _MoneyConverter_bankersRound;
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.MoneyConverter = void 0;
|
|
24
|
+
const money_js_1 = require("./money.js");
|
|
25
|
+
const errors_js_1 = require("./errors.js");
|
|
26
|
+
const currency_js_1 = require("./currency.js");
|
|
27
|
+
/**
|
|
28
|
+
* Converter for performing operations between different currencies.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const rates = new ExchangeRateService()
|
|
32
|
+
* rates.setRate('USD', 'EUR', 0.92)
|
|
33
|
+
*
|
|
34
|
+
* const converter = new MoneyConverter(rates)
|
|
35
|
+
* const euros = converter.convert(new Money('USD', '100'), 'EUR')
|
|
36
|
+
* console.log(euros.toString()) // "92.00 EUR"
|
|
37
|
+
*/
|
|
38
|
+
class MoneyConverter {
|
|
39
|
+
constructor(rateService) {
|
|
40
|
+
_MoneyConverter_instances.add(this);
|
|
41
|
+
_MoneyConverter_rateService.set(this, void 0);
|
|
42
|
+
__classPrivateFieldSet(this, _MoneyConverter_rateService, rateService, "f");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert a Money amount to another currency.
|
|
46
|
+
*
|
|
47
|
+
* @param money - The amount to convert
|
|
48
|
+
* @param targetCurrency - The target currency code
|
|
49
|
+
* @returns A new Money in the target currency
|
|
50
|
+
* @throws {ExchangeRateError} If no rate is available
|
|
51
|
+
*/
|
|
52
|
+
convert(money, targetCurrency) {
|
|
53
|
+
if (money.currency === targetCurrency) {
|
|
54
|
+
return money;
|
|
55
|
+
}
|
|
56
|
+
const currencyDef = (0, currency_js_1.getCurrency)(targetCurrency);
|
|
57
|
+
if (!currencyDef) {
|
|
58
|
+
throw new errors_js_1.CurrencyUnknownError(targetCurrency);
|
|
59
|
+
}
|
|
60
|
+
const rate = __classPrivateFieldGet(this, _MoneyConverter_rateService, "f").getRate(money.currency, targetCurrency);
|
|
61
|
+
if (!rate) {
|
|
62
|
+
throw new errors_js_1.ExchangeRateError(money.currency, targetCurrency);
|
|
63
|
+
}
|
|
64
|
+
const sourceCurrencyDef = (0, currency_js_1.getCurrency)(money.currency);
|
|
65
|
+
const sourceSubunits = money.toSubunits();
|
|
66
|
+
const sourceMultiplier = 10n ** BigInt(sourceCurrencyDef.decimalDigits);
|
|
67
|
+
const targetMultiplier = 10n ** BigInt(currencyDef.decimalDigits);
|
|
68
|
+
const RATE_PRECISION = 15n;
|
|
69
|
+
const rateMultiplier = 10n ** RATE_PRECISION;
|
|
70
|
+
const rateValue = Number(rate.rate);
|
|
71
|
+
const rateBigInt = BigInt(Math.round(rateValue * Number(rateMultiplier)));
|
|
72
|
+
const product = sourceSubunits * rateBigInt * targetMultiplier;
|
|
73
|
+
const divisor = rateMultiplier * sourceMultiplier;
|
|
74
|
+
const targetSubunits = __classPrivateFieldGet(this, _MoneyConverter_instances, "m", _MoneyConverter_bankersRound).call(this, product, divisor);
|
|
75
|
+
return money_js_1.Money.fromSubunits(targetSubunits, targetCurrency);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Add two Money amounts, converting as needed.
|
|
79
|
+
*
|
|
80
|
+
* @param a - First amount
|
|
81
|
+
* @param b - Second amount
|
|
82
|
+
* @param resultCurrency - Currency for the result (must be one of the input currencies)
|
|
83
|
+
* @returns Sum in the result currency
|
|
84
|
+
*/
|
|
85
|
+
add(a, b, resultCurrency) {
|
|
86
|
+
const aConverted = this.convert(a, resultCurrency);
|
|
87
|
+
const bConverted = this.convert(b, resultCurrency);
|
|
88
|
+
return aConverted.add(bConverted);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Subtract two Money amounts, converting as needed.
|
|
92
|
+
*
|
|
93
|
+
* @param a - Amount to subtract from
|
|
94
|
+
* @param b - Amount to subtract
|
|
95
|
+
* @param resultCurrency - Currency for the result
|
|
96
|
+
* @returns Difference in the result currency
|
|
97
|
+
*/
|
|
98
|
+
subtract(a, b, resultCurrency) {
|
|
99
|
+
const aConverted = this.convert(a, resultCurrency);
|
|
100
|
+
const bConverted = this.convert(b, resultCurrency);
|
|
101
|
+
return aConverted.subtract(bConverted);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Calculate what percentage one amount is of another.
|
|
105
|
+
* Converts both to the same currency before comparison.
|
|
106
|
+
*
|
|
107
|
+
* @param part - The partial amount
|
|
108
|
+
* @param whole - The whole amount
|
|
109
|
+
* @returns Percentage as a number (e.g., 25 for 25%)
|
|
110
|
+
*/
|
|
111
|
+
percentageOf(part, whole) {
|
|
112
|
+
// Convert both to the 'whole' currency for comparison
|
|
113
|
+
const partConverted = this.convert(part, whole.currency);
|
|
114
|
+
return (partConverted.toNumber() / whole.toNumber()) * 100;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Sum multiple Money amounts, converting all to a target currency.
|
|
118
|
+
*
|
|
119
|
+
* @param amounts - Array of Money objects (can be different currencies)
|
|
120
|
+
* @param targetCurrency - Currency for the result
|
|
121
|
+
* @returns Total in the target currency
|
|
122
|
+
*/
|
|
123
|
+
sum(amounts, targetCurrency) {
|
|
124
|
+
let total = money_js_1.Money.zero(targetCurrency);
|
|
125
|
+
for (const amount of amounts) {
|
|
126
|
+
const converted = this.convert(amount, targetCurrency);
|
|
127
|
+
total = total.add(converted);
|
|
128
|
+
}
|
|
129
|
+
return total;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Compare two Money amounts across currencies.
|
|
133
|
+
* Returns negative if a < b, zero if equal, positive if a > b.
|
|
134
|
+
*
|
|
135
|
+
* @param a - First amount
|
|
136
|
+
* @param b - Second amount
|
|
137
|
+
* @returns Comparison result
|
|
138
|
+
*/
|
|
139
|
+
compare(a, b) {
|
|
140
|
+
// Convert b to a's currency for comparison
|
|
141
|
+
const bConverted = this.convert(b, a.currency);
|
|
142
|
+
return money_js_1.Money.compare(a, bConverted);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the exchange rate service (for direct rate access).
|
|
146
|
+
*/
|
|
147
|
+
get rateService() {
|
|
148
|
+
return __classPrivateFieldGet(this, _MoneyConverter_rateService, "f");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
exports.MoneyConverter = MoneyConverter;
|
|
152
|
+
_MoneyConverter_rateService = new WeakMap(), _MoneyConverter_instances = new WeakSet(), _MoneyConverter_bankersRound = function _MoneyConverter_bankersRound(numerator, denominator) {
|
|
153
|
+
if (denominator === 1n)
|
|
154
|
+
return numerator;
|
|
155
|
+
const quotient = numerator / denominator;
|
|
156
|
+
const remainder = numerator % denominator;
|
|
157
|
+
if (remainder === 0n)
|
|
158
|
+
return quotient;
|
|
159
|
+
const halfDenominator = denominator / 2n;
|
|
160
|
+
const absRemainder = remainder < 0n ? -remainder : remainder;
|
|
161
|
+
if (absRemainder > halfDenominator) {
|
|
162
|
+
return numerator < 0n ? quotient - 1n : quotient + 1n;
|
|
163
|
+
}
|
|
164
|
+
if (absRemainder === halfDenominator) {
|
|
165
|
+
const isQuotientEven = quotient % 2n === 0n;
|
|
166
|
+
if (isQuotientEven) {
|
|
167
|
+
return quotient;
|
|
168
|
+
}
|
|
169
|
+
return numerator < 0n ? quotient - 1n : quotient + 1n;
|
|
170
|
+
}
|
|
171
|
+
return quotient;
|
|
172
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money - An immutable value object for monetary amounts.
|
|
3
|
+
*
|
|
4
|
+
* Design principles:
|
|
5
|
+
* - Immutable: all operations return new instances
|
|
6
|
+
* - Type-safe: currency mismatches are caught at compile time (when possible) and runtime
|
|
7
|
+
* - Precise: uses BigInt internally to avoid floating-point errors
|
|
8
|
+
* - String-based API: amounts are strings to preserve precision in JSON/DB round-trips
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Serialized form of a Money object, safe for JSON.
|
|
12
|
+
*/
|
|
13
|
+
export interface MoneyObject<C extends string = string> {
|
|
14
|
+
currency: C;
|
|
15
|
+
amount: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Money class - represents a monetary amount in a specific currency.
|
|
19
|
+
*
|
|
20
|
+
* @typeParam C - The currency code type (enables compile-time currency checking)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const price = new Money('USD', '19.99')
|
|
24
|
+
* const tax = price.multiply(0.08)
|
|
25
|
+
* const total = price.add(tax)
|
|
26
|
+
* console.log(total.amount) // "21.59"
|
|
27
|
+
*/
|
|
28
|
+
export declare class Money<C extends string = string> {
|
|
29
|
+
#private;
|
|
30
|
+
readonly currency: C;
|
|
31
|
+
/**
|
|
32
|
+
* Create a new Money instance.
|
|
33
|
+
*
|
|
34
|
+
* @param currency - ISO 4217 currency code (must be registered)
|
|
35
|
+
* @param amount - The amount as a number or string
|
|
36
|
+
* @throws {CurrencyUnknownError} If the currency is not registered
|
|
37
|
+
* @throws {AmountError} If the amount is not a valid number
|
|
38
|
+
* @throws {SubunitError} If the amount has more decimals than the currency allows
|
|
39
|
+
*/
|
|
40
|
+
constructor(currency: C, amount: number | string);
|
|
41
|
+
/**
|
|
42
|
+
* The amount as a formatted string with correct decimal places.
|
|
43
|
+
* @example
|
|
44
|
+
* new Money('USD', 19.9).amount // "19.90"
|
|
45
|
+
* new Money('JPY', 1000).amount // "1000"
|
|
46
|
+
*/
|
|
47
|
+
get amount(): string;
|
|
48
|
+
/**
|
|
49
|
+
* Add another Money amount.
|
|
50
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
51
|
+
*/
|
|
52
|
+
add(other: Money<C>): Money<C>;
|
|
53
|
+
/**
|
|
54
|
+
* Subtract another Money amount.
|
|
55
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
56
|
+
*/
|
|
57
|
+
subtract(other: Money<C>): Money<C>;
|
|
58
|
+
/**
|
|
59
|
+
* Multiply by a factor.
|
|
60
|
+
*
|
|
61
|
+
* DESIGN: Rounds immediately after multiplication using banker's rounding
|
|
62
|
+
* (round half-to-even). This prevents the "split penny problem".
|
|
63
|
+
*/
|
|
64
|
+
multiply(factor: number): Money<C>;
|
|
65
|
+
/**
|
|
66
|
+
* Allocate this amount proportionally.
|
|
67
|
+
* Handles remainder distribution to avoid losing pennies.
|
|
68
|
+
*
|
|
69
|
+
* @param proportions - Array of proportions (e.g., [1, 1, 1] for three-way split)
|
|
70
|
+
* @returns Array of Money objects that sum to the original amount
|
|
71
|
+
*/
|
|
72
|
+
allocate(proportions: number[]): Money<C>[];
|
|
73
|
+
/**
|
|
74
|
+
* Check if this amount equals another.
|
|
75
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
76
|
+
*/
|
|
77
|
+
equalTo(other: Money<C>): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Check if this amount is greater than another.
|
|
80
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
81
|
+
*/
|
|
82
|
+
greaterThan(other: Money<C>): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Check if this amount is less than another.
|
|
85
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
86
|
+
*/
|
|
87
|
+
lessThan(other: Money<C>): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Check if this amount is greater than or equal to another.
|
|
90
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
91
|
+
*/
|
|
92
|
+
greaterThanOrEqual(other: Money<C>): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Check if this amount is less than or equal to another.
|
|
95
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
96
|
+
*/
|
|
97
|
+
lessThanOrEqual(other: Money<C>): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Check if this amount is zero.
|
|
100
|
+
*/
|
|
101
|
+
isZero(): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Check if this amount is positive (greater than zero).
|
|
104
|
+
*/
|
|
105
|
+
isPositive(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Check if this amount is negative (less than zero).
|
|
108
|
+
*/
|
|
109
|
+
isNegative(): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Convert to a plain object (safe for JSON).
|
|
112
|
+
*/
|
|
113
|
+
toJSON(): MoneyObject<C>;
|
|
114
|
+
/**
|
|
115
|
+
* Convert to string representation.
|
|
116
|
+
*/
|
|
117
|
+
toString(): string;
|
|
118
|
+
/**
|
|
119
|
+
* Get the amount as a number (may lose precision for large values).
|
|
120
|
+
* Use with caution - prefer string-based operations.
|
|
121
|
+
*/
|
|
122
|
+
toNumber(): number;
|
|
123
|
+
/**
|
|
124
|
+
* Get the amount in subunits (e.g., cents for USD).
|
|
125
|
+
* Useful for database storage (Stripe-style integer storage).
|
|
126
|
+
*/
|
|
127
|
+
toSubunits(): bigint;
|
|
128
|
+
/**
|
|
129
|
+
* Create a Money instance from a plain object.
|
|
130
|
+
*/
|
|
131
|
+
static fromObject<C extends string>(obj: MoneyObject<C>): Money<C>;
|
|
132
|
+
/**
|
|
133
|
+
* Create a Money instance from subunits (e.g., cents).
|
|
134
|
+
* Useful for loading from database (Stripe-style integer storage).
|
|
135
|
+
*/
|
|
136
|
+
static fromSubunits<C extends string>(subunits: bigint | number, currency: C): Money<C>;
|
|
137
|
+
/**
|
|
138
|
+
* Compare two Money objects (for use with Array.sort).
|
|
139
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
140
|
+
*/
|
|
141
|
+
static compare<C extends string>(a: Money<C>, b: Money<C>): -1 | 0 | 1;
|
|
142
|
+
/**
|
|
143
|
+
* Create a zero amount in the specified currency.
|
|
144
|
+
*/
|
|
145
|
+
static zero<C extends string>(currency: C): Money<C>;
|
|
146
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Money - An immutable value object for monetary amounts.
|
|
4
|
+
*
|
|
5
|
+
* Design principles:
|
|
6
|
+
* - Immutable: all operations return new instances
|
|
7
|
+
* - Type-safe: currency mismatches are caught at compile time (when possible) and runtime
|
|
8
|
+
* - Precise: uses BigInt internally to avoid floating-point errors
|
|
9
|
+
* - String-based API: amounts are strings to preserve precision in JSON/DB round-trips
|
|
10
|
+
*/
|
|
11
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
12
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
13
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
14
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
15
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
16
|
+
};
|
|
17
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
18
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
19
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
20
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
21
|
+
};
|
|
22
|
+
var _Money_instances, _a, _Money_subunits, _Money_currencyDef, _Money_parseAmount, _Money_assertSameCurrency, _Money_getInternalValue, _Money_parseFactor, _Money_roundedDivide, _Money_createFromSubunits, _Money_formatSubunits;
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.Money = void 0;
|
|
25
|
+
const errors_js_1 = require("./errors.js");
|
|
26
|
+
const currency_js_1 = require("./currency.js");
|
|
27
|
+
/**
|
|
28
|
+
* Money class - represents a monetary amount in a specific currency.
|
|
29
|
+
*
|
|
30
|
+
* @typeParam C - The currency code type (enables compile-time currency checking)
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const price = new Money('USD', '19.99')
|
|
34
|
+
* const tax = price.multiply(0.08)
|
|
35
|
+
* const total = price.add(tax)
|
|
36
|
+
* console.log(total.amount) // "21.59"
|
|
37
|
+
*/
|
|
38
|
+
class Money {
|
|
39
|
+
/**
|
|
40
|
+
* Create a new Money instance.
|
|
41
|
+
*
|
|
42
|
+
* @param currency - ISO 4217 currency code (must be registered)
|
|
43
|
+
* @param amount - The amount as a number or string
|
|
44
|
+
* @throws {CurrencyUnknownError} If the currency is not registered
|
|
45
|
+
* @throws {AmountError} If the amount is not a valid number
|
|
46
|
+
* @throws {SubunitError} If the amount has more decimals than the currency allows
|
|
47
|
+
*/
|
|
48
|
+
constructor(currency, amount) {
|
|
49
|
+
_Money_instances.add(this);
|
|
50
|
+
// Private BigInt storage - stores currency native subunits directly
|
|
51
|
+
_Money_subunits.set(this, void 0);
|
|
52
|
+
_Money_currencyDef.set(this, void 0);
|
|
53
|
+
const currencyDef = (0, currency_js_1.getCurrency)(currency);
|
|
54
|
+
if (!currencyDef) {
|
|
55
|
+
throw new errors_js_1.CurrencyUnknownError(currency);
|
|
56
|
+
}
|
|
57
|
+
this.currency = currency;
|
|
58
|
+
__classPrivateFieldSet(this, _Money_currencyDef, currencyDef, "f");
|
|
59
|
+
__classPrivateFieldSet(this, _Money_subunits, __classPrivateFieldGet(this, _Money_instances, "m", _Money_parseAmount).call(this, amount), "f");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The amount as a formatted string with correct decimal places.
|
|
63
|
+
* @example
|
|
64
|
+
* new Money('USD', 19.9).amount // "19.90"
|
|
65
|
+
* new Money('JPY', 1000).amount // "1000"
|
|
66
|
+
*/
|
|
67
|
+
get amount() {
|
|
68
|
+
const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
|
|
69
|
+
const abs = __classPrivateFieldGet(this, _Money_subunits, "f") < 0n ? -__classPrivateFieldGet(this, _Money_subunits, "f") : __classPrivateFieldGet(this, _Money_subunits, "f");
|
|
70
|
+
const isNegative = __classPrivateFieldGet(this, _Money_subunits, "f") < 0n;
|
|
71
|
+
if (decimals === 0) {
|
|
72
|
+
return `${isNegative ? '-' : ''}${abs}`;
|
|
73
|
+
}
|
|
74
|
+
const multiplier = 10n ** BigInt(decimals);
|
|
75
|
+
const wholePart = abs / multiplier;
|
|
76
|
+
const fracPart = abs % multiplier;
|
|
77
|
+
const sign = isNegative ? '-' : '';
|
|
78
|
+
return `${sign}${wholePart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
79
|
+
}
|
|
80
|
+
// ============ Arithmetic Operations ============
|
|
81
|
+
/**
|
|
82
|
+
* Add another Money amount.
|
|
83
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
84
|
+
*/
|
|
85
|
+
add(other) {
|
|
86
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
87
|
+
const result = __classPrivateFieldGet(this, _Money_subunits, "f") + __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
88
|
+
return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Subtract another Money amount.
|
|
92
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
93
|
+
*/
|
|
94
|
+
subtract(other) {
|
|
95
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
96
|
+
const result = __classPrivateFieldGet(this, _Money_subunits, "f") - __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
97
|
+
return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Multiply by a factor.
|
|
101
|
+
*
|
|
102
|
+
* DESIGN: Rounds immediately after multiplication using banker's rounding
|
|
103
|
+
* (round half-to-even). This prevents the "split penny problem".
|
|
104
|
+
*/
|
|
105
|
+
multiply(factor) {
|
|
106
|
+
if (typeof factor !== 'number' || !Number.isFinite(factor)) {
|
|
107
|
+
throw new TypeError(`Factor must be a finite number, got: ${factor}`);
|
|
108
|
+
}
|
|
109
|
+
const { value: factorValue, scale } = __classPrivateFieldGet(_a, _a, "m", _Money_parseFactor).call(_a, factor);
|
|
110
|
+
const product = __classPrivateFieldGet(this, _Money_subunits, "f") * factorValue;
|
|
111
|
+
const divisor = 10n ** scale;
|
|
112
|
+
const result = __classPrivateFieldGet(_a, _a, "m", _Money_roundedDivide).call(_a, product, divisor);
|
|
113
|
+
return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Allocate this amount proportionally.
|
|
117
|
+
* Handles remainder distribution to avoid losing pennies.
|
|
118
|
+
*
|
|
119
|
+
* @param proportions - Array of proportions (e.g., [1, 1, 1] for three-way split)
|
|
120
|
+
* @returns Array of Money objects that sum to the original amount
|
|
121
|
+
*/
|
|
122
|
+
allocate(proportions) {
|
|
123
|
+
if (!Array.isArray(proportions) || proportions.length === 0) {
|
|
124
|
+
throw new TypeError('Proportions must be a non-empty array');
|
|
125
|
+
}
|
|
126
|
+
for (const p of proportions) {
|
|
127
|
+
if (typeof p !== 'number' || !Number.isFinite(p) || p < 0) {
|
|
128
|
+
throw new TypeError('All proportions must be non-negative finite numbers');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const total = proportions.reduce((sum, p) => sum + p, 0);
|
|
132
|
+
if (total <= 0) {
|
|
133
|
+
throw new TypeError('Sum of proportions must be positive');
|
|
134
|
+
}
|
|
135
|
+
const totalSubunits = __classPrivateFieldGet(this, _Money_subunits, "f");
|
|
136
|
+
// Calculate base allocations
|
|
137
|
+
const allocations = proportions.map((p) => {
|
|
138
|
+
return (totalSubunits * BigInt(Math.round(p * 1000000))) / BigInt(Math.round(total * 1000000));
|
|
139
|
+
});
|
|
140
|
+
// Distribute remainder
|
|
141
|
+
let remainder = totalSubunits - allocations.reduce((sum, a) => sum + a, 0n);
|
|
142
|
+
let i = 0;
|
|
143
|
+
while (remainder > 0n) {
|
|
144
|
+
allocations[i % allocations.length] += 1n;
|
|
145
|
+
remainder -= 1n;
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
148
|
+
while (remainder < 0n) {
|
|
149
|
+
allocations[i % allocations.length] -= 1n;
|
|
150
|
+
remainder += 1n;
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
// Convert back to Money objects
|
|
154
|
+
return allocations.map((subunits) => {
|
|
155
|
+
return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, subunits, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// ============ Comparison Operations ============
|
|
159
|
+
/**
|
|
160
|
+
* Check if this amount equals another.
|
|
161
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
162
|
+
*/
|
|
163
|
+
equalTo(other) {
|
|
164
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
165
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") === __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Check if this amount is greater than another.
|
|
169
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
170
|
+
*/
|
|
171
|
+
greaterThan(other) {
|
|
172
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
173
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") > __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check if this amount is less than another.
|
|
177
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
178
|
+
*/
|
|
179
|
+
lessThan(other) {
|
|
180
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
181
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") < __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if this amount is greater than or equal to another.
|
|
185
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
186
|
+
*/
|
|
187
|
+
greaterThanOrEqual(other) {
|
|
188
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
189
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") >= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if this amount is less than or equal to another.
|
|
193
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
194
|
+
*/
|
|
195
|
+
lessThanOrEqual(other) {
|
|
196
|
+
__classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
|
|
197
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") <= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if this amount is zero.
|
|
201
|
+
*/
|
|
202
|
+
isZero() {
|
|
203
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") === 0n;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if this amount is positive (greater than zero).
|
|
207
|
+
*/
|
|
208
|
+
isPositive() {
|
|
209
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") > 0n;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Check if this amount is negative (less than zero).
|
|
213
|
+
*/
|
|
214
|
+
isNegative() {
|
|
215
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f") < 0n;
|
|
216
|
+
}
|
|
217
|
+
// ============ Serialization ============
|
|
218
|
+
/**
|
|
219
|
+
* Convert to a plain object (safe for JSON).
|
|
220
|
+
*/
|
|
221
|
+
toJSON() {
|
|
222
|
+
return {
|
|
223
|
+
currency: this.currency,
|
|
224
|
+
amount: this.amount,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Convert to string representation.
|
|
229
|
+
*/
|
|
230
|
+
toString() {
|
|
231
|
+
return `${this.amount} ${this.currency}`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get the amount as a number (may lose precision for large values).
|
|
235
|
+
* Use with caution - prefer string-based operations.
|
|
236
|
+
*/
|
|
237
|
+
toNumber() {
|
|
238
|
+
return Number(this.amount);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get the amount in subunits (e.g., cents for USD).
|
|
242
|
+
* Useful for database storage (Stripe-style integer storage).
|
|
243
|
+
*/
|
|
244
|
+
toSubunits() {
|
|
245
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f");
|
|
246
|
+
}
|
|
247
|
+
// ============ Static Factory Methods ============
|
|
248
|
+
/**
|
|
249
|
+
* Create a Money instance from a plain object.
|
|
250
|
+
*/
|
|
251
|
+
static fromObject(obj) {
|
|
252
|
+
return new _a(obj.currency, obj.amount);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Create a Money instance from subunits (e.g., cents).
|
|
256
|
+
* Useful for loading from database (Stripe-style integer storage).
|
|
257
|
+
*/
|
|
258
|
+
static fromSubunits(subunits, currency) {
|
|
259
|
+
const currencyDef = (0, currency_js_1.getCurrency)(currency);
|
|
260
|
+
if (!currencyDef) {
|
|
261
|
+
throw new errors_js_1.CurrencyUnknownError(currency);
|
|
262
|
+
}
|
|
263
|
+
const bigintSubunits = typeof subunits === 'number' ? BigInt(subunits) : subunits;
|
|
264
|
+
return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, bigintSubunits, currency, currencyDef);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Compare two Money objects (for use with Array.sort).
|
|
268
|
+
* @throws {CurrencyMismatchError} If currencies don't match
|
|
269
|
+
*/
|
|
270
|
+
static compare(a, b) {
|
|
271
|
+
if (a.currency !== b.currency) {
|
|
272
|
+
throw new errors_js_1.CurrencyMismatchError(a.currency, b.currency);
|
|
273
|
+
}
|
|
274
|
+
const aVal = __classPrivateFieldGet(a, _Money_instances, "m", _Money_getInternalValue).call(a);
|
|
275
|
+
const bVal = __classPrivateFieldGet(b, _Money_instances, "m", _Money_getInternalValue).call(b);
|
|
276
|
+
if (aVal < bVal)
|
|
277
|
+
return -1;
|
|
278
|
+
if (aVal > bVal)
|
|
279
|
+
return 1;
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Create a zero amount in the specified currency.
|
|
284
|
+
*/
|
|
285
|
+
static zero(currency) {
|
|
286
|
+
return new _a(currency, '0');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
exports.Money = Money;
|
|
290
|
+
_a = Money, _Money_subunits = new WeakMap(), _Money_currencyDef = new WeakMap(), _Money_instances = new WeakSet(), _Money_parseAmount = function _Money_parseAmount(amount) {
|
|
291
|
+
const str = typeof amount === 'number' ? String(amount) : amount;
|
|
292
|
+
const match = str.match(/^(-)?(\d+)(?:\.(\d+))?$/);
|
|
293
|
+
if (!match) {
|
|
294
|
+
throw new errors_js_1.AmountError(amount);
|
|
295
|
+
}
|
|
296
|
+
const [, sign, whole, frac = ''] = match;
|
|
297
|
+
if (frac.length > __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits) {
|
|
298
|
+
throw new errors_js_1.SubunitError(this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits);
|
|
299
|
+
}
|
|
300
|
+
const paddedFrac = frac.padEnd(__classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits, '0');
|
|
301
|
+
const combined = BigInt(whole + paddedFrac);
|
|
302
|
+
return sign === '-' ? -combined : combined;
|
|
303
|
+
}, _Money_assertSameCurrency = function _Money_assertSameCurrency(other) {
|
|
304
|
+
if (this.currency !== other.currency) {
|
|
305
|
+
throw new errors_js_1.CurrencyMismatchError(this.currency, other.currency);
|
|
306
|
+
}
|
|
307
|
+
}, _Money_getInternalValue = function _Money_getInternalValue() {
|
|
308
|
+
return __classPrivateFieldGet(this, _Money_subunits, "f");
|
|
309
|
+
}, _Money_parseFactor = function _Money_parseFactor(factor) {
|
|
310
|
+
const str = String(factor);
|
|
311
|
+
const [base, exponent] = str.split('e');
|
|
312
|
+
const baseMatch = base.match(/^(-)?(\d+)(?:\.(\d+))?$/);
|
|
313
|
+
if (!baseMatch) {
|
|
314
|
+
// Fallback for unlikely cases, though String(number) should strictly produce valid formats
|
|
315
|
+
throw new TypeError(`Invalid factor format: ${str}`);
|
|
316
|
+
}
|
|
317
|
+
const [, sign, whole, frac = ''] = baseMatch;
|
|
318
|
+
const baseValue = BigInt((sign || '') + whole + frac);
|
|
319
|
+
const baseDecimals = frac.length;
|
|
320
|
+
const exp = exponent ? Number(exponent) : 0;
|
|
321
|
+
const netExp = exp - baseDecimals;
|
|
322
|
+
if (netExp >= 0) {
|
|
323
|
+
return { value: baseValue * 10n ** BigInt(netExp), scale: 0n };
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
return { value: baseValue, scale: BigInt(-netExp) };
|
|
327
|
+
}
|
|
328
|
+
}, _Money_roundedDivide = function _Money_roundedDivide(numerator, denominator) {
|
|
329
|
+
if (denominator === 1n)
|
|
330
|
+
return numerator;
|
|
331
|
+
const quotient = numerator / denominator;
|
|
332
|
+
const remainder = numerator % denominator;
|
|
333
|
+
if (remainder === 0n)
|
|
334
|
+
return quotient;
|
|
335
|
+
const halfDenominator = denominator / 2n;
|
|
336
|
+
const absRemainder = remainder < 0n ? -remainder : remainder;
|
|
337
|
+
if (absRemainder > halfDenominator) {
|
|
338
|
+
return numerator < 0n ? quotient - 1n : quotient + 1n;
|
|
339
|
+
}
|
|
340
|
+
if (absRemainder === halfDenominator) {
|
|
341
|
+
const isQuotientEven = quotient % 2n === 0n;
|
|
342
|
+
if (isQuotientEven) {
|
|
343
|
+
return quotient;
|
|
344
|
+
}
|
|
345
|
+
return numerator < 0n ? quotient - 1n : quotient + 1n;
|
|
346
|
+
}
|
|
347
|
+
return quotient;
|
|
348
|
+
}, _Money_createFromSubunits = function _Money_createFromSubunits(subunits, currency, currencyDef) {
|
|
349
|
+
return new _a(currency, __classPrivateFieldGet(_a, _a, "m", _Money_formatSubunits).call(_a, subunits, currencyDef));
|
|
350
|
+
}, _Money_formatSubunits = function _Money_formatSubunits(subunits, currencyDef) {
|
|
351
|
+
const decimals = currencyDef.decimalDigits;
|
|
352
|
+
const abs = subunits < 0n ? -subunits : subunits;
|
|
353
|
+
const isNegative = subunits < 0n;
|
|
354
|
+
if (decimals === 0) {
|
|
355
|
+
return `${isNegative ? '-' : ''}${abs}`;
|
|
356
|
+
}
|
|
357
|
+
const multiplier = 10n ** BigInt(decimals);
|
|
358
|
+
const wholePart = abs / multiplier;
|
|
359
|
+
const fracPart = abs % multiplier;
|
|
360
|
+
const sign = isNegative ? '-' : '';
|
|
361
|
+
return `${sign}${wholePart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
362
|
+
};
|