@ducatjs/money 0.1.0 → 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/NOTICE +73 -0
- package/README.md +479 -62
- package/data/cash-rounding.json +123 -2
- package/data/cldr-cash.json +101 -0
- package/data/historic.json +78 -0
- package/data/iso4217-list-three.xml +1185 -0
- package/dist/adyen-fx.d.ts.map +1 -1
- package/dist/adyen-fx.js +22 -4
- package/dist/adyen-fx.js.map +1 -1
- package/dist/boc.d.ts +31 -0
- package/dist/boc.d.ts.map +1 -0
- package/dist/boc.js +114 -0
- package/dist/boc.js.map +1 -0
- package/dist/boe.d.ts +33 -0
- package/dist/boe.d.ts.map +1 -0
- package/dist/boe.js +114 -0
- package/dist/boe.js.map +1 -0
- package/dist/cash.d.ts +30 -1
- package/dist/cash.d.ts.map +1 -1
- package/dist/cash.js +50 -2
- package/dist/cash.js.map +1 -1
- package/dist/convert.d.ts +10 -9
- package/dist/convert.d.ts.map +1 -1
- package/dist/convert.js +13 -3
- package/dist/convert.js.map +1 -1
- package/dist/drizzle/mysql.d.ts +105 -0
- package/dist/drizzle/mysql.d.ts.map +1 -0
- package/dist/drizzle/mysql.js +60 -0
- package/dist/drizzle/mysql.js.map +1 -0
- package/dist/drizzle/pg.d.ts +103 -0
- package/dist/drizzle/pg.d.ts.map +1 -0
- package/dist/drizzle/pg.js +60 -0
- package/dist/drizzle/pg.js.map +1 -0
- package/dist/drizzle/sqlite.d.ts +102 -0
- package/dist/drizzle/sqlite.d.ts.map +1 -0
- package/dist/drizzle/sqlite.js +59 -0
- package/dist/drizzle/sqlite.js.map +1 -0
- package/dist/ecb.d.ts +37 -2
- package/dist/ecb.d.ts.map +1 -1
- package/dist/ecb.js +148 -10
- package/dist/ecb.js.map +1 -1
- package/dist/fed.d.ts +35 -0
- package/dist/fed.d.ts.map +1 -0
- package/dist/fed.js +134 -0
- package/dist/fed.js.map +1 -0
- package/dist/fetch.d.ts +45 -0
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +167 -24
- package/dist/fetch.js.map +1 -1
- package/dist/fn.d.ts +8 -1
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +8 -1
- package/dist/fn.js.map +1 -1
- package/dist/generated/cash.d.ts +2 -0
- package/dist/generated/cash.d.ts.map +1 -1
- package/dist/generated/cash.js +101 -0
- package/dist/generated/cash.js.map +1 -1
- package/dist/generated/historic.d.ts +337 -0
- package/dist/generated/historic.d.ts.map +1 -0
- package/dist/generated/historic.js +66 -0
- package/dist/generated/historic.js.map +1 -0
- package/dist/historic.d.ts +40 -0
- package/dist/historic.d.ts.map +1 -0
- package/dist/historic.js +43 -0
- package/dist/historic.js.map +1 -0
- package/dist/history.d.ts +96 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +213 -0
- package/dist/history.js.map +1 -0
- package/dist/hmrc.d.ts +28 -0
- package/dist/hmrc.d.ts.map +1 -0
- package/dist/hmrc.js +108 -0
- package/dist/hmrc.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kysely.d.ts +120 -0
- package/dist/kysely.d.ts.map +1 -0
- package/dist/kysely.js +309 -0
- package/dist/kysely.js.map +1 -0
- package/dist/money.d.ts +93 -9
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +135 -9
- package/dist/money.js.map +1 -1
- package/dist/parse.d.ts +45 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +290 -3
- package/dist/parse.js.map +1 -1
- package/dist/pg.d.ts +100 -0
- package/dist/pg.d.ts.map +1 -0
- package/dist/pg.js +343 -0
- package/dist/pg.js.map +1 -0
- package/dist/plugins/adyen.d.ts +1 -1
- package/dist/plugins/adyen.d.ts.map +1 -1
- package/dist/plugins/adyen.js.map +1 -1
- package/dist/plugins/all.d.ts +8 -1
- package/dist/plugins/all.d.ts.map +1 -1
- package/dist/plugins/all.js +8 -1
- package/dist/plugins/all.js.map +1 -1
- package/dist/plugins/allocate.d.ts +1 -1
- package/dist/plugins/allocate.d.ts.map +1 -1
- package/dist/plugins/allocate.js.map +1 -1
- package/dist/plugins/braintree.d.ts +1 -1
- package/dist/plugins/braintree.d.ts.map +1 -1
- package/dist/plugins/braintree.js.map +1 -1
- package/dist/plugins/cash.d.ts +1 -1
- package/dist/plugins/cash.d.ts.map +1 -1
- package/dist/plugins/cash.js.map +1 -1
- package/dist/plugins/checkout.d.ts +1 -1
- package/dist/plugins/checkout.d.ts.map +1 -1
- package/dist/plugins/checkout.js.map +1 -1
- package/dist/plugins/convert.d.ts +5 -5
- package/dist/plugins/convert.d.ts.map +1 -1
- package/dist/plugins/convert.js.map +1 -1
- package/dist/plugins/exact.d.ts +1 -1
- package/dist/plugins/exact.d.ts.map +1 -1
- package/dist/plugins/exact.js.map +1 -1
- package/dist/plugins/fixed.d.ts +1 -1
- package/dist/plugins/fixed.d.ts.map +1 -1
- package/dist/plugins/fixed.js.map +1 -1
- package/dist/plugins/format.d.ts +1 -1
- package/dist/plugins/format.d.ts.map +1 -1
- package/dist/plugins/format.js.map +1 -1
- package/dist/plugins/klarna.d.ts +1 -1
- package/dist/plugins/klarna.d.ts.map +1 -1
- package/dist/plugins/klarna.js.map +1 -1
- package/dist/plugins/paypal.d.ts +1 -1
- package/dist/plugins/paypal.d.ts.map +1 -1
- package/dist/plugins/paypal.js +0 -4
- package/dist/plugins/paypal.js.map +1 -1
- package/dist/plugins/prisma.d.ts +1 -1
- package/dist/plugins/prisma.d.ts.map +1 -1
- package/dist/plugins/prisma.js +0 -4
- package/dist/plugins/prisma.js.map +1 -1
- package/dist/plugins/stripe.d.ts +1 -1
- package/dist/plugins/stripe.d.ts.map +1 -1
- package/dist/plugins/stripe.js +0 -4
- package/dist/plugins/stripe.js.map +1 -1
- package/dist/plugins/tax.d.ts +6 -6
- package/dist/plugins/tax.d.ts.map +1 -1
- package/dist/plugins/tax.js.map +1 -1
- package/dist/policy.d.ts +34 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +90 -0
- package/dist/policy.js.map +1 -0
- package/dist/prisma.d.ts +25 -14
- package/dist/prisma.d.ts.map +1 -1
- package/dist/prisma.js +29 -19
- package/dist/prisma.js.map +1 -1
- package/dist/provider.d.ts +21 -7
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +51 -11
- package/dist/provider.js.map +1 -1
- package/dist/rate-text.d.ts +38 -0
- package/dist/rate-text.d.ts.map +1 -0
- package/dist/rate-text.js +143 -0
- package/dist/rate-text.js.map +1 -0
- package/dist/rate.d.ts +61 -5
- package/dist/rate.d.ts.map +1 -1
- package/dist/rate.js +133 -33
- package/dist/rate.js.map +1 -1
- package/dist/rounding.js +1 -1
- package/dist/rounding.js.map +1 -1
- package/dist/schema.d.ts +43 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +77 -0
- package/dist/schema.js.map +1 -0
- package/dist/sql.d.ts +54 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +131 -0
- package/dist/sql.js.map +1 -0
- package/dist/tax.d.ts +13 -9
- package/dist/tax.d.ts.map +1 -1
- package/dist/tax.js +20 -4
- package/dist/tax.js.map +1 -1
- package/dist/testing/arbitraries.d.ts +43 -0
- package/dist/testing/arbitraries.d.ts.map +1 -0
- package/dist/testing/arbitraries.js +98 -0
- package/dist/testing/arbitraries.js.map +1 -0
- package/dist/testing/index.d.ts +67 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +159 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/jest.d.ts +2 -0
- package/dist/testing/jest.d.ts.map +1 -0
- package/dist/testing/jest.js +15 -0
- package/dist/testing/jest.js.map +1 -0
- package/dist/testing/vitest.d.ts +7 -0
- package/dist/testing/vitest.d.ts.map +1 -0
- package/dist/testing/vitest.js +12 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/valibot.d.ts +33 -0
- package/dist/valibot.d.ts.map +1 -0
- package/dist/valibot.js +61 -0
- package/dist/valibot.js.map +1 -0
- package/dist/zod.d.ts +36 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +58 -0
- package/dist/zod.js.map +1 -0
- package/package.json +116 -5
- package/src/adyen-fx.ts +25 -4
- package/src/boc.ts +136 -0
- package/src/boe.ts +131 -0
- package/src/cash.ts +70 -2
- package/src/convert.ts +33 -15
- package/src/drizzle/mysql.ts +78 -0
- package/src/drizzle/pg.ts +78 -0
- package/src/drizzle/sqlite.ts +70 -0
- package/src/ecb.ts +179 -13
- package/src/fed.ts +146 -0
- package/src/fetch.ts +207 -28
- package/src/fn.ts +14 -0
- package/src/generated/cash.ts +103 -0
- package/src/generated/historic.ts +67 -0
- package/src/historic.ts +77 -0
- package/src/history.ts +333 -0
- package/src/hmrc.ts +125 -0
- package/src/index.ts +4 -0
- package/src/kysely.ts +588 -0
- package/src/money.ts +210 -12
- package/src/parse.ts +371 -4
- package/src/pg.ts +410 -0
- package/src/plugins/adyen.ts +3 -2
- package/src/plugins/all.ts +10 -0
- package/src/plugins/allocate.ts +3 -2
- package/src/plugins/braintree.ts +3 -2
- package/src/plugins/cash.ts +3 -2
- package/src/plugins/checkout.ts +3 -2
- package/src/plugins/convert.ts +6 -6
- package/src/plugins/exact.ts +3 -2
- package/src/plugins/fixed.ts +3 -2
- package/src/plugins/format.ts +3 -2
- package/src/plugins/klarna.ts +3 -2
- package/src/plugins/paypal.ts +3 -2
- package/src/plugins/prisma.ts +3 -2
- package/src/plugins/stripe.ts +3 -2
- package/src/plugins/tax.ts +11 -11
- package/src/policy.ts +124 -0
- package/src/prisma.ts +91 -10
- package/src/provider.ts +81 -16
- package/src/rate-text.ts +151 -0
- package/src/rate.ts +205 -36
- package/src/rounding.ts +1 -1
- package/src/schema.ts +119 -0
- package/src/sql.ts +178 -0
- package/src/tax.ts +40 -9
- package/src/testing/arbitraries.ts +134 -0
- package/src/testing/index.ts +247 -0
- package/src/testing/jest.ts +18 -0
- package/src/testing/vitest.ts +18 -0
- package/src/valibot.ts +87 -0
- package/src/zod.ts +78 -0
package/src/boe.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `@ducatjs/money/convert/boe`: an offline parser for the Bank of England's daily spot exchange
|
|
2
|
+
// rates against sterling. Parser only: see parseBankOfEnglandCsv on licensing.
|
|
3
|
+
import { quotedTable } from './history.js';
|
|
4
|
+
import type { RateTable } from './provider.js';
|
|
5
|
+
import { addDayRate, csvRows, isCalendarDay, newestFirst, readRateCell } from './rate-text.js';
|
|
6
|
+
|
|
7
|
+
// The Bank's daily spot series against sterling, by code (currency units per £1).
|
|
8
|
+
const BOE_SERIES: Readonly<Record<string, string>> = {
|
|
9
|
+
XUDLADS: 'AUD',
|
|
10
|
+
XUDLZOS3: 'BGN',
|
|
11
|
+
XUDLCDS: 'CAD',
|
|
12
|
+
XUDLBK89: 'CNY',
|
|
13
|
+
XUDLBK25: 'CZK',
|
|
14
|
+
XUDLDKS: 'DKK',
|
|
15
|
+
XUDLERS: 'EUR',
|
|
16
|
+
XUDLHDS: 'HKD',
|
|
17
|
+
XUDLBK33: 'HUF',
|
|
18
|
+
XUDLBK97: 'INR',
|
|
19
|
+
XUDLBK78: 'ILS',
|
|
20
|
+
XUDLJYS: 'JPY',
|
|
21
|
+
XUDLBK83: 'MYR',
|
|
22
|
+
XUDLNDS: 'NZD',
|
|
23
|
+
XUDLNKS: 'NOK',
|
|
24
|
+
XUDLBK47: 'PLN',
|
|
25
|
+
XUDLZOS4: 'RON',
|
|
26
|
+
XUDLSRS: 'SAR',
|
|
27
|
+
XUDLSGS: 'SGD',
|
|
28
|
+
XUDLZRS: 'ZAR',
|
|
29
|
+
XUDLBK93: 'KRW',
|
|
30
|
+
XUDLSKS: 'SEK',
|
|
31
|
+
XUDLSFS: 'CHF',
|
|
32
|
+
XUDLTWS: 'TWD',
|
|
33
|
+
XUDLBK87: 'THB',
|
|
34
|
+
XUDLBK95: 'TRY',
|
|
35
|
+
XUDLUSS: 'USD',
|
|
36
|
+
};
|
|
37
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
38
|
+
|
|
39
|
+
function notBoe(why: string): never {
|
|
40
|
+
throw new SyntaxError(`Not a Bank of England spot rates CSV file: ${why}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** '22 Dec 2025' as an ISO date, or SyntaxError. */
|
|
44
|
+
function isoDate(text: string): string {
|
|
45
|
+
const match = /^(\d\d) ([A-Z][a-z]{2}) (\d{4})$/.exec(text.trim());
|
|
46
|
+
const month = match === null ? 0 : MONTHS.indexOf(match[2]!) + 1;
|
|
47
|
+
const day = match === null ? '' : `${match[3]}-${String(month).padStart(2, '0')}-${match[1]}`;
|
|
48
|
+
if (month === 0 || !isCalendarDay(day)) {
|
|
49
|
+
notBoe(`bad date ${JSON.stringify(text)}`);
|
|
50
|
+
}
|
|
51
|
+
return day;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Options for parseBankOfEnglandCsv. */
|
|
55
|
+
export interface BankOfEnglandOptions {
|
|
56
|
+
/**
|
|
57
|
+
* The decimals the Bank publishes, rebuilt because its CSV strips trailing zeros ('1.35' for
|
|
58
|
+
* 1.3500). Default 4, as its database shows every spot series.
|
|
59
|
+
*/
|
|
60
|
+
readonly decimals?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parses the Bank of England database's CSV of daily spot rates against sterling (series XUDL…,
|
|
65
|
+
* downloaded with codes as column titles: `DATE,XUDLUSS,XUDLERS,…` and dates like `22 Dec 2025`)
|
|
66
|
+
* into one GBP-based table per day, newest first. Rates are currency units per £1, with `source:
|
|
67
|
+
* 'BoE'` and `type: 'spot'`. The CSV strips trailing zeros, so they are rebuilt to `decimals`
|
|
68
|
+
* (default 4). Holidays have no row; an empty cell (before a series starts, or after it ends, as
|
|
69
|
+
* for BGN) is a missing value. An unknown series code, a bad date, two different values for a day
|
|
70
|
+
* or any other unreadable cell throws SyntaxError. No `availableAt` is set: the Bank says only
|
|
71
|
+
* that daily rates are updated "no more than two working days after the date to which they
|
|
72
|
+
* relate".
|
|
73
|
+
*
|
|
74
|
+
* **Licensing: you must check the licence of every Bank of England series you load.** The database
|
|
75
|
+
* is under the Open Government Licence,
|
|
76
|
+
* but the Bank's legal page says "selected exchange rate data and series are excluded from this
|
|
77
|
+
* licence" and "reproduced by the Bank under licence from third parties", and its spot rates
|
|
78
|
+
* page says LSEG and Bloomberg BFIX may be sources, with LSEG content not to be redistributed
|
|
79
|
+
* without LSEG's consent. No series is named either way, so ducat ships no Bank of England data
|
|
80
|
+
* and no fetcher. Parse only files you have downloaded and are allowed to use, and get the Bank's
|
|
81
|
+
* (or LSEG's) permission before you republish any of it. The Bank also says these "are not
|
|
82
|
+
* official rates". For a licensed UK source, see HMRC's monthly rates
|
|
83
|
+
* (@ducatjs/money/convert/hmrc).
|
|
84
|
+
*/
|
|
85
|
+
export function parseBankOfEnglandCsv(csv: string, options?: BankOfEnglandOptions): RateTable[] {
|
|
86
|
+
const decimals = options?.decimals ?? 4;
|
|
87
|
+
const [header, ...rows] = csvRows(csv.charCodeAt(0) === 0xfeff ? csv.slice(1) : csv);
|
|
88
|
+
if (header === undefined || header[0]?.trim() !== 'DATE' || header.length < 2) {
|
|
89
|
+
notBoe('no DATE column');
|
|
90
|
+
}
|
|
91
|
+
const currencies = header.slice(1).map((code) => {
|
|
92
|
+
const currency = BOE_SERIES[code.trim()];
|
|
93
|
+
if (currency === undefined) {
|
|
94
|
+
notBoe(`unknown series ${code} (use the daily spot series against sterling, XUDL…)`);
|
|
95
|
+
}
|
|
96
|
+
return currency;
|
|
97
|
+
});
|
|
98
|
+
if (new Set(currencies).size !== currencies.length) {
|
|
99
|
+
notBoe('a series appears twice');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A download always has a row per business day, so none at all is a cut-off file, not a range
|
|
103
|
+
// of holidays.
|
|
104
|
+
if (rows.length === 0) {
|
|
105
|
+
notBoe('no observations');
|
|
106
|
+
}
|
|
107
|
+
const days = new Map<string, Record<string, string | null>>();
|
|
108
|
+
for (const cells of rows) {
|
|
109
|
+
if (cells.length !== header.length) {
|
|
110
|
+
notBoe(`a row has ${cells.length} cells, the header ${header.length}`);
|
|
111
|
+
}
|
|
112
|
+
const day = isoDate(cells[0]!);
|
|
113
|
+
currencies.forEach((currency, i) => {
|
|
114
|
+
addDayRate(days, day, currency, readRateCell(cells[i + 1]!, `${currency} on ${day}`));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const tables: RateTable[] = [];
|
|
119
|
+
for (const [day, rates] of days) {
|
|
120
|
+
if (Object.values(rates).some((rate) => rate !== null)) {
|
|
121
|
+
tables.push(
|
|
122
|
+
quotedTable({ base: 'GBP', source: 'BoE', type: 'spot', asOf: day, decimals, rates }),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// A download only has rows for business days, so rows without a single rate aren't data.
|
|
127
|
+
if (tables.length === 0) {
|
|
128
|
+
notBoe('no rates in any row');
|
|
129
|
+
}
|
|
130
|
+
return newestFirst(tables);
|
|
131
|
+
}
|
package/src/cash.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { CASH_RULES, type CashRule } from './generated/cash.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CurrencyMismatchError,
|
|
4
|
+
make,
|
|
5
|
+
scaleUp,
|
|
6
|
+
toScale,
|
|
7
|
+
type Money,
|
|
8
|
+
type RoundingPolicy,
|
|
9
|
+
} from './money.js';
|
|
10
|
+
import { roundingPolicy } from './policy.js';
|
|
3
11
|
import { divideAndRound, divideAndRoundBigint, type RoundingMode } from './rounding.js';
|
|
4
12
|
import { currencyExponent } from './currency.js';
|
|
5
13
|
|
|
@@ -109,7 +117,67 @@ export function cashRound<C extends string>(money: Money<C>, options?: CashRound
|
|
|
109
117
|
return money;
|
|
110
118
|
}
|
|
111
119
|
const inc = make(money.currency, rule.increment, currencyExponent(money.currency));
|
|
112
|
-
return roundToIncrement(money, inc, options?.rounding ?? 'halfExpand');
|
|
120
|
+
return roundToIncrement(money, inc, options?.rounding ?? rule.rounding ?? 'halfExpand');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The cash-rounding rule for a currency as a `RoundingPolicy`, to pass wherever a rounding mode
|
|
125
|
+
* goes: `m.multiply('1.075', cashPolicy('CHF'))` rounds once, straight to CHF 0.05. The policy only
|
|
126
|
+
* accepts amounts in `currency`. Throws a RangeError when no rule applies (EUR needs a country);
|
|
127
|
+
* use `isoPolicy` for currencies without cash rounding.
|
|
128
|
+
*/
|
|
129
|
+
export function cashPolicy(currency: string, options?: CashRoundOptions): RoundingPolicy {
|
|
130
|
+
const rule = cashRule(currency, options?.country);
|
|
131
|
+
if (!rule || (options?.includeUnverified === false && !rule.verified)) {
|
|
132
|
+
const where = options?.country === undefined ? '' : ` in ${options.country}`;
|
|
133
|
+
throw new RangeError(`No cash-rounding rule for ${currency}${where}`);
|
|
134
|
+
}
|
|
135
|
+
return roundingPolicy({
|
|
136
|
+
scale: currencyExponent(currency),
|
|
137
|
+
increment: rule.increment,
|
|
138
|
+
rounding: options?.rounding ?? rule.rounding ?? 'halfExpand',
|
|
139
|
+
currency,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Options for `currencyDigits`. */
|
|
144
|
+
export interface CurrencyDigitsOptions {
|
|
145
|
+
/** 'standard' (the default): the ISO 4217 exponent. 'cash': the decimals of a cash amount. */
|
|
146
|
+
readonly usage?: 'standard' | 'cash';
|
|
147
|
+
/** For cash: the country (ISO 3166 alpha-2), which EUR needs. */
|
|
148
|
+
readonly country?: string;
|
|
149
|
+
/** For cash: whether to use rules backed only by secondary sources (default true). */
|
|
150
|
+
readonly includeUnverified?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The number of decimals a currency's amounts are shown with: its ISO 4217 exponent, or with
|
|
155
|
+
* `{ usage: 'cash' }`, the decimals of a cash amount (what CLDR calls `cashDigits`): none when cash
|
|
156
|
+
* is rounded to whole units, the exponent otherwise.
|
|
157
|
+
*
|
|
158
|
+
* currencyDigits('SEK') // 2
|
|
159
|
+
* currencyDigits('SEK', { usage: 'cash' }) // 0: cash is rounded to whole kronor
|
|
160
|
+
* currencyDigits('CHF', { usage: 'cash' }) // 2: CHF 0.05 still needs two decimals
|
|
161
|
+
*
|
|
162
|
+
* A currency without a cash rule (or EUR without a country that has one) takes its exponent.
|
|
163
|
+
* Throws for unknown codes, like `currencyExponent`.
|
|
164
|
+
*/
|
|
165
|
+
export function currencyDigits(currency: string, options?: CurrencyDigitsOptions): number {
|
|
166
|
+
const exponent = currencyExponent(currency);
|
|
167
|
+
if (options?.usage !== 'cash') {
|
|
168
|
+
return exponent;
|
|
169
|
+
}
|
|
170
|
+
const rule = cashRule(currency, options.country);
|
|
171
|
+
if (!rule || (options.includeUnverified === false && !rule.verified)) {
|
|
172
|
+
return exponent;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Cash amounts show no decimals once the step is whole units (SEK 1, HUF 5, UZS 50), and the
|
|
176
|
+
// currency's usual decimals otherwise: DKK 0.50 is shown as 10.50, not 10.5, as in CLDR.
|
|
177
|
+
if (rule.increment % 10 ** exponent === 0) {
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
return exponent;
|
|
113
181
|
}
|
|
114
182
|
|
|
115
183
|
export { CASH_RULES, type CashRule };
|
package/src/convert.ts
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import { currencyExponent } from './currency.js';
|
|
3
3
|
import { exact, type Rounded } from './exact.js';
|
|
4
4
|
import { ratio, type Factor } from './decimal.js';
|
|
5
|
-
import { make, type CurrencyInput, type Money } from './money.js';
|
|
5
|
+
import { make, type CurrencyInput, type Money, type RoundingInput } from './money.js';
|
|
6
6
|
import { rateOf, type RateProvider } from './provider.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
convertWithPolicy,
|
|
9
|
+
isRate,
|
|
10
|
+
multiplyByRatioAtScale,
|
|
11
|
+
type ConvertOptions,
|
|
12
|
+
type Rate,
|
|
13
|
+
} from './rate.js';
|
|
14
|
+
import { DEFAULT_ROUNDING } from './rounding.js';
|
|
9
15
|
|
|
10
16
|
export {
|
|
11
17
|
exchangeRate,
|
|
@@ -18,6 +24,7 @@ export {
|
|
|
18
24
|
type ConvertOptions,
|
|
19
25
|
type RateInfo,
|
|
20
26
|
type RateJSON,
|
|
27
|
+
type RateQuote,
|
|
21
28
|
type RateType,
|
|
22
29
|
} from './rate.js';
|
|
23
30
|
export {
|
|
@@ -58,7 +65,8 @@ function isProvider(value: unknown): value is RateProvider {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
|
-
* Converts `money` at a Rate, rounding once to the target currency's exponent (or `options.scale`)
|
|
68
|
+
* Converts `money` at a Rate, rounding once to the target currency's exponent (or `options.scale`),
|
|
69
|
+
* or with a rounding policy to its scale and increment (`@ducatjs/money/policy`).
|
|
62
70
|
* The rate's pair is checked, at compile time for literal currencies and at run time otherwise.
|
|
63
71
|
*
|
|
64
72
|
* convert(money('100', 'EUR'), eurUsd, 'halfEven') // 111.65 USD
|
|
@@ -67,7 +75,7 @@ function isProvider(value: unknown): value is RateProvider {
|
|
|
67
75
|
export function convert<From extends string, To extends string>(
|
|
68
76
|
money: Money<From>,
|
|
69
77
|
rate: Rate<NoInfer<From>, To>,
|
|
70
|
-
rounding?:
|
|
78
|
+
rounding?: RoundingInput,
|
|
71
79
|
options?: ConvertOptions,
|
|
72
80
|
): Money<To>;
|
|
73
81
|
/**
|
|
@@ -80,7 +88,7 @@ export function convert<To extends CurrencyInput>(
|
|
|
80
88
|
money: Money,
|
|
81
89
|
to: To,
|
|
82
90
|
rate: Factor,
|
|
83
|
-
rounding?:
|
|
91
|
+
rounding?: RoundingInput,
|
|
84
92
|
options?: ConvertOptions,
|
|
85
93
|
): Money<To>;
|
|
86
94
|
/**
|
|
@@ -93,18 +101,18 @@ export function convert<To extends CurrencyInput>(
|
|
|
93
101
|
money: Money,
|
|
94
102
|
to: To,
|
|
95
103
|
provider: RateProvider,
|
|
96
|
-
rounding?:
|
|
104
|
+
rounding?: RoundingInput,
|
|
97
105
|
options?: ProviderConvertOptions,
|
|
98
106
|
): Money<To>;
|
|
99
107
|
export function convert(
|
|
100
108
|
money: Money,
|
|
101
109
|
rateOrTarget: Rate | string,
|
|
102
|
-
roundingOrRate?:
|
|
103
|
-
optionsOrRounding?: ConvertOptions |
|
|
110
|
+
roundingOrRate?: RoundingInput | Factor | RateProvider,
|
|
111
|
+
optionsOrRounding?: ConvertOptions | RoundingInput,
|
|
104
112
|
maybeOptions?: ProviderConvertOptions,
|
|
105
113
|
): Money {
|
|
106
114
|
if (isRate(rateOrTarget)) {
|
|
107
|
-
const rounding = roundingOrRate as
|
|
115
|
+
const rounding = roundingOrRate as RoundingInput | undefined;
|
|
108
116
|
return rateOrTarget.convert(money, rounding, optionsOrRounding as ConvertOptions | undefined);
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -115,7 +123,7 @@ export function convert(
|
|
|
115
123
|
query = { asOf: maybeOptions.asOf };
|
|
116
124
|
}
|
|
117
125
|
const rate = rateOf(roundingOrRate, money.currency, to, query);
|
|
118
|
-
return rate.convert(money, optionsOrRounding as
|
|
126
|
+
return rate.convert(money, optionsOrRounding as RoundingInput | undefined, maybeOptions);
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
// The bare-rate form converts directly, without building a Rate, to keep it allocation-light.
|
|
@@ -128,7 +136,10 @@ export function convert(
|
|
|
128
136
|
if (rate.num <= 0n) {
|
|
129
137
|
throw new RangeError('Exchange rate must be positive');
|
|
130
138
|
}
|
|
131
|
-
const rounding = (optionsOrRounding as
|
|
139
|
+
const rounding = (optionsOrRounding as RoundingInput | undefined) ?? DEFAULT_ROUNDING;
|
|
140
|
+
if (typeof rounding === 'function') {
|
|
141
|
+
return convertWithPolicy(money, to, rate, rounding, maybeOptions);
|
|
142
|
+
}
|
|
132
143
|
const scale = maybeOptions?.scale ?? currencyExponent(to);
|
|
133
144
|
const minor = multiplyByRatioAtScale(money.minor, rate, scale - money.scale, rounding);
|
|
134
145
|
return make(to, minor, scale);
|
|
@@ -143,7 +154,8 @@ export interface DetailedConversion<From extends string, To extends string> exte
|
|
|
143
154
|
|
|
144
155
|
/**
|
|
145
156
|
* Converts at `rate` like convert(), and also reports the residual: `value + residual` is exactly
|
|
146
|
-
* money × rate, so a ledger can book the rounding difference.
|
|
157
|
+
* money × rate, so a ledger can book the rounding difference. With a rounding policy, the value
|
|
158
|
+
* is on the policy's scale and increment, and the residual is still the exact difference.
|
|
147
159
|
*
|
|
148
160
|
* const { value, residual } = convertDetailed(money('0.07', 'EUR'), eurUsd, 'halfEven');
|
|
149
161
|
* // value 0.08 USD, residual -0.001845 USD (as an exact ExactMoney)
|
|
@@ -151,9 +163,15 @@ export interface DetailedConversion<From extends string, To extends string> exte
|
|
|
151
163
|
export function convertDetailed<From extends string, To extends string>(
|
|
152
164
|
money: Money<From>,
|
|
153
165
|
rate: Rate<NoInfer<From>, To>,
|
|
154
|
-
rounding:
|
|
166
|
+
rounding: RoundingInput = DEFAULT_ROUNDING,
|
|
155
167
|
options?: ConvertOptions,
|
|
156
168
|
): DetailedConversion<From, To> {
|
|
157
|
-
const
|
|
169
|
+
const converted = exact(money).convert(rate);
|
|
170
|
+
if (typeof rounding === 'function') {
|
|
171
|
+
// The policy rounds; the residual is still exactly what it left out.
|
|
172
|
+
const value = rate.convert(money, rounding, options);
|
|
173
|
+
return { value, residual: converted.subtract(exact(value)), rate };
|
|
174
|
+
}
|
|
175
|
+
const rounded = converted.roundWithResidual(rounding, options?.scale);
|
|
158
176
|
return { value: rounded.value, residual: rounded.residual, rate };
|
|
159
177
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/drizzle/mysql`: Drizzle ORM column types for MySQL and MariaDB (mysql2,
|
|
3
|
+
* PlanetScale). Selects return `Money`, inserts and updates accept it, and the currency is part of
|
|
4
|
+
* the column's type.
|
|
5
|
+
*
|
|
6
|
+
* import { mysqlTable } from 'drizzle-orm/mysql-core';
|
|
7
|
+
* import { moneyMinor } from '@ducatjs/money/drizzle/mysql';
|
|
8
|
+
*
|
|
9
|
+
* const orders = mysqlTable('orders', { total: moneyMinor('EUR')('total_minor').notNull() });
|
|
10
|
+
*/
|
|
11
|
+
import { customType } from 'drizzle-orm/mysql-core';
|
|
12
|
+
import type { CurrencyInput, Money, MoneyJSON } from '../money.js';
|
|
13
|
+
import {
|
|
14
|
+
checkDecimalSize,
|
|
15
|
+
decimalFromDriver,
|
|
16
|
+
decimalToDriver,
|
|
17
|
+
DEFAULT_DECIMAL_SIZE,
|
|
18
|
+
jsonFromDriver,
|
|
19
|
+
jsonToDriver,
|
|
20
|
+
minorFromDriver,
|
|
21
|
+
minorToDriver,
|
|
22
|
+
type DecimalColumnSize,
|
|
23
|
+
} from '../sql.js';
|
|
24
|
+
|
|
25
|
+
export { DEFAULT_DECIMAL_SIZE, type DecimalColumnSize } from '../sql.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A BIGINT column of minor units in a fixed currency (`1234` is 12.34 EUR), read back as
|
|
29
|
+
* `Money<C>`. Exact up to the column's limit of ±9.2·10^18 minor units. mysql2 returns BIGINT as a
|
|
30
|
+
* number by default, which is exact only below 2^53; beyond that it throws, so set
|
|
31
|
+
* `supportBigNumbers: true` (with `bigNumberStrings: true`) on the connection for larger amounts.
|
|
32
|
+
* Writing another currency, or a value at a scale other than the currency's, throws.
|
|
33
|
+
*
|
|
34
|
+
* total: moneyMinor('EUR')('total_minor').notNull()
|
|
35
|
+
*/
|
|
36
|
+
export function moneyMinor<const C extends CurrencyInput>(currency: C) {
|
|
37
|
+
return customType<{ data: Money<C>; driverData: string | number | bigint }>({
|
|
38
|
+
dataType: () => 'bigint',
|
|
39
|
+
toDriver: (money) => minorToDriver(money, currency).toString(),
|
|
40
|
+
fromDriver: (value) => minorFromDriver(value, currency),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A DECIMAL(precision, scale) column, DECIMAL(19,4) by default, read back as `Money<C>`. The driver
|
|
46
|
+
* must return DECIMAL as a string (mysql2's default; don't set `decimalNumbers`): a number has been
|
|
47
|
+
* through a float and throws. Writing a value the column could not hold unchanged throws (outside
|
|
48
|
+
* strict mode MySQL would clamp it), and so does a column scale below the currency's exponent.
|
|
49
|
+
*
|
|
50
|
+
* price: moneyDecimal('EUR', { precision: 19, scale: 4 })('price').notNull()
|
|
51
|
+
*/
|
|
52
|
+
export function moneyDecimal<const C extends CurrencyInput>(
|
|
53
|
+
currency: C,
|
|
54
|
+
size: DecimalColumnSize = DEFAULT_DECIMAL_SIZE,
|
|
55
|
+
) {
|
|
56
|
+
checkDecimalSize(currency, size);
|
|
57
|
+
|
|
58
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
59
|
+
dataType: () => `decimal(${size.precision}, ${size.scale})`,
|
|
60
|
+
toDriver: (money) => decimalToDriver(money, currency, size),
|
|
61
|
+
fromDriver: (value) => decimalFromDriver(value, currency),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A JSON column holding `{"amount":"12.34","currency":"EUR"}`, with the currency in each value.
|
|
67
|
+
* Pass a currency to fix it: then a value in another currency throws, on write and on read.
|
|
68
|
+
*
|
|
69
|
+
* refund: moneyJson()('refund')
|
|
70
|
+
*/
|
|
71
|
+
export function moneyJson<const C extends CurrencyInput = CurrencyInput>(currency?: C) {
|
|
72
|
+
return customType<{ data: Money<C>; driverData: string | MoneyJSON }>({
|
|
73
|
+
dataType: () => 'json',
|
|
74
|
+
toDriver: (money) => jsonToDriver(money, currency),
|
|
75
|
+
// mysql2 parses JSON columns itself; other drivers may return the text
|
|
76
|
+
fromDriver: (value) => jsonFromDriver(value, currency),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/drizzle/pg`: Drizzle ORM column types for Postgres (node-postgres, postgres.js,
|
|
3
|
+
* PGlite, Neon). Selects return `Money`, inserts and updates accept it, and the currency is part of
|
|
4
|
+
* the column's type.
|
|
5
|
+
*
|
|
6
|
+
* import { pgTable } from 'drizzle-orm/pg-core';
|
|
7
|
+
* import { moneyMinor } from '@ducatjs/money/drizzle/pg';
|
|
8
|
+
*
|
|
9
|
+
* const orders = pgTable('orders', { total: moneyMinor('EUR')('total_minor').notNull() });
|
|
10
|
+
*/
|
|
11
|
+
import { customType } from 'drizzle-orm/pg-core';
|
|
12
|
+
import type { CurrencyInput, Money, MoneyJSON } from '../money.js';
|
|
13
|
+
import {
|
|
14
|
+
checkDecimalSize,
|
|
15
|
+
decimalFromDriver,
|
|
16
|
+
decimalToDriver,
|
|
17
|
+
DEFAULT_DECIMAL_SIZE,
|
|
18
|
+
jsonFromDriver,
|
|
19
|
+
jsonToDriver,
|
|
20
|
+
minorFromDriver,
|
|
21
|
+
minorToDriver,
|
|
22
|
+
type DecimalColumnSize,
|
|
23
|
+
} from '../sql.js';
|
|
24
|
+
|
|
25
|
+
export { DEFAULT_DECIMAL_SIZE, type DecimalColumnSize } from '../sql.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A BIGINT column of minor units in a fixed currency (`1234` is 12.34 EUR), read back as
|
|
29
|
+
* `Money<C>`. Exact up to the column's limit of ±9.2·10^18 minor units. Writing another currency, or
|
|
30
|
+
* a value at a scale other than the currency's, throws.
|
|
31
|
+
*
|
|
32
|
+
* total: moneyMinor('EUR')('total_minor').notNull()
|
|
33
|
+
*/
|
|
34
|
+
export function moneyMinor<const C extends CurrencyInput>(currency: C) {
|
|
35
|
+
return customType<{ data: Money<C>; driverData: string | number | bigint }>({
|
|
36
|
+
dataType: () => 'bigint',
|
|
37
|
+
// a string, not a bigint: node-postgres cannot bind a bigint parameter
|
|
38
|
+
toDriver: (money) => minorToDriver(money, currency).toString(),
|
|
39
|
+
// node-postgres returns int8 as a string, postgres.js as a string or a bigint
|
|
40
|
+
fromDriver: (value) => minorFromDriver(value, currency),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A NUMERIC(precision, scale) column, NUMERIC(19,4) by default, read back as `Money<C>`. The
|
|
46
|
+
* driver must return NUMERIC as a string (node-postgres and postgres.js do); a number has been
|
|
47
|
+
* through a float and throws, as do `NaN` and `Infinity`. Writing a value the column could not hold
|
|
48
|
+
* unchanged throws, and so does a column scale below the currency's exponent.
|
|
49
|
+
*
|
|
50
|
+
* price: moneyDecimal('EUR', { precision: 19, scale: 4 })('price').notNull()
|
|
51
|
+
*/
|
|
52
|
+
export function moneyDecimal<const C extends CurrencyInput>(
|
|
53
|
+
currency: C,
|
|
54
|
+
size: DecimalColumnSize = DEFAULT_DECIMAL_SIZE,
|
|
55
|
+
) {
|
|
56
|
+
checkDecimalSize(currency, size);
|
|
57
|
+
|
|
58
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
59
|
+
dataType: () => `numeric(${size.precision}, ${size.scale})`,
|
|
60
|
+
toDriver: (money) => decimalToDriver(money, currency, size),
|
|
61
|
+
fromDriver: (value) => decimalFromDriver(value, currency),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A JSONB column holding `{"amount":"12.34","currency":"EUR"}`, with the currency in each value.
|
|
67
|
+
* Pass a currency to fix it: then a value in another currency throws, on write and on read.
|
|
68
|
+
*
|
|
69
|
+
* refund: moneyJson()('refund')
|
|
70
|
+
*/
|
|
71
|
+
export function moneyJson<const C extends CurrencyInput = CurrencyInput>(currency?: C) {
|
|
72
|
+
return customType<{ data: Money<C>; driverData: string | MoneyJSON }>({
|
|
73
|
+
dataType: () => 'jsonb',
|
|
74
|
+
toDriver: (money) => jsonToDriver(money, currency),
|
|
75
|
+
// node-postgres parses JSONB itself; other drivers may return the text
|
|
76
|
+
fromDriver: (value) => jsonFromDriver(value, currency),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/drizzle/sqlite`: Drizzle ORM column types for SQLite (better-sqlite3, libSQL,
|
|
3
|
+
* Bun). Selects return `Money`, inserts and updates accept it, and the currency is part of the
|
|
4
|
+
* column's type.
|
|
5
|
+
*
|
|
6
|
+
* import { sqliteTable } from 'drizzle-orm/sqlite-core';
|
|
7
|
+
* import { moneyMinor } from '@ducatjs/money/drizzle/sqlite';
|
|
8
|
+
*
|
|
9
|
+
* const orders = sqliteTable('orders', { total: moneyMinor('EUR')('total_minor').notNull() });
|
|
10
|
+
*/
|
|
11
|
+
import { customType } from 'drizzle-orm/sqlite-core';
|
|
12
|
+
import { currencyExponent } from '../currency.js';
|
|
13
|
+
import type { CurrencyInput, Money } from '../money.js';
|
|
14
|
+
import {
|
|
15
|
+
decimalFromDriver,
|
|
16
|
+
decimalToDriver,
|
|
17
|
+
jsonFromDriver,
|
|
18
|
+
jsonToDriver,
|
|
19
|
+
minorFromDriver,
|
|
20
|
+
minorToDriver,
|
|
21
|
+
} from '../sql.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An INTEGER column of minor units in a fixed currency (`1234` is 12.34 EUR), read back as
|
|
25
|
+
* `Money<C>`. Exact up to SQLite's limit of ±9.2·10^18 minor units. better-sqlite3 returns integers
|
|
26
|
+
* as numbers by default, which are exact only below 2^53; beyond that reading throws, so call
|
|
27
|
+
* `defaultSafeIntegers(true)` on the database for larger amounts. Writing another currency, or a
|
|
28
|
+
* value at a scale other than the currency's, throws.
|
|
29
|
+
*
|
|
30
|
+
* total: moneyMinor('EUR')('total_minor').notNull()
|
|
31
|
+
*/
|
|
32
|
+
export function moneyMinor<const C extends CurrencyInput>(currency: C) {
|
|
33
|
+
return customType<{ data: Money<C>; driverData: number | bigint }>({
|
|
34
|
+
dataType: () => 'integer',
|
|
35
|
+
toDriver: (money) => minorToDriver(money, currency),
|
|
36
|
+
fromDriver: (value) => minorFromDriver(value, currency),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A TEXT column holding the exact decimal string (`12.34`), read back as `Money<C>`. SQLite's
|
|
42
|
+
* NUMERIC affinity would store some decimals as REAL and lose digits, so they are kept as text.
|
|
43
|
+
* Writing another currency, or a value at a scale other than the currency's, throws.
|
|
44
|
+
*
|
|
45
|
+
* price: moneyDecimal('EUR')('price').notNull()
|
|
46
|
+
*/
|
|
47
|
+
export function moneyDecimal<const C extends CurrencyInput>(currency: C) {
|
|
48
|
+
// fail at the schema definition, not on the first query, for an unknown currency
|
|
49
|
+
currencyExponent(currency);
|
|
50
|
+
|
|
51
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
52
|
+
dataType: () => 'text',
|
|
53
|
+
toDriver: (money) => decimalToDriver(money, currency, undefined),
|
|
54
|
+
fromDriver: (value) => decimalFromDriver(value, currency),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A TEXT column holding `{"amount":"12.34","currency":"EUR"}`, with the currency in each value.
|
|
60
|
+
* Pass a currency to fix it: then a value in another currency throws, on write and on read.
|
|
61
|
+
*
|
|
62
|
+
* refund: moneyJson()('refund')
|
|
63
|
+
*/
|
|
64
|
+
export function moneyJson<const C extends CurrencyInput = CurrencyInput>(currency?: C) {
|
|
65
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
66
|
+
dataType: () => 'text',
|
|
67
|
+
toDriver: (money) => jsonToDriver(money, currency),
|
|
68
|
+
fromDriver: (value) => jsonFromDriver(value, currency),
|
|
69
|
+
});
|
|
70
|
+
}
|