@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.
- package/LICENSE +21 -0
- package/README.md +238 -2
- package/dist/bag.d.ts +48 -0
- package/dist/bag.d.ts.map +1 -0
- package/dist/bag.js +110 -0
- package/dist/bag.js.map +1 -0
- package/dist/currency.d.ts +64 -0
- package/dist/currency.d.ts.map +1 -0
- package/dist/currency.js +148 -0
- package/dist/currency.js.map +1 -0
- package/dist/data/iso4217.d.ts +9 -0
- package/dist/data/iso4217.d.ts.map +1 -0
- package/dist/data/iso4217.js +11 -0
- package/dist/data/iso4217.js.map +1 -0
- package/dist/drizzle/config.d.ts +27 -0
- package/dist/drizzle/config.d.ts.map +1 -0
- package/dist/drizzle/config.js +16 -0
- package/dist/drizzle/config.js.map +1 -0
- package/dist/drizzle/mysql.d.ts +36 -0
- package/dist/drizzle/mysql.d.ts.map +1 -0
- package/dist/drizzle/mysql.js +45 -0
- package/dist/drizzle/mysql.js.map +1 -0
- package/dist/drizzle/pg.d.ts +38 -0
- package/dist/drizzle/pg.d.ts.map +1 -0
- package/dist/drizzle/pg.js +49 -0
- package/dist/drizzle/pg.js.map +1 -0
- package/dist/drizzle/sqlite.d.ts +38 -0
- package/dist/drizzle/sqlite.d.ts.map +1 -0
- package/dist/drizzle/sqlite.js +45 -0
- package/dist/drizzle/sqlite.js.map +1 -0
- package/dist/drizzle.d.ts +56 -0
- package/dist/drizzle.d.ts.map +1 -0
- package/dist/drizzle.js +77 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/exact.d.ts +20 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +23 -0
- package/dist/exact.js.map +1 -0
- package/dist/exchange.d.ts +84 -0
- package/dist/exchange.d.ts.map +1 -0
- package/dist/exchange.js +215 -0
- package/dist/exchange.js.map +1 -0
- package/dist/format.d.ts +41 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +122 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/columns.d.ts +25 -0
- package/dist/internal/columns.d.ts.map +1 -0
- package/dist/internal/columns.js +47 -0
- package/dist/internal/columns.js.map +1 -0
- package/dist/internal/units.d.ts +66 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +205 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/money.d.ts +180 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +577 -0
- package/dist/money.js.map +1 -0
- package/dist/parse.d.ts +53 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +222 -0
- package/dist/parse.js.map +1 -0
- package/dist/provider.d.ts +22 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +56 -0
- package/dist/provider.js.map +1 -0
- package/dist/rational.d.ts +67 -0
- package/dist/rational.d.ts.map +1 -0
- package/dist/rational.js +169 -0
- package/dist/rational.js.map +1 -0
- package/dist/stripe.d.ts +37 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +75 -0
- package/dist/stripe.js.map +1 -0
- package/dist/zod.d.ts +54 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +96 -0
- package/dist/zod.js.map +1 -0
- package/package.json +137 -4
- package/src/bag.ts +128 -0
- package/src/currency.ts +174 -0
- package/src/data/iso4217.ts +13 -0
- package/src/drizzle/config.ts +39 -0
- package/src/drizzle/mysql.ts +61 -0
- package/src/drizzle/pg.ts +64 -0
- package/src/drizzle/sqlite.ts +61 -0
- package/src/errors.ts +61 -0
- package/src/exact.ts +29 -0
- package/src/exchange.ts +280 -0
- package/src/format.ts +148 -0
- package/src/index.ts +16 -0
- package/src/internal/columns.ts +74 -0
- package/src/internal/units.ts +223 -0
- package/src/money.ts +692 -0
- package/src/parse.ts +278 -0
- package/src/provider.ts +67 -0
- package/src/rational.ts +214 -0
- package/src/stripe.ts +84 -0
- package/src/zod.ts +123 -0
package/src/currency.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currencies. The ISO 4217 list ships with the package, so a currency's number
|
|
3
|
+
* of decimals is the same on every runtime and device. `Intl` is only used for
|
|
4
|
+
* display (see `@arki/money/format`); it accepts made-up codes such as `ZZZ`
|
|
5
|
+
* and its decimals can differ from ISO, so it never decides arithmetic.
|
|
6
|
+
*
|
|
7
|
+
* `Currency.of('EUR')` returns the same object every time, so comparing two
|
|
8
|
+
* currencies is a pointer check on the hot path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { InvalidArgumentError } from '@arki/math/errors';
|
|
12
|
+
|
|
13
|
+
import { ISO_4217 } from './data/iso4217.js';
|
|
14
|
+
import { CurrencyMismatchError, UnknownCurrencyError } from './errors.js';
|
|
15
|
+
import { MAX_SCALE } from './internal/units.js';
|
|
16
|
+
|
|
17
|
+
export { ISO_4217_VERSION } from './data/iso4217.js';
|
|
18
|
+
|
|
19
|
+
const BRAND = Symbol('Currency');
|
|
20
|
+
const CUSTOM_CODE = /^[a-z][\w.:-]{0,31}$/i;
|
|
21
|
+
|
|
22
|
+
let byCode: Map<string, Currency> | undefined;
|
|
23
|
+
let byNumeric: Map<string, Currency> | undefined;
|
|
24
|
+
|
|
25
|
+
/** A currency, or the code of an ISO 4217 currency. */
|
|
26
|
+
export type CurrencyLike<C extends string = string> = C | Currency<C>;
|
|
27
|
+
|
|
28
|
+
/** What `Currency.custom` needs. */
|
|
29
|
+
export type CurrencyDefinition<C extends string = string> = {
|
|
30
|
+
/** Letters, digits and `. _ : -`, up to 32 characters, not an ISO 4217 code: `'BTC'`, `'crypto:ETH'`, `'points'`. */
|
|
31
|
+
code: C;
|
|
32
|
+
/** Number of decimals, 0–100: 8 for BTC, 18 for ETH, 0 for loyalty points. */
|
|
33
|
+
scale: number;
|
|
34
|
+
/** Optional three-digit numeric code. */
|
|
35
|
+
numeric?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** An immutable currency: its code, its ISO numeric code and its number of decimals. */
|
|
39
|
+
export class Currency<C extends string = string> {
|
|
40
|
+
/** `'EUR'`. */
|
|
41
|
+
readonly code: C;
|
|
42
|
+
/** Three-digit ISO numeric code as a string, so `'008'` keeps its zeros. `undefined` for most custom currencies. */
|
|
43
|
+
readonly numeric: string | undefined;
|
|
44
|
+
/** Number of decimals: 2 for EUR, 0 for JPY, 3 for KWD. ISO codes without minor units (XAU, XTS) use 0. */
|
|
45
|
+
readonly scale: number;
|
|
46
|
+
/** `true` for a currency from the shipped ISO 4217 table. */
|
|
47
|
+
readonly iso: boolean;
|
|
48
|
+
|
|
49
|
+
private constructor(code: C, numeric: string | undefined, scale: number, iso: boolean, brand: typeof BRAND) {
|
|
50
|
+
if (brand !== BRAND) throw new TypeError('Use Currency.of(code) or Currency.custom(definition)');
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.numeric = numeric;
|
|
53
|
+
this.scale = scale;
|
|
54
|
+
this.iso = iso;
|
|
55
|
+
Object.freeze(this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The ISO 4217 currency with this upper-case code: `Currency.of('EUR')`.
|
|
60
|
+
* Throws `UnknownCurrencyError` for anything not in the table, including `'eur'`.
|
|
61
|
+
*/
|
|
62
|
+
static of<const C extends string>(code: C): Currency<C> {
|
|
63
|
+
const found = Currency.table().get(code);
|
|
64
|
+
if (found === undefined || !hasCode(found, code)) throw new UnknownCurrencyError(String(code));
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The ISO 4217 currency with this numeric code: `Currency.fromNumeric('978')` or `(978)` → EUR. */
|
|
69
|
+
static fromNumeric(numeric: string | number): Currency {
|
|
70
|
+
const key = typeof numeric === 'number' ? String(numeric).padStart(3, '0') : numeric;
|
|
71
|
+
if (byNumeric === undefined) {
|
|
72
|
+
byNumeric = new Map();
|
|
73
|
+
for (const currency of Currency.table().values()) {
|
|
74
|
+
if (currency.numeric !== undefined) byNumeric.set(currency.numeric, currency);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const found = byNumeric.get(key);
|
|
78
|
+
if (found === undefined) throw new UnknownCurrencyError(key);
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A currency that is not in ISO 4217: crypto, loyalty points, a withdrawn
|
|
84
|
+
* currency you still hold data in. It is not registered anywhere; keep the
|
|
85
|
+
* object and pass it where you would pass a code. Two custom currencies with
|
|
86
|
+
* the same code and scale are treated as the same currency.
|
|
87
|
+
*/
|
|
88
|
+
static custom<const C extends string>(definition: CurrencyDefinition<C>): Currency<C> {
|
|
89
|
+
const { code, scale, numeric } = definition;
|
|
90
|
+
if (typeof code !== 'string' || !CUSTOM_CODE.test(code)) {
|
|
91
|
+
throw new InvalidArgumentError(
|
|
92
|
+
`A currency code is 1–32 letters, digits or ". _ : -", starting with a letter; got ${JSON.stringify(code)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (Currency.table().has(code)) {
|
|
96
|
+
throw new InvalidArgumentError(`${code} is an ISO 4217 currency; use Currency.of('${code}')`);
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
99
|
+
throw new InvalidArgumentError(`A currency scale is a whole number from 0 to ${MAX_SCALE}, got ${scale}`);
|
|
100
|
+
}
|
|
101
|
+
if (numeric !== undefined && !/^\d{3}$/.test(numeric)) {
|
|
102
|
+
throw new InvalidArgumentError(`A numeric currency code has three digits, got ${JSON.stringify(numeric)}`);
|
|
103
|
+
}
|
|
104
|
+
return new Currency(code, numeric, scale, false, BRAND);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `true` when `code` is in the shipped ISO 4217 table. */
|
|
108
|
+
static isKnown(code: string): boolean {
|
|
109
|
+
return Currency.table().has(code);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every ISO 4217 code in the shipped table, sorted. */
|
|
113
|
+
static codes(): string[] {
|
|
114
|
+
return [...Currency.table().keys()];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Same code and same scale. */
|
|
118
|
+
eq(other: Currency): boolean {
|
|
119
|
+
return this === other || (this.code === other.code && this.scale === other.scale);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
toString(): C {
|
|
123
|
+
return this.code;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
toJSON(): C {
|
|
127
|
+
return this.code;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[Symbol.for('nodejs.util.inspect.custom')](): string {
|
|
131
|
+
return `Currency(${this.code})`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[Symbol.for('Deno.customInspect')](): string {
|
|
135
|
+
return `Currency(${this.code})`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Decodes the packed table on first use. */
|
|
139
|
+
private static table(): Map<string, Currency> {
|
|
140
|
+
if (byCode !== undefined) return byCode;
|
|
141
|
+
const table = new Map<string, Currency>();
|
|
142
|
+
for (let i = 0; i < ISO_4217.length; i += 7) {
|
|
143
|
+
const code = ISO_4217.slice(i, i + 3);
|
|
144
|
+
const digit = ISO_4217.charAt(i + 6);
|
|
145
|
+
table.set(code, new Currency(code, ISO_4217.slice(i + 3, i + 6), digit === '-' ? 0 : Number(digit), true, BRAND));
|
|
146
|
+
}
|
|
147
|
+
byCode = table;
|
|
148
|
+
return table;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasCode<C extends string>(currency: Currency, code: C): currency is Currency<C> {
|
|
153
|
+
return currency.code === code;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Accepts a `Currency` or an ISO code. */
|
|
157
|
+
export function toCurrency<C extends string>(currency: CurrencyLike<C>): Currency<C> {
|
|
158
|
+
if (currency instanceof Currency) return currency;
|
|
159
|
+
if (typeof currency !== 'string') throw new TypeError('Expected a currency code or a Currency');
|
|
160
|
+
return Currency.of(currency);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Finds a currency by code among `custom`, then in the ISO table. */
|
|
164
|
+
export function resolveCurrency(code: string, custom: readonly Currency[] | undefined): Currency {
|
|
165
|
+
if (custom !== undefined) {
|
|
166
|
+
for (const currency of custom) if (currency.code === code) return currency;
|
|
167
|
+
}
|
|
168
|
+
return Currency.of(code);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Throws `CurrencyMismatchError` unless the two are the same currency. */
|
|
172
|
+
export function assertSameCurrency(a: Currency, b: Currency): void {
|
|
173
|
+
if (a !== b && (a.code !== b.code || a.scale !== b.scale)) throw new CurrencyMismatchError(a.code, b.code);
|
|
174
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Generated by scripts/generate-iso4217.mjs. Do not edit by hand.
|
|
2
|
+
// Source: ISO 4217 list one, published by SIX on 2026-09-17. 178 currencies.
|
|
3
|
+
|
|
4
|
+
/** Publication date of the ISO 4217 list this table was built from. */
|
|
5
|
+
export const ISO_4217_VERSION = '2026-09-17';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every active ISO 4217 currency, 7 characters each: alphabetic code (3),
|
|
9
|
+
* numeric code (3), minor-unit digits (1). '-' marks a code with no minor
|
|
10
|
+
* unit in ISO (gold, the test code, ...); those are treated as 0 digits.
|
|
11
|
+
*/
|
|
12
|
+
export const ISO_4217 =
|
|
13
|
+
'AED7842AFN9712ALL0082AMD0512AOA9732ARS0322AUD0362AWG5332AZN9442BAM9772BBD0522BDT0502BHD0483BIF1080BMD0602BND0962BOB0682BOV9842BRL9862BSD0442BTN0642BWP0722BYN9332BZD0842CAD1242CDF9762CHE9472CHF7562CHW9482CLF9904CLP1520CNY1562COP1702COU9702CRC1882CUP1922CVE1322CZK2032DJF2620DKK2082DOP2142DZD0122EGP8182ERN2322ETB2302EUR9782FJD2422FKP2382GBP8262GEL9812GHS9362GIP2922GMD2702GNF3240GTQ3202GYD3282HKD3442HNL3402HTG3322HUF3482IDR3602ILS3762INR3562IQD3683IRR3642ISK3520JMD3882JOD4003JPY3920KES4042KGS4172KHR1162KMF1740KPW4082KRW4100KWD4143KYD1362KZT3982LAK4182LBP4222LKR1442LRD4302LSL4262LYD4343MAD5042MDL4982MGA9692MKD8072MMK1042MNT4962MOP4462MRU9292MUR4802MVR4622MWK4542MXN4842MXV9792MYR4582MZN9432NAD5162NGN5662NIO5582NOK5782NPR5242NZD5542OMR5123PAB5902PEN6042PGK5982PHP6082PKR5862PLN9852PYG6000QAR6342RON9462RSD9412RUB6432RWF6460SAR6822SBD0902SCR6902SDG9382SEK7522SGD7022SHP6542SLE9252SOS7062SRD9682SSP7282STN9302SVC2222SYP7602SZL7482THB7642TJS9722TMT9342TND7883TOP7762TRY9492TTD7802TWD9012TZS8342UAH9802UGX8000USD8402USN9972UYI9400UYU8582UYW9274UZS8602VED9262VES9282VND7040VUV5480WST8822XAD3962XAF9500XAG961-XAU959-XBA955-XBB956-XBC957-XBD958-XCD9512XCG5322XDR960-XOF9520XPD964-XPF9530XPT962-XSU994-XTS963-XUA965-XXX999-YER8862ZAR7102ZMW9672ZWG9242';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Column options shared by every SQL dialect. */
|
|
2
|
+
|
|
3
|
+
import type { Currency, CurrencyLike } from '../currency.js';
|
|
4
|
+
import { toCurrency } from '../currency.js';
|
|
5
|
+
import { InvalidArgumentError } from '../errors.js';
|
|
6
|
+
import { MAX_SCALE } from '../internal/units.js';
|
|
7
|
+
|
|
8
|
+
export type MoneyMinorConfig<C extends string> = {
|
|
9
|
+
currency: CurrencyLike<C>;
|
|
10
|
+
/** Decimals the stored integer carries. Defaults to the currency's. */
|
|
11
|
+
scale?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MoneyDecimalConfig<C extends string> = {
|
|
15
|
+
currency: CurrencyLike<C>;
|
|
16
|
+
/** Total digits, as in `numeric(precision, scale)`. Default 19. Ignored by SQLite. */
|
|
17
|
+
precision?: number;
|
|
18
|
+
/** Decimals stored. Defaults to the currency's. */
|
|
19
|
+
scale?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MoneyJsonConfig = {
|
|
23
|
+
/** Custom currencies to resolve by code on read. */
|
|
24
|
+
currencies?: readonly Currency[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Validates a column config once, when the table is defined. */
|
|
28
|
+
export function resolveColumn<C extends string>(
|
|
29
|
+
config: { currency: CurrencyLike<C>; scale?: number } | undefined,
|
|
30
|
+
column: string,
|
|
31
|
+
): { currency: Currency<C>; scale: number } {
|
|
32
|
+
if (config === undefined) throw new InvalidArgumentError(`${column} needs a config with a currency`);
|
|
33
|
+
const currency = toCurrency(config.currency);
|
|
34
|
+
const scale = config.scale ?? currency.scale;
|
|
35
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
36
|
+
throw new InvalidArgumentError(`${column}: scale must be a whole number from 0 to ${MAX_SCALE}, got ${scale}`);
|
|
37
|
+
}
|
|
38
|
+
return { currency, scale };
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle columns for MySQL and MariaDB that read and write `Money`.
|
|
3
|
+
* Optional: needs `drizzle-orm` 1.x installed.
|
|
4
|
+
*
|
|
5
|
+
* - `moneyMinor(name, { currency })` — a `bigint` column of minor units ↔ `Money<C>`.
|
|
6
|
+
* - `moneyDecimal(name, { currency, precision, scale })` — a `decimal` column ↔ `Money<C>`.
|
|
7
|
+
* - `moneyJson(name)` — a `json` column of `{ amount, currency }` ↔ `Money` in any currency.
|
|
8
|
+
*
|
|
9
|
+
* `mysql2` returns `DECIMAL` as text and `JSON` as parsed objects by default,
|
|
10
|
+
* and the `bigint` column declares a text codec, so no value passes through a
|
|
11
|
+
* float. If a driver still returns a big integer as an unsafe `number`, reading
|
|
12
|
+
* it throws instead of returning a wrong amount.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { customType } from 'drizzle-orm/mysql-core';
|
|
16
|
+
|
|
17
|
+
import type { IntegerDriverValue } from '../internal/columns.js';
|
|
18
|
+
import type { Money } from '../money.js';
|
|
19
|
+
import type { MoneyDecimalConfig, MoneyJsonConfig, MoneyMinorConfig } from './config.js';
|
|
20
|
+
import {
|
|
21
|
+
decimalFromDriver,
|
|
22
|
+
decimalToDriver,
|
|
23
|
+
jsonFromDriver,
|
|
24
|
+
jsonToDriver,
|
|
25
|
+
minorFromDriver,
|
|
26
|
+
minorToDriver,
|
|
27
|
+
} from '../internal/columns.js';
|
|
28
|
+
import { resolveColumn } from './config.js';
|
|
29
|
+
|
|
30
|
+
export type { MoneyDecimalConfig, MoneyJsonConfig, MoneyMinorConfig } from './config.js';
|
|
31
|
+
|
|
32
|
+
/** `bigint` column ↔ `Money<C>` in one fixed currency. */
|
|
33
|
+
export function moneyMinor<const C extends string>(name: string, config: MoneyMinorConfig<C>) {
|
|
34
|
+
const { currency, scale } = resolveColumn(config, 'moneyMinor');
|
|
35
|
+
return customType<{ data: Money<C>; driverData: IntegerDriverValue; config: MoneyMinorConfig<C> }>({
|
|
36
|
+
dataType: () => 'bigint',
|
|
37
|
+
codec: 'bigint:string',
|
|
38
|
+
toDriver: value => String(minorToDriver(value, currency, scale)),
|
|
39
|
+
fromDriver: value => minorFromDriver(value, currency, scale),
|
|
40
|
+
})(name, config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `decimal(precision, scale)` column ↔ `Money<C>` in one fixed currency. */
|
|
44
|
+
export function moneyDecimal<const C extends string>(name: string, config: MoneyDecimalConfig<C>) {
|
|
45
|
+
const { currency, scale } = resolveColumn(config, 'moneyDecimal');
|
|
46
|
+
const precision = config.precision ?? 19;
|
|
47
|
+
return customType<{ data: Money<C>; driverData: string | number; config: MoneyDecimalConfig<C> }>({
|
|
48
|
+
dataType: () => `decimal(${precision}, ${scale})`,
|
|
49
|
+
toDriver: value => decimalToDriver(value, currency, scale),
|
|
50
|
+
fromDriver: value => decimalFromDriver(value, currency, scale),
|
|
51
|
+
})(name, config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `json` column of `{ amount, currency }` ↔ `Money` in any currency. */
|
|
55
|
+
export function moneyJson(name: string, config: MoneyJsonConfig = {}) {
|
|
56
|
+
return customType<{ data: Money; driverData: unknown; config: MoneyJsonConfig }>({
|
|
57
|
+
dataType: () => 'json',
|
|
58
|
+
toDriver: jsonToDriver,
|
|
59
|
+
fromDriver: value => jsonFromDriver(value, config.currencies),
|
|
60
|
+
})(name, config);
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle columns for Postgres that read and write `Money`. Optional: needs
|
|
3
|
+
* `drizzle-orm` 1.x installed.
|
|
4
|
+
*
|
|
5
|
+
* - `moneyMinor(name, { currency })` — a `bigint` column of minor units for one
|
|
6
|
+
* fixed currency ↔ `Money<C>`. Best for sums in SQL.
|
|
7
|
+
* - `moneyDecimal(name, { currency, precision, scale })` — a `numeric` column ↔
|
|
8
|
+
* `Money<C>`. Readable in SQL tools; holds more decimals than the currency.
|
|
9
|
+
* - `moneyJson(name)` — a `jsonb` column of `{ amount, currency }` ↔ `Money`
|
|
10
|
+
* in any currency. For prices that vary by row.
|
|
11
|
+
*
|
|
12
|
+
* The columns declare text codecs, so every driver (node-postgres,
|
|
13
|
+
* postgres.js, PGlite, Bun SQL) hands back text and no value passes through a
|
|
14
|
+
* float. Writing a `Money` in another currency, or with more decimals than the
|
|
15
|
+
* column holds, throws instead of storing a wrong value.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { customType } from 'drizzle-orm/pg-core';
|
|
19
|
+
|
|
20
|
+
import type { Money } from '../money.js';
|
|
21
|
+
import type { MoneyDecimalConfig, MoneyJsonConfig, MoneyMinorConfig } from './config.js';
|
|
22
|
+
import {
|
|
23
|
+
decimalFromDriver,
|
|
24
|
+
decimalToDriver,
|
|
25
|
+
jsonFromDriver,
|
|
26
|
+
jsonToDriver,
|
|
27
|
+
minorFromDriver,
|
|
28
|
+
minorToDriver,
|
|
29
|
+
} from '../internal/columns.js';
|
|
30
|
+
import { resolveColumn } from './config.js';
|
|
31
|
+
|
|
32
|
+
export type { MoneyDecimalConfig, MoneyJsonConfig, MoneyMinorConfig } from './config.js';
|
|
33
|
+
|
|
34
|
+
/** `bigint` column ↔ `Money<C>` in one fixed currency. */
|
|
35
|
+
export function moneyMinor<const C extends string>(name: string, config: MoneyMinorConfig<C>) {
|
|
36
|
+
const { currency, scale } = resolveColumn(config, 'moneyMinor');
|
|
37
|
+
return customType<{ data: Money<C>; driverData: string; config: MoneyMinorConfig<C> }>({
|
|
38
|
+
dataType: () => 'bigint',
|
|
39
|
+
codec: 'bigint:string',
|
|
40
|
+
toDriver: value => String(minorToDriver(value, currency, scale)),
|
|
41
|
+
fromDriver: value => minorFromDriver(value, currency, scale),
|
|
42
|
+
})(name, config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `numeric(precision, scale)` column ↔ `Money<C>` in one fixed currency. */
|
|
46
|
+
export function moneyDecimal<const C extends string>(name: string, config: MoneyDecimalConfig<C>) {
|
|
47
|
+
const { currency, scale } = resolveColumn(config, 'moneyDecimal');
|
|
48
|
+
const precision = config.precision ?? 19;
|
|
49
|
+
return customType<{ data: Money<C>; driverData: string; config: MoneyDecimalConfig<C> }>({
|
|
50
|
+
dataType: () => `numeric(${precision}, ${scale})`,
|
|
51
|
+
codec: 'numeric',
|
|
52
|
+
toDriver: value => decimalToDriver(value, currency, scale),
|
|
53
|
+
fromDriver: value => decimalFromDriver(value, currency, scale),
|
|
54
|
+
})(name, config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `jsonb` column of `{ amount, currency }` ↔ `Money` in any currency. */
|
|
58
|
+
export function moneyJson(name: string, config: MoneyJsonConfig = {}) {
|
|
59
|
+
return customType<{ data: Money; driverData: string; config: MoneyJsonConfig }>({
|
|
60
|
+
dataType: () => 'jsonb',
|
|
61
|
+
toDriver: jsonToDriver,
|
|
62
|
+
fromDriver: value => jsonFromDriver(value, config.currencies),
|
|
63
|
+
})(name, config);
|
|
64
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle columns for SQLite (libSQL, Turso, better-sqlite3, Bun SQLite,
|
|
3
|
+
* Cloudflare D1, Expo SQLite) that read and write `Money`. Optional: needs
|
|
4
|
+
* `drizzle-orm` 1.x installed.
|
|
5
|
+
*
|
|
6
|
+
* - `moneyMinor(name, { currency })` — an `integer` column of minor units ↔ `Money<C>`.
|
|
7
|
+
* - `moneyDecimal(name, { currency, scale })` — a `text` column of exact decimal
|
|
8
|
+
* text ↔ `Money<C>`. SQLite has no exact decimal type, so text keeps every digit.
|
|
9
|
+
* - `moneyJson(name)` — a `text` column of `{ amount, currency }` JSON ↔ `Money`.
|
|
10
|
+
*
|
|
11
|
+
* SQLite integers are 64-bit, but most drivers return JavaScript numbers.
|
|
12
|
+
* Amounts up to 2^53 minor units are exact. A bigger stored amount throws on
|
|
13
|
+
* read instead of becoming a wrong value; to read those, switch the driver to
|
|
14
|
+
* big integers (libSQL `intMode: 'bigint'`, better-sqlite3 `safeIntegers()`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { customType } from 'drizzle-orm/sqlite-core';
|
|
18
|
+
|
|
19
|
+
import type { IntegerDriverValue } from '../internal/columns.js';
|
|
20
|
+
import type { Money } from '../money.js';
|
|
21
|
+
import type { MoneyDecimalConfig, MoneyJsonConfig, MoneyMinorConfig } from './config.js';
|
|
22
|
+
import {
|
|
23
|
+
decimalFromDriver,
|
|
24
|
+
decimalToDriver,
|
|
25
|
+
jsonFromDriver,
|
|
26
|
+
jsonToDriver,
|
|
27
|
+
minorFromDriver,
|
|
28
|
+
minorToDriver,
|
|
29
|
+
} from '../internal/columns.js';
|
|
30
|
+
import { resolveColumn } from './config.js';
|
|
31
|
+
|
|
32
|
+
export type { MoneyDecimalConfig, MoneyJsonConfig, MoneyMinorConfig } from './config.js';
|
|
33
|
+
|
|
34
|
+
/** `integer` column ↔ `Money<C>` in one fixed currency. Writes a `bigint` past 2^53. */
|
|
35
|
+
export function moneyMinor<const C extends string>(name: string, config: MoneyMinorConfig<C>) {
|
|
36
|
+
const { currency, scale } = resolveColumn(config, 'moneyMinor');
|
|
37
|
+
return customType<{ data: Money<C>; driverData: IntegerDriverValue; config: MoneyMinorConfig<C> }>({
|
|
38
|
+
dataType: () => 'integer',
|
|
39
|
+
toDriver: value => minorToDriver(value, currency, scale),
|
|
40
|
+
fromDriver: value => minorFromDriver(value, currency, scale),
|
|
41
|
+
})(name, config);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `text` column of exact decimal text ↔ `Money<C>` in one fixed currency. */
|
|
45
|
+
export function moneyDecimal<const C extends string>(name: string, config: MoneyDecimalConfig<C>) {
|
|
46
|
+
const { currency, scale } = resolveColumn(config, 'moneyDecimal');
|
|
47
|
+
return customType<{ data: Money<C>; driverData: string; config: MoneyDecimalConfig<C> }>({
|
|
48
|
+
dataType: () => 'text',
|
|
49
|
+
toDriver: value => decimalToDriver(value, currency, scale),
|
|
50
|
+
fromDriver: value => decimalFromDriver(value, currency, scale),
|
|
51
|
+
})(name, config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `text` column of `{ amount, currency }` JSON ↔ `Money` in any currency. */
|
|
55
|
+
export function moneyJson(name: string, config: MoneyJsonConfig = {}) {
|
|
56
|
+
return customType<{ data: Money; driverData: string; config: MoneyJsonConfig }>({
|
|
57
|
+
dataType: () => 'text',
|
|
58
|
+
toDriver: jsonToDriver,
|
|
59
|
+
fromDriver: value => jsonFromDriver(value, config.currencies),
|
|
60
|
+
})(name, config);
|
|
61
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure this package raises on purpose extends `MathError` from
|
|
3
|
+
* `@arki/math`, so one `instanceof MathError` catches both libraries. The
|
|
4
|
+
* money-only failures also extend `MoneyError`. Wrong runtime types (a number
|
|
5
|
+
* where a `Money` was required, or `money + 1`) throw the built-in `TypeError`.
|
|
6
|
+
* Nothing here returns `NaN` or `null` as a hidden failure channel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { MathError } from '@arki/math/errors';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
DivisionByZeroError,
|
|
13
|
+
EmptyDataError,
|
|
14
|
+
InvalidArgumentError,
|
|
15
|
+
MathError,
|
|
16
|
+
NumberFormatError,
|
|
17
|
+
NumericRangeError,
|
|
18
|
+
ResourceLimitError,
|
|
19
|
+
RoundingNecessaryError,
|
|
20
|
+
UnsupportedFeatureError,
|
|
21
|
+
} from '@arki/math/errors';
|
|
22
|
+
|
|
23
|
+
/** Base class of the failures that only money can have. */
|
|
24
|
+
export class MoneyError extends MathError {}
|
|
25
|
+
|
|
26
|
+
/** Two amounts in different currencies were combined or compared. */
|
|
27
|
+
export class CurrencyMismatchError extends MoneyError {
|
|
28
|
+
/** Code of the currency on the left-hand side. */
|
|
29
|
+
readonly left: string;
|
|
30
|
+
/** Code of the currency on the right-hand side. */
|
|
31
|
+
readonly right: string;
|
|
32
|
+
|
|
33
|
+
constructor(left: string, right: string) {
|
|
34
|
+
super(
|
|
35
|
+
left === right
|
|
36
|
+
? `Cannot combine two different definitions of ${left}; they disagree on the number of decimals`
|
|
37
|
+
: `Cannot combine ${left} with ${right}`,
|
|
38
|
+
);
|
|
39
|
+
this.left = left;
|
|
40
|
+
this.right = right;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The code is not in the shipped ISO 4217 table and no custom currency was supplied for it. */
|
|
45
|
+
export class UnknownCurrencyError extends MoneyError {
|
|
46
|
+
readonly code: string;
|
|
47
|
+
|
|
48
|
+
constructor(code: string) {
|
|
49
|
+
super(
|
|
50
|
+
`Unknown currency ${JSON.stringify(code)}. ISO 4217 codes are three upper-case letters; ` +
|
|
51
|
+
'for anything else create one with Currency.custom()',
|
|
52
|
+
);
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** No exchange rate is known for the pair, or a rate is invalid (zero, negative, conflicting). */
|
|
58
|
+
export class ExchangeRateError extends MoneyError {}
|
|
59
|
+
|
|
60
|
+
/** An amount cannot be expressed in a payment provider's units (fraction it does not accept, wrong step, unsupported currency). */
|
|
61
|
+
export class ProviderAmountError extends MoneyError {}
|
package/src/exact.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges to `@arki/math`'s exact types, for multi-step math that should
|
|
3
|
+
* round only once at the end:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const share = toRational(price).times(ratio(1, 3)).plus(ratio(1, 7));
|
|
7
|
+
* Money.of(share, 'EUR', { rounding: 'halfEven' }); // Money.of reads Decimal and Rational directly
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Kept in its own entry point so the core never loads the `Decimal` and
|
|
11
|
+
* `Rational` classes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Decimal } from '@arki/math/decimal';
|
|
15
|
+
import type { Rational } from '@arki/math/rational';
|
|
16
|
+
import { decimal } from '@arki/math/decimal';
|
|
17
|
+
import { ratio } from '@arki/math/rational';
|
|
18
|
+
|
|
19
|
+
import type { Money } from './money.js';
|
|
20
|
+
|
|
21
|
+
/** `19.99 EUR` → `Decimal('19.99')`, keeping the scale. */
|
|
22
|
+
export function toDecimal(money: Money): Decimal {
|
|
23
|
+
return decimal(money.toDecimalString());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `19.99 EUR` → `1999/100`, reduced. */
|
|
27
|
+
export function toRational(money: Money): Rational {
|
|
28
|
+
return ratio(BigInt(money.units), 10n ** BigInt(money.scale));
|
|
29
|
+
}
|