@arki/money 0.0.0-stage → 0.0.4

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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -2
  3. package/dist/bag.d.ts +48 -0
  4. package/dist/bag.d.ts.map +1 -0
  5. package/dist/bag.js +110 -0
  6. package/dist/bag.js.map +1 -0
  7. package/dist/currency.d.ts +64 -0
  8. package/dist/currency.d.ts.map +1 -0
  9. package/dist/currency.js +148 -0
  10. package/dist/currency.js.map +1 -0
  11. package/dist/data/iso4217.d.ts +9 -0
  12. package/dist/data/iso4217.d.ts.map +1 -0
  13. package/dist/data/iso4217.js +11 -0
  14. package/dist/data/iso4217.js.map +1 -0
  15. package/dist/drizzle/config.d.ts +27 -0
  16. package/dist/drizzle/config.d.ts.map +1 -0
  17. package/dist/drizzle/config.js +16 -0
  18. package/dist/drizzle/config.js.map +1 -0
  19. package/dist/drizzle/mysql.d.ts +36 -0
  20. package/dist/drizzle/mysql.d.ts.map +1 -0
  21. package/dist/drizzle/mysql.js +45 -0
  22. package/dist/drizzle/mysql.js.map +1 -0
  23. package/dist/drizzle/pg.d.ts +38 -0
  24. package/dist/drizzle/pg.d.ts.map +1 -0
  25. package/dist/drizzle/pg.js +49 -0
  26. package/dist/drizzle/pg.js.map +1 -0
  27. package/dist/drizzle/sqlite.d.ts +38 -0
  28. package/dist/drizzle/sqlite.d.ts.map +1 -0
  29. package/dist/drizzle/sqlite.js +45 -0
  30. package/dist/drizzle/sqlite.js.map +1 -0
  31. package/dist/drizzle.d.ts +56 -0
  32. package/dist/drizzle.d.ts.map +1 -0
  33. package/dist/drizzle.js +77 -0
  34. package/dist/drizzle.js.map +1 -0
  35. package/dist/errors.d.ts +32 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +42 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/exact.d.ts +20 -0
  40. package/dist/exact.d.ts.map +1 -0
  41. package/dist/exact.js +23 -0
  42. package/dist/exact.js.map +1 -0
  43. package/dist/exchange.d.ts +84 -0
  44. package/dist/exchange.d.ts.map +1 -0
  45. package/dist/exchange.js +215 -0
  46. package/dist/exchange.js.map +1 -0
  47. package/dist/format.d.ts +41 -0
  48. package/dist/format.d.ts.map +1 -0
  49. package/dist/format.js +122 -0
  50. package/dist/format.js.map +1 -0
  51. package/dist/index.d.ts +7 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +4 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/internal/columns.d.ts +25 -0
  56. package/dist/internal/columns.d.ts.map +1 -0
  57. package/dist/internal/columns.js +47 -0
  58. package/dist/internal/columns.js.map +1 -0
  59. package/dist/internal/units.d.ts +66 -0
  60. package/dist/internal/units.d.ts.map +1 -0
  61. package/dist/internal/units.js +205 -0
  62. package/dist/internal/units.js.map +1 -0
  63. package/dist/money.d.ts +180 -0
  64. package/dist/money.d.ts.map +1 -0
  65. package/dist/money.js +577 -0
  66. package/dist/money.js.map +1 -0
  67. package/dist/parse.d.ts +53 -0
  68. package/dist/parse.d.ts.map +1 -0
  69. package/dist/parse.js +222 -0
  70. package/dist/parse.js.map +1 -0
  71. package/dist/provider.d.ts +22 -0
  72. package/dist/provider.d.ts.map +1 -0
  73. package/dist/provider.js +56 -0
  74. package/dist/provider.js.map +1 -0
  75. package/dist/rational.d.ts +67 -0
  76. package/dist/rational.d.ts.map +1 -0
  77. package/dist/rational.js +169 -0
  78. package/dist/rational.js.map +1 -0
  79. package/dist/stripe.d.ts +37 -0
  80. package/dist/stripe.d.ts.map +1 -0
  81. package/dist/stripe.js +75 -0
  82. package/dist/stripe.js.map +1 -0
  83. package/dist/zod.d.ts +54 -0
  84. package/dist/zod.d.ts.map +1 -0
  85. package/dist/zod.js +96 -0
  86. package/dist/zod.js.map +1 -0
  87. package/package.json +137 -4
  88. package/src/bag.ts +128 -0
  89. package/src/currency.ts +174 -0
  90. package/src/data/iso4217.ts +13 -0
  91. package/src/drizzle/config.ts +39 -0
  92. package/src/drizzle/mysql.ts +61 -0
  93. package/src/drizzle/pg.ts +64 -0
  94. package/src/drizzle/sqlite.ts +61 -0
  95. package/src/errors.ts +61 -0
  96. package/src/exact.ts +29 -0
  97. package/src/exchange.ts +280 -0
  98. package/src/format.ts +148 -0
  99. package/src/index.ts +16 -0
  100. package/src/internal/columns.ts +74 -0
  101. package/src/internal/units.ts +223 -0
  102. package/src/money.ts +692 -0
  103. package/src/parse.ts +278 -0
  104. package/src/provider.ts +67 -0
  105. package/src/rational.ts +214 -0
  106. package/src/stripe.ts +84 -0
  107. package/src/zod.ts +123 -0
package/dist/stripe.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Stripe amounts. Stripe sends integers in its own units, which differ from
3
+ * ISO 4217 for some currencies. Rules from docs.stripe.com/currencies, read
4
+ * on 2026-10-12:
5
+ *
6
+ * - Zero-decimal: BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF VND VUV XAF XOF XPF
7
+ * (`500` is 500 JPY). MGA has 2 decimals in ISO but 0 at Stripe.
8
+ * - ISK and UGX: sent with two decimals that are always `00` (`500` is 5 ISK).
9
+ * - HUF and TWD: two-decimal charges; payouts must be divisible by 100.
10
+ * - Other currencies with 2 ISO decimals: two decimals.
11
+ *
12
+ * Stripe documents no rule for currencies with 3 or 4 ISO decimals, so those
13
+ * throw `ProviderAmountError` instead of being guessed. Whether your account
14
+ * can charge a currency, and its minimum and maximum amounts, are Stripe's to
15
+ * enforce; this module only gets the units right.
16
+ */
17
+ import { toCurrency } from './currency.js';
18
+ import { ProviderAmountError } from './errors.js';
19
+ import { fromProviderUnits, toProviderUnits } from './provider.js';
20
+ /** When Stripe's rules in this module were last checked. */
21
+ export const STRIPE_RULES_VERSION = '2026-10-12';
22
+ const ZERO_DECIMAL = new Set([
23
+ 'BIF',
24
+ 'CLP',
25
+ 'DJF',
26
+ 'GNF',
27
+ 'JPY',
28
+ 'KMF',
29
+ 'KRW',
30
+ 'MGA',
31
+ 'PYG',
32
+ 'RWF',
33
+ 'VND',
34
+ 'VUV',
35
+ 'XAF',
36
+ 'XOF',
37
+ 'XPF',
38
+ ]);
39
+ const WHOLE_UNITS_TWO_DECIMALS = new Set(['ISK', 'UGX']);
40
+ const WHOLE_UNIT_PAYOUTS = new Set(['HUF', 'TWD']);
41
+ /** Stripe's unit rule for a currency and operation. Throws `ProviderAmountError` where Stripe documents none. */
42
+ export function stripeRule(currency, operation = 'charge') {
43
+ const target = toCurrency(currency);
44
+ const code = target.code;
45
+ if (!target.iso)
46
+ throw new ProviderAmountError(`${code} is a custom currency; Stripe has no rule for it`);
47
+ if (ZERO_DECIMAL.has(code))
48
+ return { scale: 0 };
49
+ if (WHOLE_UNITS_TWO_DECIMALS.has(code))
50
+ return { scale: 2, step: 100 };
51
+ if (WHOLE_UNIT_PAYOUTS.has(code))
52
+ return operation === 'payout' ? { scale: 2, step: 100 } : { scale: 2 };
53
+ if (target.scale === 2)
54
+ return { scale: 2 };
55
+ throw new ProviderAmountError(`Stripe documents no amount rule for ${code} (${target.scale} decimals in ISO 4217)`);
56
+ }
57
+ /**
58
+ * `toStripeAmount(money('10.99', 'USD'))` → `1099`; `money(5, 'ISK')` → `500`.
59
+ * Throws `ProviderAmountError` for a negative amount or one Stripe cannot express (`10.50 MGA`).
60
+ */
61
+ export function toStripeAmount(amount, operation = 'charge') {
62
+ if (amount.isNegative())
63
+ throw new ProviderAmountError('Stripe amounts are never negative');
64
+ return toProviderUnits(amount, stripeRule(amount.currency, operation));
65
+ }
66
+ /** `fromStripeAmount(1099, 'usd')` → `10.99 USD`. Accepts Stripe's lower-case codes. */
67
+ export function fromStripeAmount(amount, currency, operation = 'charge') {
68
+ const target = typeof currency === 'string' ? toCurrency(currency.toUpperCase()) : currency;
69
+ return fromProviderUnits(amount, target, stripeRule(target, operation));
70
+ }
71
+ /** The `{ amount, currency }` pair a Stripe request takes: `{ amount: 1099, currency: 'usd' }`. */
72
+ export function toStripe(amount, operation = 'charge') {
73
+ return { amount: toStripeAmount(amount, operation), currency: amount.currency.code.toLowerCase() };
74
+ }
75
+ //# sourceMappingURL=stripe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.js","sourceRoot":"","sources":["../src/stripe.ts"],"names":[],"mappings":"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"}
package/dist/zod.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Zod 4 schemas that turn input into `Money`. Optional: needs `zod` installed.
3
+ *
4
+ * ```ts
5
+ * const order = z.object({ total: moneySchema('EUR', { min: money(0, 'EUR') }) });
6
+ * order.parse({ total: { amount: '19.99', currency: 'EUR' } }).total; // Money<'EUR'>
7
+ * ```
8
+ *
9
+ * The input type is the JSON shape `{ amount: string; currency: string }`, so
10
+ * RPC clients (tRPC, oRPC) infer what to send; the output type is `Money`.
11
+ */
12
+ import { z } from 'zod';
13
+ import type { CurrencyLike } from './currency.js';
14
+ import type { Money, MoneyOptions } from './money.js';
15
+ import type { ParseOptions } from './parse.js';
16
+ import { Currency } from './currency.js';
17
+ export type MoneySchemaOptions<C extends string> = {
18
+ /** Inclusive bounds. */
19
+ min?: Money<C>;
20
+ max?: Money<C>;
21
+ /** Extra digits are rounded with this mode instead of rejected. */
22
+ rounding?: MoneyOptions['rounding'];
23
+ };
24
+ export type AnyMoneySchemaOptions = MoneySchemaOptions<string> & {
25
+ /** Custom currencies to resolve by code. */
26
+ currencies?: readonly Currency[];
27
+ };
28
+ /**
29
+ * `{ amount: '19.99', currency: 'EUR' }` → `Money<'EUR'>`. Another currency,
30
+ * a bad amount or a value out of bounds becomes a Zod issue.
31
+ */
32
+ export declare function moneySchema<const C extends string>(currency: CurrencyLike<C>, options?: MoneySchemaOptions<C>): z.ZodPipe<z.ZodObject<{
33
+ amount: z.ZodString;
34
+ currency: z.ZodString;
35
+ }, z.core.$strip>, z.ZodTransform<Money<C>, {
36
+ amount: string;
37
+ currency: string;
38
+ }>>;
39
+ /** `{ amount, currency }` in any ISO (or listed custom) currency → `Money`. */
40
+ export declare function anyMoneySchema(options?: AnyMoneySchemaOptions): z.ZodPipe<z.ZodObject<{
41
+ amount: z.ZodString;
42
+ currency: z.ZodString;
43
+ }, z.core.$strip>, z.ZodTransform<Money<string>, {
44
+ amount: string;
45
+ currency: string;
46
+ }>>;
47
+ /**
48
+ * A form field: locale text such as `'1.234,56 €'` → `Money<C>`, with the
49
+ * strict rules of `@arki/money/parse`.
50
+ */
51
+ export declare function moneyInputSchema<const C extends string>(currency: CurrencyLike<C>, options?: ParseOptions & Pick<MoneySchemaOptions<C>, 'min' | 'max'>): z.ZodPipe<z.ZodString, z.ZodTransform<Money<C>, string>>;
52
+ /** An ISO 4217 code from the shipped table, such as `'EUR'`. */
53
+ export declare const currencyCodeSchema: z.ZodString;
54
+ //# sourceMappingURL=zod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.d.ts","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAA+B,MAAM,eAAe,CAAC;AAKtE,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,MAAM,IAAI;IACjD,wBAAwB;IACxB,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACf,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACf,mEAAmE;IACnE,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,CAAC,MAAM,CAAC,GAAG;IAC/D,4CAA4C;IAC5C,UAAU,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC;CAClC,CAAC;AAkCF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,kBAAkB,CAAC,CAAC,CAAM;;;;;;IASjH;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B;;;;;;IAYjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EACrD,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAAM,4DAcxE;AAED,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,aAEyD,CAAC"}
package/dist/zod.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Zod 4 schemas that turn input into `Money`. Optional: needs `zod` installed.
3
+ *
4
+ * ```ts
5
+ * const order = z.object({ total: moneySchema('EUR', { min: money(0, 'EUR') }) });
6
+ * order.parse({ total: { amount: '19.99', currency: 'EUR' } }).total; // Money<'EUR'>
7
+ * ```
8
+ *
9
+ * The input type is the JSON shape `{ amount: string; currency: string }`, so
10
+ * RPC clients (tRPC, oRPC) infer what to send; the output type is `Money`.
11
+ */
12
+ import { z } from 'zod';
13
+ import { Currency, resolveCurrency, toCurrency } from './currency.js';
14
+ import { MathError } from './errors.js';
15
+ import { Money as MoneyClass } from './money.js';
16
+ import { createMoneyParser } from './parse.js';
17
+ function message(error) {
18
+ return error instanceof Error ? error.message : 'Invalid money';
19
+ }
20
+ function checkBounds(value, options, ctx) {
21
+ if (options.min !== undefined && value.lt(options.min)) {
22
+ ctx.addIssue({ code: 'custom', message: `Must be at least ${options.min.toString()}` });
23
+ }
24
+ if (options.max !== undefined && value.gt(options.max)) {
25
+ ctx.addIssue({ code: 'custom', message: `Must be at most ${options.max.toString()}` });
26
+ }
27
+ }
28
+ const moneyJsonShape = z.object({ amount: z.string(), currency: z.string() });
29
+ function settle(amount, currency, options, ctx) {
30
+ try {
31
+ const value = MoneyClass.of(amount, currency, { rounding: options.rounding });
32
+ checkBounds(value, options, ctx);
33
+ return value;
34
+ }
35
+ catch (error) {
36
+ if (!(error instanceof MathError))
37
+ throw error;
38
+ ctx.addIssue({ code: 'custom', message: message(error), path: ['amount'] });
39
+ return z.NEVER;
40
+ }
41
+ }
42
+ /**
43
+ * `{ amount: '19.99', currency: 'EUR' }` → `Money<'EUR'>`. Another currency,
44
+ * a bad amount or a value out of bounds becomes a Zod issue.
45
+ */
46
+ export function moneySchema(currency, options = {}) {
47
+ const expected = toCurrency(currency);
48
+ return moneyJsonShape.transform((input, ctx) => {
49
+ if (input.currency !== expected.code) {
50
+ ctx.addIssue({ code: 'custom', message: `Expected ${expected.code}, got ${input.currency}`, path: ['currency'] });
51
+ return z.NEVER;
52
+ }
53
+ return settle(input.amount, expected, options, ctx);
54
+ });
55
+ }
56
+ /** `{ amount, currency }` in any ISO (or listed custom) currency → `Money`. */
57
+ export function anyMoneySchema(options = {}) {
58
+ return moneyJsonShape.transform((input, ctx) => {
59
+ let currency;
60
+ try {
61
+ currency = resolveCurrency(input.currency, options.currencies);
62
+ }
63
+ catch (error) {
64
+ if (!(error instanceof MathError))
65
+ throw error;
66
+ ctx.addIssue({ code: 'custom', message: message(error), path: ['currency'] });
67
+ return z.NEVER;
68
+ }
69
+ return settle(input.amount, currency, options, ctx);
70
+ });
71
+ }
72
+ /**
73
+ * A form field: locale text such as `'1.234,56 €'` → `Money<C>`, with the
74
+ * strict rules of `@arki/money/parse`.
75
+ */
76
+ export function moneyInputSchema(currency, options = {}) {
77
+ const parser = createMoneyParser(currency, options);
78
+ return z.string().transform((text, ctx) => {
79
+ try {
80
+ const value = parser.parse(text);
81
+ checkBounds(value, options, ctx);
82
+ return value;
83
+ }
84
+ catch (error) {
85
+ if (!(error instanceof MathError))
86
+ throw error;
87
+ ctx.addIssue({ code: 'custom', message: message(error) });
88
+ return z.NEVER;
89
+ }
90
+ });
91
+ }
92
+ /** An ISO 4217 code from the shipped table, such as `'EUR'`. */
93
+ export const currencyCodeSchema = z
94
+ .string()
95
+ .refine(code => Currency.isKnown(code), { message: 'Unknown ISO 4217 currency code' });
96
+ //# sourceMappingURL=zod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.js","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,139 @@
1
1
  {
2
2
  "name": "@arki/money",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.4",
4
+ "description": "Exact, fast money for every JavaScript runtime — amounts kept as whole units (plain numbers, bigint only when huge), a shipped ISO 4217 currency table, exact-or-throw rounding, fair allocation, and currencies checked by the type system.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/arki-labs/arki.git"
9
+ },
10
+ "homepage": "https://arki.dev",
11
+ "bugs": {
12
+ "url": "https://github.com/arki-labs/arki/issues"
13
+ },
14
+ "keywords": [
15
+ "arki",
16
+ "typescript",
17
+ "money",
18
+ "currency",
19
+ "iso4217",
20
+ "monetary",
21
+ "decimal",
22
+ "rounding",
23
+ "allocate",
24
+ "bigint",
25
+ "finance",
26
+ "price",
27
+ "exchange",
28
+ "stripe",
29
+ "parse"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./currency": {
40
+ "types": "./dist/currency.d.ts",
41
+ "import": "./dist/currency.js",
42
+ "default": "./dist/currency.js"
43
+ },
44
+ "./errors": {
45
+ "types": "./dist/errors.d.ts",
46
+ "import": "./dist/errors.js",
47
+ "default": "./dist/errors.js"
48
+ },
49
+ "./exact": {
50
+ "types": "./dist/exact.d.ts",
51
+ "import": "./dist/exact.js",
52
+ "default": "./dist/exact.js"
53
+ },
54
+ "./format": {
55
+ "types": "./dist/format.d.ts",
56
+ "import": "./dist/format.js",
57
+ "default": "./dist/format.js"
58
+ },
59
+ "./parse": {
60
+ "types": "./dist/parse.d.ts",
61
+ "import": "./dist/parse.js",
62
+ "default": "./dist/parse.js"
63
+ },
64
+ "./exchange": {
65
+ "types": "./dist/exchange.d.ts",
66
+ "import": "./dist/exchange.js",
67
+ "default": "./dist/exchange.js"
68
+ },
69
+ "./provider": {
70
+ "types": "./dist/provider.d.ts",
71
+ "import": "./dist/provider.js",
72
+ "default": "./dist/provider.js"
73
+ },
74
+ "./stripe": {
75
+ "types": "./dist/stripe.d.ts",
76
+ "import": "./dist/stripe.js",
77
+ "default": "./dist/stripe.js"
78
+ },
79
+ "./rational": {
80
+ "types": "./dist/rational.d.ts",
81
+ "import": "./dist/rational.js",
82
+ "default": "./dist/rational.js"
83
+ },
84
+ "./bag": {
85
+ "types": "./dist/bag.d.ts",
86
+ "import": "./dist/bag.js",
87
+ "default": "./dist/bag.js"
88
+ },
89
+ "./zod": {
90
+ "types": "./dist/zod.d.ts",
91
+ "import": "./dist/zod.js",
92
+ "default": "./dist/zod.js"
93
+ },
94
+ "./drizzle/pg": {
95
+ "types": "./dist/drizzle/pg.d.ts",
96
+ "import": "./dist/drizzle/pg.js",
97
+ "default": "./dist/drizzle/pg.js"
98
+ },
99
+ "./drizzle/mysql": {
100
+ "types": "./dist/drizzle/mysql.d.ts",
101
+ "import": "./dist/drizzle/mysql.js",
102
+ "default": "./dist/drizzle/mysql.js"
103
+ },
104
+ "./drizzle/sqlite": {
105
+ "types": "./dist/drizzle/sqlite.d.ts",
106
+ "import": "./dist/drizzle/sqlite.js",
107
+ "default": "./dist/drizzle/sqlite.js"
108
+ },
109
+ "./package.json": "./package.json"
110
+ },
111
+ "files": [
112
+ "dist/**",
113
+ "src/**",
114
+ "!src/**/*.test.*",
115
+ "!src/**/*.spec.*",
116
+ "!src/**/__tests__/**",
117
+ "README.md",
118
+ "LICENSE",
119
+ "package.json"
120
+ ],
121
+ "dependencies": {
122
+ "@arki/math": "0.0.5"
123
+ },
124
+ "peerDependencies": {
125
+ "drizzle-orm": ">=1.0.0-rc.4 <2",
126
+ "zod": "^4.0.0"
127
+ },
128
+ "peerDependenciesMeta": {
129
+ "drizzle-orm": {
130
+ "optional": true
131
+ },
132
+ "zod": {
133
+ "optional": true
134
+ }
135
+ },
136
+ "publishConfig": {
137
+ "access": "public"
138
+ }
139
+ }
package/src/bag.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * A wallet of several currencies: `10 EUR + 5 USD` stays two balances. It never
3
+ * pretends mixed amounts have one total until you convert it, and then it
4
+ * converts each balance exactly and rounds once.
5
+ *
6
+ * ```ts
7
+ * const cart = MoneyBag.of(money(10, 'EUR'), money(5, 'USD')).plus(money('2.50', 'EUR'));
8
+ * cart.get('EUR').toString(); // '25/2 EUR'
9
+ * cart.convert(fx, 'EUR', { rounding: 'halfEven' }); // one EUR total
10
+ * ```
11
+ */
12
+
13
+ import { ratio } from '@arki/math/rational';
14
+
15
+ import type { Currency, CurrencyLike } from './currency.js';
16
+ import type { Converter } from './exchange.js';
17
+ import type { MoneyJSON, MoneyOptions } from './money.js';
18
+ import type { ExactMoney } from './rational.js';
19
+ import { assertSameCurrency, toCurrency } from './currency.js';
20
+ import { Money } from './money.js';
21
+ import { RationalMoney } from './rational.js';
22
+
23
+ const BRAND = Symbol('MoneyBag');
24
+
25
+ function exact(value: ExactMoney): RationalMoney {
26
+ return value instanceof RationalMoney ? value : RationalMoney.from(value);
27
+ }
28
+
29
+ /** An immutable set of balances, at most one per currency. Zero balances are dropped. */
30
+ export class MoneyBag {
31
+ private readonly balances: ReadonlyMap<string, RationalMoney>;
32
+
33
+ private constructor(balances: ReadonlyMap<string, RationalMoney>, brand: typeof BRAND) {
34
+ if (brand !== BRAND) throw new TypeError('Use MoneyBag.of() instead of new MoneyBag()');
35
+ this.balances = balances;
36
+ }
37
+
38
+ static empty(): MoneyBag {
39
+ return new MoneyBag(new Map(), BRAND);
40
+ }
41
+
42
+ /** `MoneyBag.of(money(10, 'EUR'), money(5, 'USD'))`. */
43
+ static of(...items: (ExactMoney | MoneyBag)[]): MoneyBag {
44
+ return MoneyBag.empty().plus(...items);
45
+ }
46
+
47
+ /** Adds amounts or whole bags. Returns a new bag. */
48
+ plus(...items: (ExactMoney | MoneyBag)[]): MoneyBag {
49
+ return this.combine(items, false);
50
+ }
51
+
52
+ /** Subtracts amounts or whole bags. Balances may go negative. Returns a new bag. */
53
+ minus(...items: (ExactMoney | MoneyBag)[]): MoneyBag {
54
+ return this.combine(items, true);
55
+ }
56
+
57
+ /** The exact balance in one currency; zero when the bag holds none. */
58
+ get<const C extends string>(currency: CurrencyLike<C>): RationalMoney<C> {
59
+ const target = toCurrency(currency);
60
+ const found = this.balances.get(target.code);
61
+ if (found === undefined) return RationalMoney.zero(target);
62
+ assertSameCurrency(found.currency, target);
63
+ return RationalMoney.create(found.amount, target);
64
+ }
65
+
66
+ /** The currencies with a non-zero balance, sorted by code. */
67
+ currencies(): Currency[] {
68
+ return this.entries().map(entry => entry.currency);
69
+ }
70
+
71
+ /** The non-zero balances, sorted by currency code. */
72
+ entries(): RationalMoney[] {
73
+ return [...this.balances.values()].sort((a, b) => (a.currency.code < b.currency.code ? -1 : 1));
74
+ }
75
+
76
+ /** `true` when every balance is zero. */
77
+ isZero(): boolean {
78
+ return this.balances.size === 0;
79
+ }
80
+
81
+ /**
82
+ * One total in `currency`: each balance is converted exactly, the results are
83
+ * added, and the sum is rounded once. Missing rates throw `ExchangeRateError`.
84
+ */
85
+ convert<const C extends string>(converter: Converter, currency: CurrencyLike<C>, options?: MoneyOptions): Money<C> {
86
+ const target = toCurrency(currency);
87
+ let total = ratio(0);
88
+ for (const balance of this.balances.values()) {
89
+ const rate = converter.rate(balance.currency, target);
90
+ total = total.plus(balance.amount.times(ratio(BigInt(rate.numerator), BigInt(rate.denominator))));
91
+ }
92
+ return Money.of(total, target, options);
93
+ }
94
+
95
+ /** `[{ amount: '25/2', currency: 'EUR' }, ...]`, sorted by currency code. */
96
+ toJSON(): MoneyJSON[] {
97
+ return this.entries().map(entry => entry.toJSON());
98
+ }
99
+
100
+ /** `'12.5 EUR + 5 USD'`-style text with exact fractions: `'25/2 EUR + 5 USD'`. Empty bag: `'0'`. */
101
+ toString(): string {
102
+ const entries = this.entries();
103
+ return entries.length === 0 ? '0' : entries.map(String).join(' + ');
104
+ }
105
+
106
+ [Symbol.for('nodejs.util.inspect.custom')](): string {
107
+ return `MoneyBag(${this.toString()})`;
108
+ }
109
+
110
+ [Symbol.for('Deno.customInspect')](): string {
111
+ return `MoneyBag(${this.toString()})`;
112
+ }
113
+
114
+ private combine(items: readonly (ExactMoney | MoneyBag)[], subtract: boolean): MoneyBag {
115
+ const next = new Map(this.balances);
116
+ const add = (value: RationalMoney): void => {
117
+ const code = value.currency.code;
118
+ const current = next.get(code);
119
+ const delta = subtract ? value.negated() : value;
120
+ next.set(code, current === undefined ? delta : current.plus(delta));
121
+ };
122
+ for (const item of items) {
123
+ if (item instanceof MoneyBag) for (const balance of item.balances.values()) add(balance);
124
+ else add(exact(item));
125
+ }
126
+ return new MoneyBag(new Map([...next].filter(([, balance]) => !balance.isZero())), BRAND);
127
+ }
128
+ }