subunit-money 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Conny Brunnkvist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # subunit-money
2
+
3
+ A _TypeScript-first_ [value object](https://martinfowler.com/bliki/ValueObject.html) for dealing with money and currencies. Uses _currency subunits_ (cents, pence, etc.) internally, via BigInt, for precision-safe calculations.
4
+
5
+ > **Note**: This is a complete TypeScript rewrite of [`cbrunnkvist/es-money`](https://github.com/cbrunnkvist/es-money), modernized with BigInt internals, enhanced type safety, and currency conversion support.
6
+
7
+ ## Basic Usage
8
+
9
+ ```typescript
10
+ import { Money } from 'subunit-money'
11
+
12
+ const price = new Money('USD', '19.99')
13
+ const tax = price.multiply(0.0825)
14
+ const total = price.add(tax)
15
+
16
+ console.log(total.toString()) // "21.63 USD"
17
+ console.log(total.amount) // "21.63" (string, safe for JSON/DB)
18
+ console.log(total.toNumber()) // 21.63 (number, for calculations)
19
+ ```
20
+
21
+ ## Why Model Money as a Value Object?
22
+
23
+ Naive JavaScript math fails for monetary values in subtle but critical ways:
24
+
25
+ **Floating-Point Errors**: JavaScript represents decimals in binary, so values like 19.99 are approximations. Operations on approximations compound the error.
26
+
27
+ **Deferred Rounding**: The classic example is `0.1 + 0.2 === 0.3` returning `false`. But in accounting, deferred rounding is worse—you might accumulate errors silently across many transactions.
28
+
29
+ **The Split Penny Problem**: Imagine charging tax ($1.649175) on 10 items. If you round per-item (legally required on receipts), that's $1.65 × 10 = $16.50. But if you defer rounding, 10 × $1.649175 = $16.49. That missing penny is a real problem. Money objects round immediately after multiplication to prevent this.
30
+
31
+ This library uses BigInt internally to store currency in subunits (cents, satoshis, etc.), making all arithmetic exact.
32
+
33
+ ## Features
34
+
35
+ - **Precision-safe**: BigInt internals prevent floating-point errors (`0.1 + 0.2` works correctly)
36
+ - **Type-safe**: Full TypeScript support with currency-branded types
37
+ - **Immutable**: All operations return new instances
38
+ - **Serialization-safe**: String amounts survive JSON/database round-trips
39
+ - **Zero dependencies**: Just Node.js 18+
40
+ - **Cryptocurrency ready**: Supports 8+ decimal places (Bitcoin, etc.)
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install subunit-money
46
+ ```
47
+
48
+ ## API Reference
49
+
50
+ ### Creating Money
51
+
52
+ ```typescript
53
+ // From string (recommended)
54
+ new Money('USD', '100.00')
55
+
56
+ // From number
57
+ new Money('USD', 100)
58
+
59
+ // From subunits (e.g., cents from database)
60
+ Money.fromSubunits(10000n, 'USD') // $100.00
61
+
62
+ // Zero amount
63
+ Money.zero('EUR') // 0.00 EUR
64
+
65
+ // From plain object (e.g., from JSON)
66
+ Money.fromObject({ currency: 'USD', amount: '50.00' })
67
+ ```
68
+
69
+ ### Arithmetic
70
+
71
+ ```typescript
72
+ const a = new Money('USD', '100.00')
73
+ const b = new Money('USD', '25.00')
74
+
75
+ a.add(b) // 125.00 USD
76
+ a.subtract(b) // 75.00 USD
77
+ a.multiply(1.5) // 150.00 USD
78
+
79
+ // Allocate proportionally (never loses cents)
80
+ a.allocate([70, 30]) // [70.00 USD, 30.00 USD]
81
+ a.allocate([1, 1, 1]) // [33.34 USD, 33.33 USD, 33.33 USD]
82
+ ```
83
+
84
+ ### Comparisons
85
+
86
+ ```typescript
87
+ const a = new Money('USD', '100.00')
88
+ const b = new Money('USD', '50.00')
89
+
90
+ a.equalTo(b) // false
91
+ a.greaterThan(b) // true
92
+ a.lessThan(b) // false
93
+ a.isZero() // false
94
+ a.isPositive() // true
95
+ a.isNegative() // false
96
+
97
+ // For sorting
98
+ Money.compare(a, b) // 1 (positive = a > b)
99
+ [b, a].sort(Money.compare) // [50.00, 100.00]
100
+ ```
101
+
102
+ ### Serialization
103
+
104
+ ```typescript
105
+ const money = new Money('USD', '99.99')
106
+
107
+ // For JSON APIs
108
+ money.toJSON() // { currency: 'USD', amount: '99.99' }
109
+ JSON.stringify(money) // '{"currency":"USD","amount":"99.99"}'
110
+
111
+ // For display
112
+ money.toString() // "99.99 USD"
113
+
114
+ // For database storage (as integer)
115
+ money.toSubunits() // 9999n (BigInt)
116
+ ```
117
+
118
+ ## Currency Conversion
119
+
120
+ For cross-currency operations, use `ExchangeRateService` and `MoneyConverter`:
121
+
122
+ ```typescript
123
+ import { Money, ExchangeRateService, MoneyConverter } from 'subunit-money'
124
+
125
+ // Set up exchange rates
126
+ const rates = new ExchangeRateService()
127
+ rates.setRate('USD', 'EUR', '0.92')
128
+ rates.setRate('USD', 'GBP', '0.79')
129
+
130
+ // Create converter
131
+ const converter = new MoneyConverter(rates)
132
+
133
+ // Convert currencies
134
+ const dollars = new Money('USD', '100.00')
135
+ const euros = converter.convert(dollars, 'EUR')
136
+ console.log(euros.toString()) // "92.00 EUR"
137
+
138
+ // Cross-currency arithmetic
139
+ const usd = new Money('USD', '100.00')
140
+ const eur = new Money('EUR', '50.00')
141
+ const total = converter.add(usd, eur, 'USD') // Sum in USD
142
+
143
+ // Sum multiple currencies
144
+ const amounts = [
145
+ new Money('USD', '100.00'),
146
+ new Money('EUR', '50.00'),
147
+ new Money('GBP', '25.00')
148
+ ]
149
+ converter.sum(amounts, 'USD') // Total in USD
150
+ ```
151
+
152
+ ### Exchange Rate Features
153
+
154
+ ```typescript
155
+ // Inverse rates are auto-created
156
+ rates.setRate('USD', 'EUR', '0.92')
157
+ rates.getRate('EUR', 'USD') // Returns ~1.087 automatically
158
+
159
+ // You can disable auto-inverse if you want to set both directions explicitly
160
+ rates.setRate('EUR', 'USD', '1.10', undefined, false) // Won't auto-create USD→EUR
161
+ ```
162
+
163
+ ## Custom Currencies
164
+
165
+ The module includes 120+ currencies. Add custom ones:
166
+
167
+ ```typescript
168
+ import { registerCurrency, Money } from 'subunit-money'
169
+
170
+ // Add cryptocurrency
171
+ registerCurrency('BTC', 8)
172
+ const bitcoin = new Money('BTC', '0.00010000')
173
+
174
+ // Add custom token
175
+ registerCurrency('POINTS', 0)
176
+ const points = new Money('POINTS', '500')
177
+ ```
178
+
179
+ ## Error Handling
180
+
181
+ All errors extend `Error` with specific types:
182
+
183
+ ```typescript
184
+ import {
185
+ CurrencyMismatchError, // Adding USD + EUR without converter
186
+ CurrencyUnknownError, // Unknown currency code
187
+ SubunitError, // Too many decimal places
188
+ AmountError, // Invalid amount format
189
+ ExchangeRateError // Missing exchange rate
190
+ } from 'subunit-money'
191
+
192
+ try {
193
+ const usd = new Money('USD', '100.00')
194
+ const eur = new Money('EUR', '50.00')
195
+ usd.add(eur) // Throws CurrencyMismatchError
196
+ } catch (e) {
197
+ if (e instanceof CurrencyMismatchError) {
198
+ console.log(`Cannot mix ${e.fromCurrency} and ${e.toCurrency}`)
199
+ }
200
+ }
201
+ ```
202
+
203
+ ## Design Philosophy
204
+
205
+ - **Minimal surface area**: A value object should be just a value object
206
+ - **Fail fast**: Errors thrown immediately, not silently propagated
207
+ - **No localization**: Formatting for display is your app's concern
208
+ - **No rounding rules**: Currency-specific rounding is application-specific
209
+
210
+ ## Why String Amounts?
211
+
212
+ The `amount` property returns a string to prevent accidental precision loss:
213
+
214
+ ```typescript
215
+ const money = new Money('USD', '0.10')
216
+
217
+ // Safe: survives JSON round-trip
218
+ const json = JSON.stringify(money)
219
+ const restored = Money.fromObject(JSON.parse(json))
220
+ restored.equalTo(money) // true
221
+
222
+ // Safe: database storage
223
+ db.insert({ price: money.amount }) // "0.10"
224
+ const loaded = new Money('USD', row.price) // Works perfectly
225
+ ```
226
+
227
+ Use `toNumber()` only when you explicitly need numeric calculations.
228
+
229
+ ## Requirements
230
+
231
+ - Node.js 18+
232
+ - TypeScript 5+ (for consumers using TypeScript)
233
+
234
+ ## License
235
+
236
+ Copyright (c) 2016-2025 Conny Brunnkvist. Licensed under the [MIT License](./LICENSE)