monetra 0.0.1

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) 2026 Zascia Hugo
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,140 @@
1
+ # Monetra
2
+
3
+ **Monetra** is a currency-aware, integer-based money engine designed for financial correctness. It explicitly avoids floating-point arithmetic and enforces strict monetary invariants.
4
+
5
+ It is intended for use in wallet-based financial systems where correctness is paramount.
6
+
7
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
8
+ ![TypeScript](https://img.shields.io/badge/language-TypeScript-blue.svg)
9
+ ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)
10
+
11
+ ## Features
12
+
13
+ - ❌ **No floating-point arithmetic**: All values are stored in minor units (BigInt).
14
+ - ❌ **No silent rounding**: Rounding must be explicit.
15
+ - ❌ **No implicit currency conversion**: Operations between different currencies throw errors.
16
+ - ✅ **Immutable**: All operations return new objects.
17
+ - ✅ **Locale-aware formatting**: Built on `Intl` standards.
18
+ - ✅ **Allocation Engine**: Deterministic splitting of funds (e.g., 33% / 33% / 34%).
19
+
20
+ ## Table of Contents
21
+
22
+ - [Quick Start](#quick-start)
23
+ - [Core Concepts](#core-concepts)
24
+ - [Usage Examples](#usage-examples)
25
+ - [Documentation](#documentation)
26
+ - [Testing](#testing)
27
+ - [Contributing](#contributing)
28
+ - [License](#license)
29
+
30
+ ## Quick Start
31
+
32
+ ### Installation
33
+
34
+ ```bash
35
+ npm install monetra
36
+ # or
37
+ yarn add monetra
38
+ # or
39
+ pnpm add monetra
40
+ ```
41
+
42
+ ### Basic Usage
43
+
44
+ ```typescript
45
+ import { Money, USD, RoundingMode } from "monetra";
46
+
47
+ // Create money from major units (e.g., "10.50")
48
+ const price = Money.fromMajor("10.50", USD);
49
+
50
+ // Create money from minor units (e.g., 100 cents)
51
+ const tax = Money.fromMinor(100, USD); // $1.00
52
+
53
+ // Arithmetic
54
+ const total = price.add(tax); // $11.50
55
+
56
+ // Formatting
57
+ console.log(total.format()); // "$11.50"
58
+ console.log(total.format({ locale: "de-DE" })); // "11,50 $"
59
+
60
+ // Multiplication with explicit rounding
61
+ const discount = total.multiply(0.15, { rounding: RoundingMode.HALF_UP });
62
+ const finalPrice = total.subtract(discount);
63
+ ```
64
+
65
+ ## Core Concepts
66
+
67
+ ### Integer-Only Representation
68
+
69
+ Monetra stores all values in minor units (e.g., cents) using `BigInt`. This avoids the precision errors common with floating-point math.
70
+
71
+ - `$10.50` is stored as `1050n`.
72
+ - `¥100` is stored as `100n`.
73
+
74
+ ### Immutability
75
+
76
+ Money objects are immutable. Operations like `add` or `multiply` return new instances.
77
+
78
+ ```typescript
79
+ const a = Money.fromMajor("10.00", USD);
80
+ const b = a.add(Money.fromMajor("5.00", USD));
81
+
82
+ console.log(a.format()); // "$10.00" (unchanged)
83
+ console.log(b.format()); // "$15.00"
84
+ ```
85
+
86
+ ### Explicit Rounding
87
+
88
+ Operations that result in fractional minor units (like multiplication) require an explicit rounding mode.
89
+
90
+ ```typescript
91
+ const m = Money.fromMajor("10.00", USD);
92
+ // m.multiply(0.333); // Throws RoundingRequiredError
93
+ m.multiply(0.333, { rounding: RoundingMode.HALF_UP }); // OK
94
+ ```
95
+
96
+ ## Usage Examples
97
+
98
+ ### Allocation (Splitting Funds)
99
+
100
+ Split money without losing a cent. Remainders are distributed deterministically using the largest remainder method.
101
+
102
+ ```typescript
103
+ const pot = Money.fromMajor("100.00", USD);
104
+ const [part1, part2, part3] = pot.allocate([1, 1, 1]);
105
+
106
+ // part1: $33.34
107
+ // part2: $33.33
108
+ // part3: $33.33
109
+ // Sum: $100.00
110
+ ```
111
+
112
+ ## Documentation
113
+
114
+ For more detailed information, please refer to the documentation in the `docs` folder:
115
+
116
+ - [API Reference](docs/001-API-REFERENCE.md)
117
+ - [Feature Guide](docs/002-FEATURE-GUIDE.md)
118
+
119
+ ## Testing
120
+
121
+ We maintain 100% test coverage to ensure financial correctness.
122
+
123
+ ```bash
124
+ # Run all tests
125
+ npm test
126
+
127
+ # Run tests in watch mode
128
+ npm run test:watch
129
+
130
+ # Generate coverage report
131
+ npm run test:coverage
132
+ ```
133
+
134
+ ## Contributing
135
+
136
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started.
137
+
138
+ ## License
139
+
140
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CURRENCIES: () => CURRENCIES,
24
+ CurrencyMismatchError: () => CurrencyMismatchError,
25
+ EUR: () => EUR,
26
+ GBP: () => GBP,
27
+ InsufficientFundsError: () => InsufficientFundsError,
28
+ InvalidPrecisionError: () => InvalidPrecisionError,
29
+ JPY: () => JPY,
30
+ MonetraError: () => MonetraError,
31
+ Money: () => Money,
32
+ OverflowError: () => OverflowError,
33
+ RoundingMode: () => RoundingMode,
34
+ RoundingRequiredError: () => RoundingRequiredError,
35
+ USD: () => USD,
36
+ ZAR: () => ZAR,
37
+ divideWithRounding: () => divideWithRounding,
38
+ getCurrency: () => getCurrency,
39
+ getMinorUnitExponent: () => getMinorUnitExponent
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/errors/BaseError.ts
44
+ var MonetraError = class extends Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = this.constructor.name;
48
+ Object.setPrototypeOf(this, new.target.prototype);
49
+ }
50
+ };
51
+
52
+ // src/errors/CurrencyMismatchError.ts
53
+ var CurrencyMismatchError = class extends MonetraError {
54
+ constructor(expected, actual) {
55
+ super(`Currency mismatch: expected ${expected}, got ${actual}`);
56
+ }
57
+ };
58
+
59
+ // src/errors/InvalidPrecisionError.ts
60
+ var InvalidPrecisionError = class extends MonetraError {
61
+ constructor(message) {
62
+ super(message);
63
+ }
64
+ };
65
+
66
+ // src/errors/RoundingRequiredError.ts
67
+ var RoundingRequiredError = class extends MonetraError {
68
+ constructor() {
69
+ super("Rounding is required for this operation but was not provided.");
70
+ }
71
+ };
72
+
73
+ // src/errors/InsufficientFundsError.ts
74
+ var InsufficientFundsError = class extends MonetraError {
75
+ constructor() {
76
+ super("Insufficient funds for this operation.");
77
+ }
78
+ };
79
+
80
+ // src/errors/OverflowError.ts
81
+ var OverflowError = class extends MonetraError {
82
+ constructor(message = "Arithmetic overflow") {
83
+ super(message);
84
+ }
85
+ };
86
+
87
+ // src/money/guards.ts
88
+ function assertSameCurrency(a, b) {
89
+ if (a.currency.code !== b.currency.code) {
90
+ throw new CurrencyMismatchError(a.currency.code, b.currency.code);
91
+ }
92
+ }
93
+
94
+ // src/rounding/strategies.ts
95
+ var RoundingMode = /* @__PURE__ */ ((RoundingMode3) => {
96
+ RoundingMode3["HALF_UP"] = "HALF_UP";
97
+ RoundingMode3["HALF_DOWN"] = "HALF_DOWN";
98
+ RoundingMode3["HALF_EVEN"] = "HALF_EVEN";
99
+ RoundingMode3["FLOOR"] = "FLOOR";
100
+ RoundingMode3["CEIL"] = "CEIL";
101
+ return RoundingMode3;
102
+ })(RoundingMode || {});
103
+
104
+ // src/rounding/index.ts
105
+ function divideWithRounding(numerator, denominator, mode) {
106
+ if (denominator === 0n) {
107
+ throw new Error("Division by zero");
108
+ }
109
+ const quotient = numerator / denominator;
110
+ const remainder = numerator % denominator;
111
+ if (remainder === 0n) {
112
+ return quotient;
113
+ }
114
+ const sign = (numerator >= 0n ? 1n : -1n) * (denominator >= 0n ? 1n : -1n);
115
+ const absRemainder = remainder < 0n ? -remainder : remainder;
116
+ const absDenominator = denominator < 0n ? -denominator : denominator;
117
+ const isHalf = absRemainder * 2n === absDenominator;
118
+ const isMoreThanHalf = absRemainder * 2n > absDenominator;
119
+ switch (mode) {
120
+ case "FLOOR" /* FLOOR */:
121
+ return sign > 0n ? quotient : quotient - 1n;
122
+ case "CEIL" /* CEIL */:
123
+ return sign > 0n ? quotient + 1n : quotient;
124
+ case "HALF_UP" /* HALF_UP */:
125
+ if (isMoreThanHalf || isHalf) {
126
+ return sign > 0n ? quotient + 1n : quotient - 1n;
127
+ }
128
+ return quotient;
129
+ case "HALF_DOWN" /* HALF_DOWN */:
130
+ if (isMoreThanHalf) {
131
+ return sign > 0n ? quotient + 1n : quotient - 1n;
132
+ }
133
+ return quotient;
134
+ case "HALF_EVEN" /* HALF_EVEN */:
135
+ if (isMoreThanHalf) {
136
+ return sign > 0n ? quotient + 1n : quotient - 1n;
137
+ }
138
+ if (isHalf) {
139
+ if (quotient % 2n !== 0n) {
140
+ return sign > 0n ? quotient + 1n : quotient - 1n;
141
+ }
142
+ }
143
+ return quotient;
144
+ default:
145
+ throw new Error(`Unsupported rounding mode: ${mode}`);
146
+ }
147
+ }
148
+
149
+ // src/money/arithmetic.ts
150
+ function add(a, b) {
151
+ return a + b;
152
+ }
153
+ function subtract(a, b) {
154
+ return a - b;
155
+ }
156
+ function multiply(amount, multiplier, rounding) {
157
+ const { numerator, denominator } = parseMultiplier(multiplier);
158
+ const product = amount * numerator;
159
+ if (product % denominator === 0n) {
160
+ return product / denominator;
161
+ }
162
+ if (!rounding) {
163
+ throw new RoundingRequiredError();
164
+ }
165
+ return divideWithRounding(product, denominator, rounding);
166
+ }
167
+ function parseMultiplier(multiplier) {
168
+ const s = multiplier.toString();
169
+ if (/[eE]/.test(s)) {
170
+ throw new Error("Scientific notation not supported");
171
+ }
172
+ const parts = s.split(".");
173
+ if (parts.length > 2) {
174
+ throw new Error("Invalid number format");
175
+ }
176
+ const integerPart = parts[0];
177
+ const fractionalPart = parts[1] || "";
178
+ const denominator = 10n ** BigInt(fractionalPart.length);
179
+ const numerator = BigInt(integerPart + fractionalPart);
180
+ return { numerator, denominator };
181
+ }
182
+
183
+ // src/money/allocation.ts
184
+ function allocate(amount, ratios) {
185
+ if (ratios.length === 0) {
186
+ throw new Error("Cannot allocate to empty ratios");
187
+ }
188
+ const scaledRatios = ratios.map((r) => {
189
+ const s = r.toString();
190
+ if (/[eE]/.test(s)) throw new Error("Scientific notation not supported");
191
+ const parts = s.split(".");
192
+ const decimals = parts[1] ? parts[1].length : 0;
193
+ const value = BigInt(parts[0] + (parts[1] || ""));
194
+ return { value, decimals };
195
+ });
196
+ const maxDecimals = Math.max(...scaledRatios.map((r) => r.decimals));
197
+ const normalizedRatios = scaledRatios.map((r) => {
198
+ const factor = 10n ** BigInt(maxDecimals - r.decimals);
199
+ return r.value * factor;
200
+ });
201
+ const total = normalizedRatios.reduce((sum, r) => sum + r, 0n);
202
+ if (total === 0n) {
203
+ throw new Error("Total ratio must be greater than zero");
204
+ }
205
+ const results = [];
206
+ let allocatedTotal = 0n;
207
+ for (let i = 0; i < normalizedRatios.length; i++) {
208
+ const ratio = normalizedRatios[i];
209
+ const share = amount * ratio / total;
210
+ const remainder = amount * ratio % total;
211
+ results.push({ share, remainder, index: i });
212
+ allocatedTotal += share;
213
+ }
214
+ let leftOver = amount - allocatedTotal;
215
+ results.sort((a, b) => {
216
+ if (b.remainder > a.remainder) return 1;
217
+ if (b.remainder < a.remainder) return -1;
218
+ return 0;
219
+ });
220
+ for (let i = 0; i < Number(leftOver); i++) {
221
+ results[i].share += 1n;
222
+ }
223
+ results.sort((a, b) => a.index - b.index);
224
+ return results.map((r) => r.share);
225
+ }
226
+
227
+ // src/format/parser.ts
228
+ function parseToMinor(amount, currency) {
229
+ if (/[eE]/.test(amount)) {
230
+ throw new Error("Scientific notation not supported");
231
+ }
232
+ if (/[^0-9.-]/.test(amount)) {
233
+ throw new Error("Invalid characters in amount");
234
+ }
235
+ const parts = amount.split(".");
236
+ if (parts.length > 2) {
237
+ throw new Error("Invalid format: multiple decimal points");
238
+ }
239
+ const integerPart = parts[0];
240
+ const fractionalPart = parts[1] || "";
241
+ if (fractionalPart.length > currency.decimals) {
242
+ throw new InvalidPrecisionError(
243
+ `Precision ${fractionalPart.length} exceeds currency decimals ${currency.decimals}`
244
+ );
245
+ }
246
+ const paddedFractional = fractionalPart.padEnd(currency.decimals, "0");
247
+ const combined = integerPart + paddedFractional;
248
+ if (combined === "-" || combined === "") {
249
+ throw new Error("Invalid format");
250
+ }
251
+ return BigInt(combined);
252
+ }
253
+
254
+ // src/format/formatter.ts
255
+ function format(money, options) {
256
+ const locale = options?.locale || money.currency.locale || "en-US";
257
+ const showSymbol = options?.symbol ?? true;
258
+ const decimals = money.currency.decimals;
259
+ const minor = money.minor;
260
+ const absMinor = minor < 0n ? -minor : minor;
261
+ const divisor = 10n ** BigInt(decimals);
262
+ const integerPart = absMinor / divisor;
263
+ const fractionalPart = absMinor % divisor;
264
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
265
+ const parts = new Intl.NumberFormat(locale, {
266
+ style: "decimal",
267
+ minimumFractionDigits: 1
268
+ }).formatToParts(1.1);
269
+ const decimalSeparator = parts.find((p) => p.type === "decimal")?.value || ".";
270
+ const integerFormatted = new Intl.NumberFormat(locale, {
271
+ style: "decimal",
272
+ useGrouping: true
273
+ }).format(integerPart);
274
+ const absString = decimals > 0 ? `${integerFormatted}${decimalSeparator}${fractionalStr}` : integerFormatted;
275
+ if (!showSymbol) {
276
+ return minor < 0n ? `-${absString}` : absString;
277
+ }
278
+ const templateParts = new Intl.NumberFormat(locale, {
279
+ style: "currency",
280
+ currency: money.currency.code
281
+ }).formatToParts(minor < 0n ? -1 : 1);
282
+ let result = "";
283
+ let numberInserted = false;
284
+ for (const part of templateParts) {
285
+ if (["integer", "group", "decimal", "fraction"].includes(part.type)) {
286
+ if (!numberInserted) {
287
+ result += absString;
288
+ numberInserted = true;
289
+ }
290
+ } else if (part.type === "currency") {
291
+ result += part.value;
292
+ } else {
293
+ result += part.value;
294
+ }
295
+ }
296
+ return result;
297
+ }
298
+
299
+ // src/money/Money.ts
300
+ var Money = class _Money {
301
+ constructor(minor, currency) {
302
+ this.minor = minor;
303
+ this.currency = currency;
304
+ }
305
+ static fromMinor(minor, currency) {
306
+ return new _Money(BigInt(minor), currency);
307
+ }
308
+ static fromMajor(amount, currency) {
309
+ const minor = parseToMinor(amount, currency);
310
+ return new _Money(minor, currency);
311
+ }
312
+ static zero(currency) {
313
+ return new _Money(0n, currency);
314
+ }
315
+ add(other) {
316
+ assertSameCurrency(this, other);
317
+ return new _Money(add(this.minor, other.minor), this.currency);
318
+ }
319
+ subtract(other) {
320
+ assertSameCurrency(this, other);
321
+ return new _Money(subtract(this.minor, other.minor), this.currency);
322
+ }
323
+ multiply(multiplier, options) {
324
+ const result = multiply(this.minor, multiplier, options?.rounding);
325
+ return new _Money(result, this.currency);
326
+ }
327
+ allocate(ratios) {
328
+ const shares = allocate(this.minor, ratios);
329
+ return shares.map((share) => new _Money(share, this.currency));
330
+ }
331
+ format(options) {
332
+ return format(this, options);
333
+ }
334
+ equals(other) {
335
+ return this.currency.code === other.currency.code && this.minor === other.minor;
336
+ }
337
+ greaterThan(other) {
338
+ assertSameCurrency(this, other);
339
+ return this.minor > other.minor;
340
+ }
341
+ lessThan(other) {
342
+ assertSameCurrency(this, other);
343
+ return this.minor < other.minor;
344
+ }
345
+ isZero() {
346
+ return this.minor === 0n;
347
+ }
348
+ isNegative() {
349
+ return this.minor < 0n;
350
+ }
351
+ };
352
+
353
+ // src/currency/iso4217.ts
354
+ var USD = {
355
+ code: "USD",
356
+ decimals: 2,
357
+ symbol: "$",
358
+ locale: "en-US"
359
+ };
360
+ var EUR = {
361
+ code: "EUR",
362
+ decimals: 2,
363
+ symbol: "\u20AC",
364
+ locale: "de-DE"
365
+ // Default locale, can be overridden
366
+ };
367
+ var GBP = {
368
+ code: "GBP",
369
+ decimals: 2,
370
+ symbol: "\xA3",
371
+ locale: "en-GB"
372
+ };
373
+ var JPY = {
374
+ code: "JPY",
375
+ decimals: 0,
376
+ symbol: "\xA5",
377
+ locale: "ja-JP"
378
+ };
379
+ var ZAR = {
380
+ code: "ZAR",
381
+ decimals: 2,
382
+ symbol: "R",
383
+ locale: "en-ZA"
384
+ };
385
+ var CURRENCIES = {
386
+ USD,
387
+ EUR,
388
+ GBP,
389
+ JPY,
390
+ ZAR
391
+ };
392
+ function getCurrency(code) {
393
+ const currency = CURRENCIES[code.toUpperCase()];
394
+ if (!currency) {
395
+ throw new Error(`Unknown currency code: ${code}`);
396
+ }
397
+ return currency;
398
+ }
399
+
400
+ // src/currency/precision.ts
401
+ function getMinorUnitExponent(currency) {
402
+ return 10n ** BigInt(currency.decimals);
403
+ }
404
+ // Annotate the CommonJS export names for ESM import in node:
405
+ 0 && (module.exports = {
406
+ CURRENCIES,
407
+ CurrencyMismatchError,
408
+ EUR,
409
+ GBP,
410
+ InsufficientFundsError,
411
+ InvalidPrecisionError,
412
+ JPY,
413
+ MonetraError,
414
+ Money,
415
+ OverflowError,
416
+ RoundingMode,
417
+ RoundingRequiredError,
418
+ USD,
419
+ ZAR,
420
+ divideWithRounding,
421
+ getCurrency,
422
+ getMinorUnitExponent
423
+ });
424
+ //# sourceMappingURL=index.js.map