@storepass/money 0.1.5 → 0.2.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/README.md +77 -0
- package/dist/currency.d.ts +83 -1
- package/dist/index.cjs +98 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +98 -30
- package/dist/money.d.ts +23 -4
- package/dist/zod.cjs +119 -0
- package/dist/zod.d.ts +109 -0
- package/dist/zod.mjs +83 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -111,6 +111,83 @@ const json = m.toJSON();
|
|
|
111
111
|
|
|
112
112
|
`amount` is always in minor units.
|
|
113
113
|
|
|
114
|
+
### Zod schemas and codecs
|
|
115
|
+
|
|
116
|
+
Zod support is available from a separate entry point, so importing the core
|
|
117
|
+
money primitives does not require Zod:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { Currency, defineCurrency, Money } from "@storepass/money";
|
|
121
|
+
import {
|
|
122
|
+
createMoneyCodec,
|
|
123
|
+
currencyDefinitionSchema,
|
|
124
|
+
moneyCodec,
|
|
125
|
+
moneyInstanceSchema,
|
|
126
|
+
moneySchema,
|
|
127
|
+
} from "@storepass/money/zod";
|
|
128
|
+
import { decode, encode } from "zod";
|
|
129
|
+
|
|
130
|
+
const serialized = moneySchema.parse({ amount: 1234, currency: "USD" });
|
|
131
|
+
const money = decode(moneyCodec, serialized);
|
|
132
|
+
|
|
133
|
+
moneyInstanceSchema.parse(money); // Money
|
|
134
|
+
encode(moneyCodec, money); // { amount: 1234, currency: "USD" }
|
|
135
|
+
|
|
136
|
+
currencyDefinitionSchema.parse(Currency.USD);
|
|
137
|
+
|
|
138
|
+
const XAU = defineCurrency("XAU", 4);
|
|
139
|
+
const goldCodec = createMoneyCodec({ XAU });
|
|
140
|
+
decode(goldCodec, { amount: 12345, currency: "XAU" });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`moneySchema` (also exported as `serializedMoneySchema`) accepts custom
|
|
144
|
+
currency codes and validates that `amount` is a safe integer in minor units.
|
|
145
|
+
`builtInSerializedMoneySchema` and `moneyCodec` restrict values to the
|
|
146
|
+
currencies included in `Currency`.
|
|
147
|
+
|
|
148
|
+
### Display formatting
|
|
149
|
+
|
|
150
|
+
`Money#format` wraps `Intl.NumberFormat`, honoring the currency's
|
|
151
|
+
`decimalPlaces` and gracefully degrading for non-ISO codes.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
Money.fromNumber(Currency.USD, 1234.5).format("en-US"); // "$1,234.50"
|
|
155
|
+
Money.fromNumber(Currency.EUR, 1234.56).format("de-DE"); // "1.234,56 €"
|
|
156
|
+
Money.fromNumber(Currency.JPY, 1500).format("en-US"); // "¥1,500"
|
|
157
|
+
Money.fromNumber(Currency.BTC, 0.12345678).format("en-US"); // "BTC 0.12345678"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For non-ISO codes, you can supply a `displayCode` so `Intl` still renders a
|
|
161
|
+
proper currency symbol while arithmetic precision is preserved separately:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const USDH = defineCurrency("USDH", 3, { displayCode: "USD" });
|
|
165
|
+
Money.fromNumber(USDH, 12.345).format("en-US"); // "$12.345"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## ISO 4217 compatibility
|
|
169
|
+
|
|
170
|
+
Standard currencies in `Currency` carry `numericCode` and `iso4217: true`.
|
|
171
|
+
Custom assets (e.g., `BTC`, `USDH`) are flagged `iso4217: false`. When
|
|
172
|
+
defining your own, pass the metadata explicitly:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const SLE = defineCurrency("SLE", 2, { numericCode: 925, iso4217: true });
|
|
176
|
+
const XAU = defineCurrency("XAU", 4); // non-monetary, no ISO flags
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`defineCurrency` validates `iso4217` codes against `/^[A-Z]{3}$/` and
|
|
180
|
+
requires a numeric code in `0..999`.
|
|
181
|
+
|
|
182
|
+
### Staying compliant
|
|
183
|
+
|
|
184
|
+
ISO 4217 is amended a few times per year by SIX Interbank Clearing
|
|
185
|
+
(deprecations, redenominations, occasional additions). The hardcoded list
|
|
186
|
+
in [src/currency.ts](./src/currency.ts) tracks a pinned revision (see the
|
|
187
|
+
header comment). To stay current, periodically diff against the
|
|
188
|
+
[official XML list](https://www.six-group.com/en/products-services/financial-information/data-standards.html)
|
|
189
|
+
and update both `decimalPlaces` and `numericCode` for any drift.
|
|
190
|
+
|
|
114
191
|
## Notes and Guarantees
|
|
115
192
|
|
|
116
193
|
- Currency mismatch operations throw (for example, adding USD to EUR).
|
package/dist/currency.d.ts
CHANGED
|
@@ -1,121 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency metadata.
|
|
3
|
+
*
|
|
4
|
+
* For ISO 4217 currencies, `iso4217` is `true` and `numericCode` is the
|
|
5
|
+
* official 3-digit numeric code. `displayCode` is an optional override that
|
|
6
|
+
* `Intl.NumberFormat` should use when the primary `code` is non-standard
|
|
7
|
+
* (e.g., legacy systems calling Bitcoin "BTC" instead of the unofficial "XBT").
|
|
8
|
+
*
|
|
9
|
+
* Standard currencies in this file follow ISO 4217 as of revision 173
|
|
10
|
+
* (published 2024-06). Run `bun run scripts/sync-iso4217.ts` to refresh.
|
|
11
|
+
*/
|
|
1
12
|
export interface CurrencyDefinition {
|
|
13
|
+
/** Alpha code (typically ISO 4217 alpha-3, but free-form for custom assets). */
|
|
2
14
|
code: string;
|
|
15
|
+
/** Minor-unit precision used for internal arithmetic. */
|
|
3
16
|
decimalPlaces: number;
|
|
17
|
+
/** ISO 4217 numeric code (0-999). Omitted for non-standard assets. */
|
|
18
|
+
numericCode?: number;
|
|
19
|
+
/** `true` when `code` is an officially registered ISO 4217 alpha-3 code. */
|
|
20
|
+
iso4217?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Optional alpha-3 used for `Intl.NumberFormat` display. Falls back to `code`.
|
|
23
|
+
* Useful for non-ISO codes that have a registered alias.
|
|
24
|
+
*/
|
|
25
|
+
displayCode?: string;
|
|
4
26
|
}
|
|
5
27
|
export declare const Currency: {
|
|
6
28
|
readonly USD: {
|
|
7
29
|
readonly code: "USD";
|
|
8
30
|
readonly decimalPlaces: 2;
|
|
31
|
+
readonly numericCode: 840;
|
|
32
|
+
readonly iso4217: true;
|
|
9
33
|
};
|
|
10
34
|
readonly USDH: {
|
|
11
35
|
readonly code: "USDH";
|
|
12
36
|
readonly decimalPlaces: 3;
|
|
37
|
+
readonly iso4217: false;
|
|
38
|
+
readonly displayCode: "USD";
|
|
13
39
|
};
|
|
14
40
|
readonly EUR: {
|
|
15
41
|
readonly code: "EUR";
|
|
16
42
|
readonly decimalPlaces: 2;
|
|
43
|
+
readonly numericCode: 978;
|
|
44
|
+
readonly iso4217: true;
|
|
17
45
|
};
|
|
18
46
|
readonly GBP: {
|
|
19
47
|
readonly code: "GBP";
|
|
20
48
|
readonly decimalPlaces: 2;
|
|
49
|
+
readonly numericCode: 826;
|
|
50
|
+
readonly iso4217: true;
|
|
21
51
|
};
|
|
22
52
|
readonly JPY: {
|
|
23
53
|
readonly code: "JPY";
|
|
24
54
|
readonly decimalPlaces: 0;
|
|
55
|
+
readonly numericCode: 392;
|
|
56
|
+
readonly iso4217: true;
|
|
25
57
|
};
|
|
26
58
|
readonly CAD: {
|
|
27
59
|
readonly code: "CAD";
|
|
28
60
|
readonly decimalPlaces: 2;
|
|
61
|
+
readonly numericCode: 124;
|
|
62
|
+
readonly iso4217: true;
|
|
29
63
|
};
|
|
30
64
|
readonly AUD: {
|
|
31
65
|
readonly code: "AUD";
|
|
32
66
|
readonly decimalPlaces: 2;
|
|
67
|
+
readonly numericCode: 36;
|
|
68
|
+
readonly iso4217: true;
|
|
33
69
|
};
|
|
34
70
|
readonly NZD: {
|
|
35
71
|
readonly code: "NZD";
|
|
36
72
|
readonly decimalPlaces: 2;
|
|
73
|
+
readonly numericCode: 554;
|
|
74
|
+
readonly iso4217: true;
|
|
37
75
|
};
|
|
38
76
|
readonly CHF: {
|
|
39
77
|
readonly code: "CHF";
|
|
40
78
|
readonly decimalPlaces: 2;
|
|
79
|
+
readonly numericCode: 756;
|
|
80
|
+
readonly iso4217: true;
|
|
41
81
|
};
|
|
42
82
|
readonly SEK: {
|
|
43
83
|
readonly code: "SEK";
|
|
44
84
|
readonly decimalPlaces: 2;
|
|
85
|
+
readonly numericCode: 752;
|
|
86
|
+
readonly iso4217: true;
|
|
45
87
|
};
|
|
46
88
|
readonly NOK: {
|
|
47
89
|
readonly code: "NOK";
|
|
48
90
|
readonly decimalPlaces: 2;
|
|
91
|
+
readonly numericCode: 578;
|
|
92
|
+
readonly iso4217: true;
|
|
49
93
|
};
|
|
50
94
|
readonly DKK: {
|
|
51
95
|
readonly code: "DKK";
|
|
52
96
|
readonly decimalPlaces: 2;
|
|
97
|
+
readonly numericCode: 208;
|
|
98
|
+
readonly iso4217: true;
|
|
53
99
|
};
|
|
54
100
|
readonly PLN: {
|
|
55
101
|
readonly code: "PLN";
|
|
56
102
|
readonly decimalPlaces: 2;
|
|
103
|
+
readonly numericCode: 985;
|
|
104
|
+
readonly iso4217: true;
|
|
57
105
|
};
|
|
58
106
|
readonly CZK: {
|
|
59
107
|
readonly code: "CZK";
|
|
60
108
|
readonly decimalPlaces: 2;
|
|
109
|
+
readonly numericCode: 203;
|
|
110
|
+
readonly iso4217: true;
|
|
61
111
|
};
|
|
62
112
|
readonly HUF: {
|
|
63
113
|
readonly code: "HUF";
|
|
64
114
|
readonly decimalPlaces: 2;
|
|
115
|
+
readonly numericCode: 348;
|
|
116
|
+
readonly iso4217: true;
|
|
65
117
|
};
|
|
66
118
|
readonly BRL: {
|
|
67
119
|
readonly code: "BRL";
|
|
68
120
|
readonly decimalPlaces: 2;
|
|
121
|
+
readonly numericCode: 986;
|
|
122
|
+
readonly iso4217: true;
|
|
69
123
|
};
|
|
70
124
|
readonly MXN: {
|
|
71
125
|
readonly code: "MXN";
|
|
72
126
|
readonly decimalPlaces: 2;
|
|
127
|
+
readonly numericCode: 484;
|
|
128
|
+
readonly iso4217: true;
|
|
73
129
|
};
|
|
74
130
|
readonly ARS: {
|
|
75
131
|
readonly code: "ARS";
|
|
76
132
|
readonly decimalPlaces: 2;
|
|
133
|
+
readonly numericCode: 32;
|
|
134
|
+
readonly iso4217: true;
|
|
77
135
|
};
|
|
78
136
|
readonly CLP: {
|
|
79
137
|
readonly code: "CLP";
|
|
80
138
|
readonly decimalPlaces: 0;
|
|
139
|
+
readonly numericCode: 152;
|
|
140
|
+
readonly iso4217: true;
|
|
81
141
|
};
|
|
82
142
|
readonly INR: {
|
|
83
143
|
readonly code: "INR";
|
|
84
144
|
readonly decimalPlaces: 2;
|
|
145
|
+
readonly numericCode: 356;
|
|
146
|
+
readonly iso4217: true;
|
|
85
147
|
};
|
|
86
148
|
readonly SGD: {
|
|
87
149
|
readonly code: "SGD";
|
|
88
150
|
readonly decimalPlaces: 2;
|
|
151
|
+
readonly numericCode: 702;
|
|
152
|
+
readonly iso4217: true;
|
|
89
153
|
};
|
|
90
154
|
readonly HKD: {
|
|
91
155
|
readonly code: "HKD";
|
|
92
156
|
readonly decimalPlaces: 2;
|
|
157
|
+
readonly numericCode: 344;
|
|
158
|
+
readonly iso4217: true;
|
|
93
159
|
};
|
|
94
160
|
readonly KRW: {
|
|
95
161
|
readonly code: "KRW";
|
|
96
162
|
readonly decimalPlaces: 0;
|
|
163
|
+
readonly numericCode: 410;
|
|
164
|
+
readonly iso4217: true;
|
|
97
165
|
};
|
|
98
166
|
readonly CNY: {
|
|
99
167
|
readonly code: "CNY";
|
|
100
168
|
readonly decimalPlaces: 2;
|
|
169
|
+
readonly numericCode: 156;
|
|
170
|
+
readonly iso4217: true;
|
|
101
171
|
};
|
|
102
172
|
readonly ZAR: {
|
|
103
173
|
readonly code: "ZAR";
|
|
104
174
|
readonly decimalPlaces: 2;
|
|
175
|
+
readonly numericCode: 710;
|
|
176
|
+
readonly iso4217: true;
|
|
105
177
|
};
|
|
106
178
|
readonly AED: {
|
|
107
179
|
readonly code: "AED";
|
|
108
180
|
readonly decimalPlaces: 2;
|
|
181
|
+
readonly numericCode: 784;
|
|
182
|
+
readonly iso4217: true;
|
|
109
183
|
};
|
|
110
184
|
readonly SAR: {
|
|
111
185
|
readonly code: "SAR";
|
|
112
186
|
readonly decimalPlaces: 2;
|
|
187
|
+
readonly numericCode: 682;
|
|
188
|
+
readonly iso4217: true;
|
|
113
189
|
};
|
|
114
190
|
readonly BTC: {
|
|
115
191
|
readonly code: "BTC";
|
|
116
192
|
readonly decimalPlaces: 8;
|
|
193
|
+
readonly iso4217: false;
|
|
117
194
|
};
|
|
118
195
|
};
|
|
119
196
|
export type CurrencyCode = keyof typeof Currency;
|
|
120
197
|
export type CurrencyType = (typeof Currency)[CurrencyCode];
|
|
121
|
-
export
|
|
198
|
+
export interface DefineCurrencyOptions {
|
|
199
|
+
numericCode?: number;
|
|
200
|
+
iso4217?: boolean;
|
|
201
|
+
displayCode?: string;
|
|
202
|
+
}
|
|
203
|
+
export declare function defineCurrency(code: string, decimalPlaces: number, options?: DefineCurrencyOptions): CurrencyDefinition;
|
package/dist/index.cjs
CHANGED
|
@@ -47,43 +47,67 @@ module.exports = __toCommonJS(exports_src);
|
|
|
47
47
|
|
|
48
48
|
// src/currency.ts
|
|
49
49
|
var Currency = {
|
|
50
|
-
USD: { code: "USD", decimalPlaces: 2 },
|
|
51
|
-
USDH: { code: "USDH", decimalPlaces: 3 },
|
|
52
|
-
EUR: { code: "EUR", decimalPlaces: 2 },
|
|
53
|
-
GBP: { code: "GBP", decimalPlaces: 2 },
|
|
54
|
-
JPY: { code: "JPY", decimalPlaces: 0 },
|
|
55
|
-
CAD: { code: "CAD", decimalPlaces: 2 },
|
|
56
|
-
AUD: { code: "AUD", decimalPlaces: 2 },
|
|
57
|
-
NZD: { code: "NZD", decimalPlaces: 2 },
|
|
58
|
-
CHF: { code: "CHF", decimalPlaces: 2 },
|
|
59
|
-
SEK: { code: "SEK", decimalPlaces: 2 },
|
|
60
|
-
NOK: { code: "NOK", decimalPlaces: 2 },
|
|
61
|
-
DKK: { code: "DKK", decimalPlaces: 2 },
|
|
62
|
-
PLN: { code: "PLN", decimalPlaces: 2 },
|
|
63
|
-
CZK: { code: "CZK", decimalPlaces: 2 },
|
|
64
|
-
HUF: { code: "HUF", decimalPlaces: 2 },
|
|
65
|
-
BRL: { code: "BRL", decimalPlaces: 2 },
|
|
66
|
-
MXN: { code: "MXN", decimalPlaces: 2 },
|
|
67
|
-
ARS: { code: "ARS", decimalPlaces: 2 },
|
|
68
|
-
CLP: { code: "CLP", decimalPlaces: 0 },
|
|
69
|
-
INR: { code: "INR", decimalPlaces: 2 },
|
|
70
|
-
SGD: { code: "SGD", decimalPlaces: 2 },
|
|
71
|
-
HKD: { code: "HKD", decimalPlaces: 2 },
|
|
72
|
-
KRW: { code: "KRW", decimalPlaces: 0 },
|
|
73
|
-
CNY: { code: "CNY", decimalPlaces: 2 },
|
|
74
|
-
ZAR: { code: "ZAR", decimalPlaces: 2 },
|
|
75
|
-
AED: { code: "AED", decimalPlaces: 2 },
|
|
76
|
-
SAR: { code: "SAR", decimalPlaces: 2 },
|
|
77
|
-
BTC: { code: "BTC", decimalPlaces: 8 }
|
|
50
|
+
USD: { code: "USD", decimalPlaces: 2, numericCode: 840, iso4217: true },
|
|
51
|
+
USDH: { code: "USDH", decimalPlaces: 3, iso4217: false, displayCode: "USD" },
|
|
52
|
+
EUR: { code: "EUR", decimalPlaces: 2, numericCode: 978, iso4217: true },
|
|
53
|
+
GBP: { code: "GBP", decimalPlaces: 2, numericCode: 826, iso4217: true },
|
|
54
|
+
JPY: { code: "JPY", decimalPlaces: 0, numericCode: 392, iso4217: true },
|
|
55
|
+
CAD: { code: "CAD", decimalPlaces: 2, numericCode: 124, iso4217: true },
|
|
56
|
+
AUD: { code: "AUD", decimalPlaces: 2, numericCode: 36, iso4217: true },
|
|
57
|
+
NZD: { code: "NZD", decimalPlaces: 2, numericCode: 554, iso4217: true },
|
|
58
|
+
CHF: { code: "CHF", decimalPlaces: 2, numericCode: 756, iso4217: true },
|
|
59
|
+
SEK: { code: "SEK", decimalPlaces: 2, numericCode: 752, iso4217: true },
|
|
60
|
+
NOK: { code: "NOK", decimalPlaces: 2, numericCode: 578, iso4217: true },
|
|
61
|
+
DKK: { code: "DKK", decimalPlaces: 2, numericCode: 208, iso4217: true },
|
|
62
|
+
PLN: { code: "PLN", decimalPlaces: 2, numericCode: 985, iso4217: true },
|
|
63
|
+
CZK: { code: "CZK", decimalPlaces: 2, numericCode: 203, iso4217: true },
|
|
64
|
+
HUF: { code: "HUF", decimalPlaces: 2, numericCode: 348, iso4217: true },
|
|
65
|
+
BRL: { code: "BRL", decimalPlaces: 2, numericCode: 986, iso4217: true },
|
|
66
|
+
MXN: { code: "MXN", decimalPlaces: 2, numericCode: 484, iso4217: true },
|
|
67
|
+
ARS: { code: "ARS", decimalPlaces: 2, numericCode: 32, iso4217: true },
|
|
68
|
+
CLP: { code: "CLP", decimalPlaces: 0, numericCode: 152, iso4217: true },
|
|
69
|
+
INR: { code: "INR", decimalPlaces: 2, numericCode: 356, iso4217: true },
|
|
70
|
+
SGD: { code: "SGD", decimalPlaces: 2, numericCode: 702, iso4217: true },
|
|
71
|
+
HKD: { code: "HKD", decimalPlaces: 2, numericCode: 344, iso4217: true },
|
|
72
|
+
KRW: { code: "KRW", decimalPlaces: 0, numericCode: 410, iso4217: true },
|
|
73
|
+
CNY: { code: "CNY", decimalPlaces: 2, numericCode: 156, iso4217: true },
|
|
74
|
+
ZAR: { code: "ZAR", decimalPlaces: 2, numericCode: 710, iso4217: true },
|
|
75
|
+
AED: { code: "AED", decimalPlaces: 2, numericCode: 784, iso4217: true },
|
|
76
|
+
SAR: { code: "SAR", decimalPlaces: 2, numericCode: 682, iso4217: true },
|
|
77
|
+
BTC: { code: "BTC", decimalPlaces: 8, iso4217: false }
|
|
78
78
|
};
|
|
79
|
-
|
|
79
|
+
var ISO_ALPHA3 = /^[A-Z]{3}$/;
|
|
80
|
+
function defineCurrency(code, decimalPlaces, options = {}) {
|
|
80
81
|
if (!code || typeof code !== "string") {
|
|
81
82
|
throw new Error("Currency code must be a non-empty string");
|
|
82
83
|
}
|
|
83
84
|
if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0) {
|
|
84
85
|
throw new Error("Decimal places must be a non-negative integer");
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
const { numericCode, iso4217, displayCode } = options;
|
|
88
|
+
if (iso4217 === true) {
|
|
89
|
+
if (!ISO_ALPHA3.test(code)) {
|
|
90
|
+
throw new Error(`ISO 4217 currency code must be three uppercase letters: got "${code}"`);
|
|
91
|
+
}
|
|
92
|
+
if (numericCode === undefined || !Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
|
|
93
|
+
throw new Error("ISO 4217 currencies require an integer numericCode in 0..999");
|
|
94
|
+
}
|
|
95
|
+
} else if (numericCode !== undefined) {
|
|
96
|
+
if (!Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
|
|
97
|
+
throw new Error("numericCode must be an integer in 0..999");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (displayCode !== undefined && !ISO_ALPHA3.test(displayCode)) {
|
|
101
|
+
throw new Error(`displayCode must be three uppercase letters: got "${displayCode}"`);
|
|
102
|
+
}
|
|
103
|
+
const def = { code, decimalPlaces };
|
|
104
|
+
if (numericCode !== undefined)
|
|
105
|
+
def.numericCode = numericCode;
|
|
106
|
+
if (iso4217 !== undefined)
|
|
107
|
+
def.iso4217 = iso4217;
|
|
108
|
+
if (displayCode !== undefined)
|
|
109
|
+
def.displayCode = displayCode;
|
|
110
|
+
return Object.freeze(def);
|
|
87
111
|
}
|
|
88
112
|
// src/money.ts
|
|
89
113
|
var INTERNAL_PRECISION = 20;
|
|
@@ -166,6 +190,27 @@ function assertSafeResult(value) {
|
|
|
166
190
|
throw new Error("Result too large: exceeds safe integer range");
|
|
167
191
|
}
|
|
168
192
|
}
|
|
193
|
+
function minorToDecimalString(minor, decimalPlaces) {
|
|
194
|
+
const negative = minor < 0n;
|
|
195
|
+
const absMinor = negative ? -minor : minor;
|
|
196
|
+
if (decimalPlaces === 0) {
|
|
197
|
+
return `${negative ? "-" : ""}${absMinor}`;
|
|
198
|
+
}
|
|
199
|
+
const scale = 10n ** BigInt(decimalPlaces);
|
|
200
|
+
const whole = absMinor / scale;
|
|
201
|
+
const fraction = (absMinor % scale).toString().padStart(decimalPlaces, "0");
|
|
202
|
+
return `${negative ? "-" : ""}${whole}.${fraction}`;
|
|
203
|
+
}
|
|
204
|
+
function resolveFractionDigits(decimalPlaces, options) {
|
|
205
|
+
const { minimumFractionDigits, maximumFractionDigits } = options;
|
|
206
|
+
return {
|
|
207
|
+
minimumFractionDigits: minimumFractionDigits ?? (maximumFractionDigits === undefined ? decimalPlaces : Math.min(decimalPlaces, maximumFractionDigits)),
|
|
208
|
+
maximumFractionDigits: maximumFractionDigits ?? (minimumFractionDigits === undefined ? decimalPlaces : Math.max(decimalPlaces, minimumFractionDigits))
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function formatExactDecimal(format, value) {
|
|
212
|
+
return format.format(value);
|
|
213
|
+
}
|
|
169
214
|
|
|
170
215
|
class Money {
|
|
171
216
|
#minor;
|
|
@@ -430,4 +475,27 @@ class Money {
|
|
|
430
475
|
currency: this.#currency.code
|
|
431
476
|
};
|
|
432
477
|
}
|
|
478
|
+
format(locales, options = {}) {
|
|
479
|
+
const value = minorToDecimalString(this.#minor, this.#currency.decimalPlaces);
|
|
480
|
+
const dp = this.#currency.decimalPlaces;
|
|
481
|
+
const displayCode = this.#currency.displayCode ?? this.#currency.code;
|
|
482
|
+
const canUseCurrencyStyle = this.#currency.iso4217 === true || this.#currency.displayCode !== undefined;
|
|
483
|
+
const fractionDigits = resolveFractionDigits(dp, options);
|
|
484
|
+
if (canUseCurrencyStyle) {
|
|
485
|
+
try {
|
|
486
|
+
return formatExactDecimal(new Intl.NumberFormat(locales, {
|
|
487
|
+
...options,
|
|
488
|
+
...fractionDigits,
|
|
489
|
+
style: "currency",
|
|
490
|
+
currency: displayCode
|
|
491
|
+
}), value);
|
|
492
|
+
} catch {}
|
|
493
|
+
}
|
|
494
|
+
const number = formatExactDecimal(new Intl.NumberFormat(locales, {
|
|
495
|
+
...options,
|
|
496
|
+
...fractionDigits,
|
|
497
|
+
style: "decimal"
|
|
498
|
+
}), value);
|
|
499
|
+
return `${this.#currency.code} ${number}`;
|
|
500
|
+
}
|
|
433
501
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Currency, type CurrencyCode, type CurrencyDefinition, type CurrencyType, defineCurrency, } from "./currency";
|
|
2
|
-
export { type FromNumberOptions, Money, type RoundingMode } from "./money";
|
|
1
|
+
export { Currency, type CurrencyCode, type CurrencyDefinition, type CurrencyType, type DefineCurrencyOptions, defineCurrency, } from "./currency";
|
|
2
|
+
export { type FromNumberOptions, Money, type RoundingMode, type SerializedMoney, } from "./money";
|
package/dist/index.mjs
CHANGED
|
@@ -1,42 +1,66 @@
|
|
|
1
1
|
// src/currency.ts
|
|
2
2
|
var Currency = {
|
|
3
|
-
USD: { code: "USD", decimalPlaces: 2 },
|
|
4
|
-
USDH: { code: "USDH", decimalPlaces: 3 },
|
|
5
|
-
EUR: { code: "EUR", decimalPlaces: 2 },
|
|
6
|
-
GBP: { code: "GBP", decimalPlaces: 2 },
|
|
7
|
-
JPY: { code: "JPY", decimalPlaces: 0 },
|
|
8
|
-
CAD: { code: "CAD", decimalPlaces: 2 },
|
|
9
|
-
AUD: { code: "AUD", decimalPlaces: 2 },
|
|
10
|
-
NZD: { code: "NZD", decimalPlaces: 2 },
|
|
11
|
-
CHF: { code: "CHF", decimalPlaces: 2 },
|
|
12
|
-
SEK: { code: "SEK", decimalPlaces: 2 },
|
|
13
|
-
NOK: { code: "NOK", decimalPlaces: 2 },
|
|
14
|
-
DKK: { code: "DKK", decimalPlaces: 2 },
|
|
15
|
-
PLN: { code: "PLN", decimalPlaces: 2 },
|
|
16
|
-
CZK: { code: "CZK", decimalPlaces: 2 },
|
|
17
|
-
HUF: { code: "HUF", decimalPlaces: 2 },
|
|
18
|
-
BRL: { code: "BRL", decimalPlaces: 2 },
|
|
19
|
-
MXN: { code: "MXN", decimalPlaces: 2 },
|
|
20
|
-
ARS: { code: "ARS", decimalPlaces: 2 },
|
|
21
|
-
CLP: { code: "CLP", decimalPlaces: 0 },
|
|
22
|
-
INR: { code: "INR", decimalPlaces: 2 },
|
|
23
|
-
SGD: { code: "SGD", decimalPlaces: 2 },
|
|
24
|
-
HKD: { code: "HKD", decimalPlaces: 2 },
|
|
25
|
-
KRW: { code: "KRW", decimalPlaces: 0 },
|
|
26
|
-
CNY: { code: "CNY", decimalPlaces: 2 },
|
|
27
|
-
ZAR: { code: "ZAR", decimalPlaces: 2 },
|
|
28
|
-
AED: { code: "AED", decimalPlaces: 2 },
|
|
29
|
-
SAR: { code: "SAR", decimalPlaces: 2 },
|
|
30
|
-
BTC: { code: "BTC", decimalPlaces: 8 }
|
|
3
|
+
USD: { code: "USD", decimalPlaces: 2, numericCode: 840, iso4217: true },
|
|
4
|
+
USDH: { code: "USDH", decimalPlaces: 3, iso4217: false, displayCode: "USD" },
|
|
5
|
+
EUR: { code: "EUR", decimalPlaces: 2, numericCode: 978, iso4217: true },
|
|
6
|
+
GBP: { code: "GBP", decimalPlaces: 2, numericCode: 826, iso4217: true },
|
|
7
|
+
JPY: { code: "JPY", decimalPlaces: 0, numericCode: 392, iso4217: true },
|
|
8
|
+
CAD: { code: "CAD", decimalPlaces: 2, numericCode: 124, iso4217: true },
|
|
9
|
+
AUD: { code: "AUD", decimalPlaces: 2, numericCode: 36, iso4217: true },
|
|
10
|
+
NZD: { code: "NZD", decimalPlaces: 2, numericCode: 554, iso4217: true },
|
|
11
|
+
CHF: { code: "CHF", decimalPlaces: 2, numericCode: 756, iso4217: true },
|
|
12
|
+
SEK: { code: "SEK", decimalPlaces: 2, numericCode: 752, iso4217: true },
|
|
13
|
+
NOK: { code: "NOK", decimalPlaces: 2, numericCode: 578, iso4217: true },
|
|
14
|
+
DKK: { code: "DKK", decimalPlaces: 2, numericCode: 208, iso4217: true },
|
|
15
|
+
PLN: { code: "PLN", decimalPlaces: 2, numericCode: 985, iso4217: true },
|
|
16
|
+
CZK: { code: "CZK", decimalPlaces: 2, numericCode: 203, iso4217: true },
|
|
17
|
+
HUF: { code: "HUF", decimalPlaces: 2, numericCode: 348, iso4217: true },
|
|
18
|
+
BRL: { code: "BRL", decimalPlaces: 2, numericCode: 986, iso4217: true },
|
|
19
|
+
MXN: { code: "MXN", decimalPlaces: 2, numericCode: 484, iso4217: true },
|
|
20
|
+
ARS: { code: "ARS", decimalPlaces: 2, numericCode: 32, iso4217: true },
|
|
21
|
+
CLP: { code: "CLP", decimalPlaces: 0, numericCode: 152, iso4217: true },
|
|
22
|
+
INR: { code: "INR", decimalPlaces: 2, numericCode: 356, iso4217: true },
|
|
23
|
+
SGD: { code: "SGD", decimalPlaces: 2, numericCode: 702, iso4217: true },
|
|
24
|
+
HKD: { code: "HKD", decimalPlaces: 2, numericCode: 344, iso4217: true },
|
|
25
|
+
KRW: { code: "KRW", decimalPlaces: 0, numericCode: 410, iso4217: true },
|
|
26
|
+
CNY: { code: "CNY", decimalPlaces: 2, numericCode: 156, iso4217: true },
|
|
27
|
+
ZAR: { code: "ZAR", decimalPlaces: 2, numericCode: 710, iso4217: true },
|
|
28
|
+
AED: { code: "AED", decimalPlaces: 2, numericCode: 784, iso4217: true },
|
|
29
|
+
SAR: { code: "SAR", decimalPlaces: 2, numericCode: 682, iso4217: true },
|
|
30
|
+
BTC: { code: "BTC", decimalPlaces: 8, iso4217: false }
|
|
31
31
|
};
|
|
32
|
-
|
|
32
|
+
var ISO_ALPHA3 = /^[A-Z]{3}$/;
|
|
33
|
+
function defineCurrency(code, decimalPlaces, options = {}) {
|
|
33
34
|
if (!code || typeof code !== "string") {
|
|
34
35
|
throw new Error("Currency code must be a non-empty string");
|
|
35
36
|
}
|
|
36
37
|
if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0) {
|
|
37
38
|
throw new Error("Decimal places must be a non-negative integer");
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
const { numericCode, iso4217, displayCode } = options;
|
|
41
|
+
if (iso4217 === true) {
|
|
42
|
+
if (!ISO_ALPHA3.test(code)) {
|
|
43
|
+
throw new Error(`ISO 4217 currency code must be three uppercase letters: got "${code}"`);
|
|
44
|
+
}
|
|
45
|
+
if (numericCode === undefined || !Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
|
|
46
|
+
throw new Error("ISO 4217 currencies require an integer numericCode in 0..999");
|
|
47
|
+
}
|
|
48
|
+
} else if (numericCode !== undefined) {
|
|
49
|
+
if (!Number.isInteger(numericCode) || numericCode < 0 || numericCode > 999) {
|
|
50
|
+
throw new Error("numericCode must be an integer in 0..999");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (displayCode !== undefined && !ISO_ALPHA3.test(displayCode)) {
|
|
54
|
+
throw new Error(`displayCode must be three uppercase letters: got "${displayCode}"`);
|
|
55
|
+
}
|
|
56
|
+
const def = { code, decimalPlaces };
|
|
57
|
+
if (numericCode !== undefined)
|
|
58
|
+
def.numericCode = numericCode;
|
|
59
|
+
if (iso4217 !== undefined)
|
|
60
|
+
def.iso4217 = iso4217;
|
|
61
|
+
if (displayCode !== undefined)
|
|
62
|
+
def.displayCode = displayCode;
|
|
63
|
+
return Object.freeze(def);
|
|
40
64
|
}
|
|
41
65
|
// src/money.ts
|
|
42
66
|
var INTERNAL_PRECISION = 20;
|
|
@@ -119,6 +143,27 @@ function assertSafeResult(value) {
|
|
|
119
143
|
throw new Error("Result too large: exceeds safe integer range");
|
|
120
144
|
}
|
|
121
145
|
}
|
|
146
|
+
function minorToDecimalString(minor, decimalPlaces) {
|
|
147
|
+
const negative = minor < 0n;
|
|
148
|
+
const absMinor = negative ? -minor : minor;
|
|
149
|
+
if (decimalPlaces === 0) {
|
|
150
|
+
return `${negative ? "-" : ""}${absMinor}`;
|
|
151
|
+
}
|
|
152
|
+
const scale = 10n ** BigInt(decimalPlaces);
|
|
153
|
+
const whole = absMinor / scale;
|
|
154
|
+
const fraction = (absMinor % scale).toString().padStart(decimalPlaces, "0");
|
|
155
|
+
return `${negative ? "-" : ""}${whole}.${fraction}`;
|
|
156
|
+
}
|
|
157
|
+
function resolveFractionDigits(decimalPlaces, options) {
|
|
158
|
+
const { minimumFractionDigits, maximumFractionDigits } = options;
|
|
159
|
+
return {
|
|
160
|
+
minimumFractionDigits: minimumFractionDigits ?? (maximumFractionDigits === undefined ? decimalPlaces : Math.min(decimalPlaces, maximumFractionDigits)),
|
|
161
|
+
maximumFractionDigits: maximumFractionDigits ?? (minimumFractionDigits === undefined ? decimalPlaces : Math.max(decimalPlaces, minimumFractionDigits))
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function formatExactDecimal(format, value) {
|
|
165
|
+
return format.format(value);
|
|
166
|
+
}
|
|
122
167
|
|
|
123
168
|
class Money {
|
|
124
169
|
#minor;
|
|
@@ -383,6 +428,29 @@ class Money {
|
|
|
383
428
|
currency: this.#currency.code
|
|
384
429
|
};
|
|
385
430
|
}
|
|
431
|
+
format(locales, options = {}) {
|
|
432
|
+
const value = minorToDecimalString(this.#minor, this.#currency.decimalPlaces);
|
|
433
|
+
const dp = this.#currency.decimalPlaces;
|
|
434
|
+
const displayCode = this.#currency.displayCode ?? this.#currency.code;
|
|
435
|
+
const canUseCurrencyStyle = this.#currency.iso4217 === true || this.#currency.displayCode !== undefined;
|
|
436
|
+
const fractionDigits = resolveFractionDigits(dp, options);
|
|
437
|
+
if (canUseCurrencyStyle) {
|
|
438
|
+
try {
|
|
439
|
+
return formatExactDecimal(new Intl.NumberFormat(locales, {
|
|
440
|
+
...options,
|
|
441
|
+
...fractionDigits,
|
|
442
|
+
style: "currency",
|
|
443
|
+
currency: displayCode
|
|
444
|
+
}), value);
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
447
|
+
const number = formatExactDecimal(new Intl.NumberFormat(locales, {
|
|
448
|
+
...options,
|
|
449
|
+
...fractionDigits,
|
|
450
|
+
style: "decimal"
|
|
451
|
+
}), value);
|
|
452
|
+
return `${this.#currency.code} ${number}`;
|
|
453
|
+
}
|
|
386
454
|
}
|
|
387
455
|
export {
|
|
388
456
|
defineCurrency,
|
package/dist/money.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ export interface FromNumberOptions {
|
|
|
4
4
|
rounding?: RoundingMode;
|
|
5
5
|
strict?: boolean;
|
|
6
6
|
}
|
|
7
|
+
/** JSON-safe representation of a Money value. Amount is in minor units. */
|
|
8
|
+
export interface SerializedMoney {
|
|
9
|
+
amount: number;
|
|
10
|
+
currency: string;
|
|
11
|
+
}
|
|
12
|
+
type MoneyFormatOptions = Omit<Intl.NumberFormatOptions, "style" | "currency" | "minimumFractionDigits" | "maximumFractionDigits"> & {
|
|
13
|
+
minimumFractionDigits?: number;
|
|
14
|
+
maximumFractionDigits?: number;
|
|
15
|
+
};
|
|
7
16
|
export declare class Money {
|
|
8
17
|
#private;
|
|
9
18
|
private constructor();
|
|
@@ -60,8 +69,18 @@ export declare class Money {
|
|
|
60
69
|
decrementByPercent(percent: number, options?: {
|
|
61
70
|
rounding?: RoundingMode;
|
|
62
71
|
}): Money;
|
|
63
|
-
toJSON():
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
toJSON(): SerializedMoney;
|
|
73
|
+
/**
|
|
74
|
+
* Format this Money for display using `Intl.NumberFormat`.
|
|
75
|
+
*
|
|
76
|
+
* - For ISO 4217 currencies, uses `style: "currency"` with the standard code
|
|
77
|
+
* (or `displayCode` if provided), respecting the currency's
|
|
78
|
+
* `decimalPlaces` (overrides Intl's locale-default fraction digits).
|
|
79
|
+
* - For non-ISO codes without a `displayCode`, falls back to a decimal
|
|
80
|
+
* format prefixed with the currency code (e.g., `"BTC 0.12345678"`).
|
|
81
|
+
* - If a runtime rejects the currency code (older engines, exotic locales),
|
|
82
|
+
* the same decimal fallback is used.
|
|
83
|
+
*/
|
|
84
|
+
format(locales?: string | string[], options?: MoneyFormatOptions): string;
|
|
67
85
|
}
|
|
86
|
+
export {};
|
package/dist/zod.cjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/zod.ts
|
|
40
|
+
var exports_zod = {};
|
|
41
|
+
__export(exports_zod, {
|
|
42
|
+
serializedMoneySchema: () => serializedMoneySchema,
|
|
43
|
+
moneySchema: () => moneySchema,
|
|
44
|
+
moneyInstanceSchema: () => moneyInstanceSchema,
|
|
45
|
+
moneyCodec: () => moneyCodec,
|
|
46
|
+
currencyDefinitionSchema: () => currencyDefinitionSchema,
|
|
47
|
+
currencyCodeSchema: () => currencyCodeSchema,
|
|
48
|
+
createMoneyCodec: () => createMoneyCodec,
|
|
49
|
+
builtInSerializedMoneySchema: () => builtInSerializedMoneySchema
|
|
50
|
+
});
|
|
51
|
+
module.exports = __toCommonJS(exports_zod);
|
|
52
|
+
var import_money = require("@storepass/money");
|
|
53
|
+
var import_zod = require("zod");
|
|
54
|
+
var builtInCurrencyCodes = Object.keys(import_money.Currency);
|
|
55
|
+
var currencyCodeSchema = import_zod.z.enum(builtInCurrencyCodes);
|
|
56
|
+
var currencyDefinitionSchema = import_zod.z.object({
|
|
57
|
+
code: import_zod.z.string().min(1),
|
|
58
|
+
decimalPlaces: import_zod.z.number().int().nonnegative(),
|
|
59
|
+
numericCode: import_zod.z.number().int().min(0).max(999).optional(),
|
|
60
|
+
iso4217: import_zod.z.boolean().optional(),
|
|
61
|
+
displayCode: import_zod.z.string().regex(/^[A-Z]{3}$/).optional()
|
|
62
|
+
}).superRefine((currency, context) => {
|
|
63
|
+
try {
|
|
64
|
+
import_money.defineCurrency(currency.code, currency.decimalPlaces, {
|
|
65
|
+
numericCode: currency.numericCode,
|
|
66
|
+
iso4217: currency.iso4217,
|
|
67
|
+
displayCode: currency.displayCode
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
context.addIssue({
|
|
71
|
+
code: "custom",
|
|
72
|
+
message: error instanceof Error ? error.message : "Invalid currency definition"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
var serializedMoneySchema = import_zod.z.object({
|
|
77
|
+
amount: import_zod.z.number().int(),
|
|
78
|
+
currency: import_zod.z.string().min(1)
|
|
79
|
+
});
|
|
80
|
+
var moneySchema = serializedMoneySchema;
|
|
81
|
+
var builtInSerializedMoneySchema = serializedMoneySchema.extend({
|
|
82
|
+
currency: currencyCodeSchema
|
|
83
|
+
});
|
|
84
|
+
var moneyInstanceSchema = import_zod.z.custom((value) => value instanceof import_money.Money, { error: "Expected Money instance" });
|
|
85
|
+
function assertCurrencyRegistry(currencies) {
|
|
86
|
+
for (const [code, currency] of Object.entries(currencies)) {
|
|
87
|
+
if (currency.code !== code) {
|
|
88
|
+
throw new Error(`Currency registry key "${code}" does not match definition code "${currency.code}"`);
|
|
89
|
+
}
|
|
90
|
+
currencyDefinitionSchema.parse(currency);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function createMoneyCodec(currencies) {
|
|
94
|
+
assertCurrencyRegistry(currencies);
|
|
95
|
+
const registeredCurrencyCodes = new Set(Object.keys(currencies));
|
|
96
|
+
const registeredCurrencyCodeSchema = import_zod.z.custom((value) => typeof value === "string" && registeredCurrencyCodes.has(value), { error: "Unknown currency code" });
|
|
97
|
+
const registeredSerializedMoneySchema = serializedMoneySchema.extend({
|
|
98
|
+
currency: registeredCurrencyCodeSchema
|
|
99
|
+
});
|
|
100
|
+
return import_zod.z.codec(registeredSerializedMoneySchema, moneyInstanceSchema, {
|
|
101
|
+
decode: ({ amount, currency }) => {
|
|
102
|
+
const registeredCurrency = currencies[currency];
|
|
103
|
+
if (registeredCurrency === undefined) {
|
|
104
|
+
throw new Error(`Unknown currency code: ${currency}`);
|
|
105
|
+
}
|
|
106
|
+
return import_money.Money.fromMinor(registeredCurrency, amount);
|
|
107
|
+
},
|
|
108
|
+
encode: (money) => {
|
|
109
|
+
const serialized = money.toJSON();
|
|
110
|
+
const currency = serialized.currency;
|
|
111
|
+
const registeredCurrency = currencies[currency];
|
|
112
|
+
if (registeredCurrency === undefined || registeredCurrency.decimalPlaces !== money.currency.decimalPlaces) {
|
|
113
|
+
throw new Error(`Money currency ${serialized.currency} is not registered with matching precision`);
|
|
114
|
+
}
|
|
115
|
+
return { amount: serialized.amount, currency };
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
var moneyCodec = createMoneyCodec(import_money.Currency);
|
package/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type CurrencyDefinition, Money } from "@storepass/money";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** Schema for the currency codes included with @storepass/money. */
|
|
4
|
+
export declare const currencyCodeSchema: z.ZodEnum<{
|
|
5
|
+
USD: "USD";
|
|
6
|
+
USDH: "USDH";
|
|
7
|
+
EUR: "EUR";
|
|
8
|
+
GBP: "GBP";
|
|
9
|
+
JPY: "JPY";
|
|
10
|
+
CAD: "CAD";
|
|
11
|
+
AUD: "AUD";
|
|
12
|
+
NZD: "NZD";
|
|
13
|
+
CHF: "CHF";
|
|
14
|
+
SEK: "SEK";
|
|
15
|
+
NOK: "NOK";
|
|
16
|
+
DKK: "DKK";
|
|
17
|
+
PLN: "PLN";
|
|
18
|
+
CZK: "CZK";
|
|
19
|
+
HUF: "HUF";
|
|
20
|
+
BRL: "BRL";
|
|
21
|
+
MXN: "MXN";
|
|
22
|
+
ARS: "ARS";
|
|
23
|
+
CLP: "CLP";
|
|
24
|
+
INR: "INR";
|
|
25
|
+
SGD: "SGD";
|
|
26
|
+
HKD: "HKD";
|
|
27
|
+
KRW: "KRW";
|
|
28
|
+
CNY: "CNY";
|
|
29
|
+
ZAR: "ZAR";
|
|
30
|
+
AED: "AED";
|
|
31
|
+
SAR: "SAR";
|
|
32
|
+
BTC: "BTC";
|
|
33
|
+
}>;
|
|
34
|
+
/** Schema for built-in and custom currency definitions. */
|
|
35
|
+
export declare const currencyDefinitionSchema: z.ZodObject<{
|
|
36
|
+
code: z.ZodString;
|
|
37
|
+
decimalPlaces: z.ZodNumber;
|
|
38
|
+
numericCode: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
iso4217: z.ZodOptional<z.ZodBoolean>;
|
|
40
|
+
displayCode: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
/**
|
|
43
|
+
* Schema for Money's JSON-safe wire representation.
|
|
44
|
+
*
|
|
45
|
+
* `amount` is a safe integer in minor units. Custom currency codes are allowed.
|
|
46
|
+
*/
|
|
47
|
+
export declare const serializedMoneySchema: z.ZodObject<{
|
|
48
|
+
amount: z.ZodNumber;
|
|
49
|
+
currency: z.ZodString;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
/** Short alias for serializedMoneySchema. */
|
|
52
|
+
export declare const moneySchema: z.ZodObject<{
|
|
53
|
+
amount: z.ZodNumber;
|
|
54
|
+
currency: z.ZodString;
|
|
55
|
+
}, z.core.$strip>;
|
|
56
|
+
/** Serialized Money restricted to the package's built-in currencies. */
|
|
57
|
+
export declare const builtInSerializedMoneySchema: z.ZodObject<{
|
|
58
|
+
amount: z.ZodNumber;
|
|
59
|
+
currency: z.ZodEnum<{
|
|
60
|
+
USD: "USD";
|
|
61
|
+
USDH: "USDH";
|
|
62
|
+
EUR: "EUR";
|
|
63
|
+
GBP: "GBP";
|
|
64
|
+
JPY: "JPY";
|
|
65
|
+
CAD: "CAD";
|
|
66
|
+
AUD: "AUD";
|
|
67
|
+
NZD: "NZD";
|
|
68
|
+
CHF: "CHF";
|
|
69
|
+
SEK: "SEK";
|
|
70
|
+
NOK: "NOK";
|
|
71
|
+
DKK: "DKK";
|
|
72
|
+
PLN: "PLN";
|
|
73
|
+
CZK: "CZK";
|
|
74
|
+
HUF: "HUF";
|
|
75
|
+
BRL: "BRL";
|
|
76
|
+
MXN: "MXN";
|
|
77
|
+
ARS: "ARS";
|
|
78
|
+
CLP: "CLP";
|
|
79
|
+
INR: "INR";
|
|
80
|
+
SGD: "SGD";
|
|
81
|
+
HKD: "HKD";
|
|
82
|
+
KRW: "KRW";
|
|
83
|
+
CNY: "CNY";
|
|
84
|
+
ZAR: "ZAR";
|
|
85
|
+
AED: "AED";
|
|
86
|
+
SAR: "SAR";
|
|
87
|
+
BTC: "BTC";
|
|
88
|
+
}>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
/** Schema for an already-hydrated Money instance. */
|
|
91
|
+
export declare const moneyInstanceSchema: z.ZodCustom<Money, Money>;
|
|
92
|
+
export type BuiltInSerializedMoney = z.infer<typeof builtInSerializedMoneySchema>;
|
|
93
|
+
export type CurrencyRegistry = Readonly<Record<string, CurrencyDefinition>>;
|
|
94
|
+
/**
|
|
95
|
+
* Creates a bidirectional Zod codec for a currency registry.
|
|
96
|
+
*
|
|
97
|
+
* Decoding turns serialized minor units into Money. Encoding calls toJSON and
|
|
98
|
+
* rejects Money values whose currency is not represented by the registry with
|
|
99
|
+
* the same precision.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createMoneyCodec<const TCurrencyRegistry extends CurrencyRegistry>(currencies: TCurrencyRegistry): z.ZodCodec<z.ZodObject<{
|
|
102
|
+
amount: z.ZodNumber;
|
|
103
|
+
currency: z.ZodCustom<Extract<keyof TCurrencyRegistry, string>, Extract<keyof TCurrencyRegistry, string>>;
|
|
104
|
+
}, z.core.$strip>, z.ZodCustom<Money, Money>>;
|
|
105
|
+
/** Codec for the currencies included with @storepass/money. */
|
|
106
|
+
export declare const moneyCodec: z.ZodCodec<z.ZodObject<{
|
|
107
|
+
amount: z.ZodNumber;
|
|
108
|
+
currency: z.ZodCustom<"USD" | "USDH" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD" | "NZD" | "CHF" | "SEK" | "NOK" | "DKK" | "PLN" | "CZK" | "HUF" | "BRL" | "MXN" | "ARS" | "CLP" | "INR" | "SGD" | "HKD" | "KRW" | "CNY" | "ZAR" | "AED" | "SAR" | "BTC", "USD" | "USDH" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD" | "NZD" | "CHF" | "SEK" | "NOK" | "DKK" | "PLN" | "CZK" | "HUF" | "BRL" | "MXN" | "ARS" | "CLP" | "INR" | "SGD" | "HKD" | "KRW" | "CNY" | "ZAR" | "AED" | "SAR" | "BTC">;
|
|
109
|
+
}, z.core.$strip>, z.ZodCustom<Money, Money>>;
|
package/dist/zod.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/zod.ts
|
|
2
|
+
import {
|
|
3
|
+
Currency,
|
|
4
|
+
defineCurrency,
|
|
5
|
+
Money
|
|
6
|
+
} from "@storepass/money";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
var builtInCurrencyCodes = Object.keys(Currency);
|
|
9
|
+
var currencyCodeSchema = z.enum(builtInCurrencyCodes);
|
|
10
|
+
var currencyDefinitionSchema = z.object({
|
|
11
|
+
code: z.string().min(1),
|
|
12
|
+
decimalPlaces: z.number().int().nonnegative(),
|
|
13
|
+
numericCode: z.number().int().min(0).max(999).optional(),
|
|
14
|
+
iso4217: z.boolean().optional(),
|
|
15
|
+
displayCode: z.string().regex(/^[A-Z]{3}$/).optional()
|
|
16
|
+
}).superRefine((currency, context) => {
|
|
17
|
+
try {
|
|
18
|
+
defineCurrency(currency.code, currency.decimalPlaces, {
|
|
19
|
+
numericCode: currency.numericCode,
|
|
20
|
+
iso4217: currency.iso4217,
|
|
21
|
+
displayCode: currency.displayCode
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
context.addIssue({
|
|
25
|
+
code: "custom",
|
|
26
|
+
message: error instanceof Error ? error.message : "Invalid currency definition"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
var serializedMoneySchema = z.object({
|
|
31
|
+
amount: z.number().int(),
|
|
32
|
+
currency: z.string().min(1)
|
|
33
|
+
});
|
|
34
|
+
var moneySchema = serializedMoneySchema;
|
|
35
|
+
var builtInSerializedMoneySchema = serializedMoneySchema.extend({
|
|
36
|
+
currency: currencyCodeSchema
|
|
37
|
+
});
|
|
38
|
+
var moneyInstanceSchema = z.custom((value) => value instanceof Money, { error: "Expected Money instance" });
|
|
39
|
+
function assertCurrencyRegistry(currencies) {
|
|
40
|
+
for (const [code, currency] of Object.entries(currencies)) {
|
|
41
|
+
if (currency.code !== code) {
|
|
42
|
+
throw new Error(`Currency registry key "${code}" does not match definition code "${currency.code}"`);
|
|
43
|
+
}
|
|
44
|
+
currencyDefinitionSchema.parse(currency);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function createMoneyCodec(currencies) {
|
|
48
|
+
assertCurrencyRegistry(currencies);
|
|
49
|
+
const registeredCurrencyCodes = new Set(Object.keys(currencies));
|
|
50
|
+
const registeredCurrencyCodeSchema = z.custom((value) => typeof value === "string" && registeredCurrencyCodes.has(value), { error: "Unknown currency code" });
|
|
51
|
+
const registeredSerializedMoneySchema = serializedMoneySchema.extend({
|
|
52
|
+
currency: registeredCurrencyCodeSchema
|
|
53
|
+
});
|
|
54
|
+
return z.codec(registeredSerializedMoneySchema, moneyInstanceSchema, {
|
|
55
|
+
decode: ({ amount, currency }) => {
|
|
56
|
+
const registeredCurrency = currencies[currency];
|
|
57
|
+
if (registeredCurrency === undefined) {
|
|
58
|
+
throw new Error(`Unknown currency code: ${currency}`);
|
|
59
|
+
}
|
|
60
|
+
return Money.fromMinor(registeredCurrency, amount);
|
|
61
|
+
},
|
|
62
|
+
encode: (money) => {
|
|
63
|
+
const serialized = money.toJSON();
|
|
64
|
+
const currency = serialized.currency;
|
|
65
|
+
const registeredCurrency = currencies[currency];
|
|
66
|
+
if (registeredCurrency === undefined || registeredCurrency.decimalPlaces !== money.currency.decimalPlaces) {
|
|
67
|
+
throw new Error(`Money currency ${serialized.currency} is not registered with matching precision`);
|
|
68
|
+
}
|
|
69
|
+
return { amount: serialized.amount, currency };
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
var moneyCodec = createMoneyCodec(Currency);
|
|
74
|
+
export {
|
|
75
|
+
serializedMoneySchema,
|
|
76
|
+
moneySchema,
|
|
77
|
+
moneyInstanceSchema,
|
|
78
|
+
moneyCodec,
|
|
79
|
+
currencyDefinitionSchema,
|
|
80
|
+
currencyCodeSchema,
|
|
81
|
+
createMoneyCodec,
|
|
82
|
+
builtInSerializedMoneySchema
|
|
83
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storepass/money",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Money primitive library",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"default": "./dist/index.cjs"
|
|
17
17
|
}
|
|
18
|
+
},
|
|
19
|
+
"./zod": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/zod.d.ts",
|
|
22
|
+
"default": "./dist/zod.mjs"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/zod.d.ts",
|
|
26
|
+
"default": "./dist/zod.cjs"
|
|
27
|
+
}
|
|
18
28
|
}
|
|
19
29
|
},
|
|
20
30
|
"files": [
|
|
@@ -22,8 +32,8 @@
|
|
|
22
32
|
],
|
|
23
33
|
"scripts": {
|
|
24
34
|
"build": "bun run build:esm && bun run build:cjs && bun run build:types",
|
|
25
|
-
"build:esm": "bun build ./src/index.ts --outfile ./dist/index.mjs --format esm --target node",
|
|
26
|
-
"build:cjs": "bun build ./src/index.ts --outfile ./dist/index.cjs --format cjs --target node",
|
|
35
|
+
"build:esm": "bun build ./src/index.ts --outfile ./dist/index.mjs --format esm --target node && bun build ./src/zod.ts --outfile ./dist/zod.mjs --format esm --target node --external '@storepass/money' --external zod",
|
|
36
|
+
"build:cjs": "bun build ./src/index.ts --outfile ./dist/index.cjs --format cjs --target node && bun build ./src/zod.ts --outfile ./dist/zod.cjs --format cjs --target node --external '@storepass/money' --external zod",
|
|
27
37
|
"build:types": "tsc -p tsconfig.build.json",
|
|
28
38
|
"test": "bun test",
|
|
29
39
|
"typecheck": "tsc --noEmit",
|
|
@@ -54,9 +64,18 @@
|
|
|
54
64
|
"registry": "https://registry.npmjs.org",
|
|
55
65
|
"access": "public"
|
|
56
66
|
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"zod": "^4.1.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"zod": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
57
75
|
"devDependencies": {
|
|
58
76
|
"@biomejs/biome": "2.3.11",
|
|
59
77
|
"@types/bun": "latest",
|
|
60
|
-
"typescript": "^5.9.3"
|
|
78
|
+
"typescript": "^5.9.3",
|
|
79
|
+
"zod": "^4.4.3"
|
|
61
80
|
}
|
|
62
81
|
}
|