@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.
Files changed (253) hide show
  1. package/NOTICE +73 -0
  2. package/README.md +479 -62
  3. package/data/cash-rounding.json +123 -2
  4. package/data/cldr-cash.json +101 -0
  5. package/data/historic.json +78 -0
  6. package/data/iso4217-list-three.xml +1185 -0
  7. package/dist/adyen-fx.d.ts.map +1 -1
  8. package/dist/adyen-fx.js +22 -4
  9. package/dist/adyen-fx.js.map +1 -1
  10. package/dist/boc.d.ts +31 -0
  11. package/dist/boc.d.ts.map +1 -0
  12. package/dist/boc.js +114 -0
  13. package/dist/boc.js.map +1 -0
  14. package/dist/boe.d.ts +33 -0
  15. package/dist/boe.d.ts.map +1 -0
  16. package/dist/boe.js +114 -0
  17. package/dist/boe.js.map +1 -0
  18. package/dist/cash.d.ts +30 -1
  19. package/dist/cash.d.ts.map +1 -1
  20. package/dist/cash.js +50 -2
  21. package/dist/cash.js.map +1 -1
  22. package/dist/convert.d.ts +10 -9
  23. package/dist/convert.d.ts.map +1 -1
  24. package/dist/convert.js +13 -3
  25. package/dist/convert.js.map +1 -1
  26. package/dist/drizzle/mysql.d.ts +105 -0
  27. package/dist/drizzle/mysql.d.ts.map +1 -0
  28. package/dist/drizzle/mysql.js +60 -0
  29. package/dist/drizzle/mysql.js.map +1 -0
  30. package/dist/drizzle/pg.d.ts +103 -0
  31. package/dist/drizzle/pg.d.ts.map +1 -0
  32. package/dist/drizzle/pg.js +60 -0
  33. package/dist/drizzle/pg.js.map +1 -0
  34. package/dist/drizzle/sqlite.d.ts +102 -0
  35. package/dist/drizzle/sqlite.d.ts.map +1 -0
  36. package/dist/drizzle/sqlite.js +59 -0
  37. package/dist/drizzle/sqlite.js.map +1 -0
  38. package/dist/ecb.d.ts +37 -2
  39. package/dist/ecb.d.ts.map +1 -1
  40. package/dist/ecb.js +148 -10
  41. package/dist/ecb.js.map +1 -1
  42. package/dist/fed.d.ts +35 -0
  43. package/dist/fed.d.ts.map +1 -0
  44. package/dist/fed.js +134 -0
  45. package/dist/fed.js.map +1 -0
  46. package/dist/fetch.d.ts +45 -0
  47. package/dist/fetch.d.ts.map +1 -1
  48. package/dist/fetch.js +167 -24
  49. package/dist/fetch.js.map +1 -1
  50. package/dist/fn.d.ts +8 -1
  51. package/dist/fn.d.ts.map +1 -1
  52. package/dist/fn.js +8 -1
  53. package/dist/fn.js.map +1 -1
  54. package/dist/generated/cash.d.ts +2 -0
  55. package/dist/generated/cash.d.ts.map +1 -1
  56. package/dist/generated/cash.js +101 -0
  57. package/dist/generated/cash.js.map +1 -1
  58. package/dist/generated/historic.d.ts +337 -0
  59. package/dist/generated/historic.d.ts.map +1 -0
  60. package/dist/generated/historic.js +66 -0
  61. package/dist/generated/historic.js.map +1 -0
  62. package/dist/historic.d.ts +40 -0
  63. package/dist/historic.d.ts.map +1 -0
  64. package/dist/historic.js +43 -0
  65. package/dist/historic.js.map +1 -0
  66. package/dist/history.d.ts +96 -0
  67. package/dist/history.d.ts.map +1 -0
  68. package/dist/history.js +213 -0
  69. package/dist/history.js.map +1 -0
  70. package/dist/hmrc.d.ts +28 -0
  71. package/dist/hmrc.d.ts.map +1 -0
  72. package/dist/hmrc.js +108 -0
  73. package/dist/hmrc.js.map +1 -0
  74. package/dist/index.d.ts +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/kysely.d.ts +120 -0
  79. package/dist/kysely.d.ts.map +1 -0
  80. package/dist/kysely.js +309 -0
  81. package/dist/kysely.js.map +1 -0
  82. package/dist/money.d.ts +93 -9
  83. package/dist/money.d.ts.map +1 -1
  84. package/dist/money.js +135 -9
  85. package/dist/money.js.map +1 -1
  86. package/dist/parse.d.ts +45 -1
  87. package/dist/parse.d.ts.map +1 -1
  88. package/dist/parse.js +290 -3
  89. package/dist/parse.js.map +1 -1
  90. package/dist/pg.d.ts +100 -0
  91. package/dist/pg.d.ts.map +1 -0
  92. package/dist/pg.js +343 -0
  93. package/dist/pg.js.map +1 -0
  94. package/dist/plugins/adyen.d.ts +1 -1
  95. package/dist/plugins/adyen.d.ts.map +1 -1
  96. package/dist/plugins/adyen.js.map +1 -1
  97. package/dist/plugins/all.d.ts +8 -1
  98. package/dist/plugins/all.d.ts.map +1 -1
  99. package/dist/plugins/all.js +8 -1
  100. package/dist/plugins/all.js.map +1 -1
  101. package/dist/plugins/allocate.d.ts +1 -1
  102. package/dist/plugins/allocate.d.ts.map +1 -1
  103. package/dist/plugins/allocate.js.map +1 -1
  104. package/dist/plugins/braintree.d.ts +1 -1
  105. package/dist/plugins/braintree.d.ts.map +1 -1
  106. package/dist/plugins/braintree.js.map +1 -1
  107. package/dist/plugins/cash.d.ts +1 -1
  108. package/dist/plugins/cash.d.ts.map +1 -1
  109. package/dist/plugins/cash.js.map +1 -1
  110. package/dist/plugins/checkout.d.ts +1 -1
  111. package/dist/plugins/checkout.d.ts.map +1 -1
  112. package/dist/plugins/checkout.js.map +1 -1
  113. package/dist/plugins/convert.d.ts +5 -5
  114. package/dist/plugins/convert.d.ts.map +1 -1
  115. package/dist/plugins/convert.js.map +1 -1
  116. package/dist/plugins/exact.d.ts +1 -1
  117. package/dist/plugins/exact.d.ts.map +1 -1
  118. package/dist/plugins/exact.js.map +1 -1
  119. package/dist/plugins/fixed.d.ts +1 -1
  120. package/dist/plugins/fixed.d.ts.map +1 -1
  121. package/dist/plugins/fixed.js.map +1 -1
  122. package/dist/plugins/format.d.ts +1 -1
  123. package/dist/plugins/format.d.ts.map +1 -1
  124. package/dist/plugins/format.js.map +1 -1
  125. package/dist/plugins/klarna.d.ts +1 -1
  126. package/dist/plugins/klarna.d.ts.map +1 -1
  127. package/dist/plugins/klarna.js.map +1 -1
  128. package/dist/plugins/paypal.d.ts +1 -1
  129. package/dist/plugins/paypal.d.ts.map +1 -1
  130. package/dist/plugins/paypal.js +0 -4
  131. package/dist/plugins/paypal.js.map +1 -1
  132. package/dist/plugins/prisma.d.ts +1 -1
  133. package/dist/plugins/prisma.d.ts.map +1 -1
  134. package/dist/plugins/prisma.js +0 -4
  135. package/dist/plugins/prisma.js.map +1 -1
  136. package/dist/plugins/stripe.d.ts +1 -1
  137. package/dist/plugins/stripe.d.ts.map +1 -1
  138. package/dist/plugins/stripe.js +0 -4
  139. package/dist/plugins/stripe.js.map +1 -1
  140. package/dist/plugins/tax.d.ts +6 -6
  141. package/dist/plugins/tax.d.ts.map +1 -1
  142. package/dist/plugins/tax.js.map +1 -1
  143. package/dist/policy.d.ts +34 -0
  144. package/dist/policy.d.ts.map +1 -0
  145. package/dist/policy.js +90 -0
  146. package/dist/policy.js.map +1 -0
  147. package/dist/prisma.d.ts +25 -14
  148. package/dist/prisma.d.ts.map +1 -1
  149. package/dist/prisma.js +29 -19
  150. package/dist/prisma.js.map +1 -1
  151. package/dist/provider.d.ts +21 -7
  152. package/dist/provider.d.ts.map +1 -1
  153. package/dist/provider.js +51 -11
  154. package/dist/provider.js.map +1 -1
  155. package/dist/rate-text.d.ts +38 -0
  156. package/dist/rate-text.d.ts.map +1 -0
  157. package/dist/rate-text.js +143 -0
  158. package/dist/rate-text.js.map +1 -0
  159. package/dist/rate.d.ts +61 -5
  160. package/dist/rate.d.ts.map +1 -1
  161. package/dist/rate.js +133 -33
  162. package/dist/rate.js.map +1 -1
  163. package/dist/rounding.js +1 -1
  164. package/dist/rounding.js.map +1 -1
  165. package/dist/schema.d.ts +43 -0
  166. package/dist/schema.d.ts.map +1 -0
  167. package/dist/schema.js +77 -0
  168. package/dist/schema.js.map +1 -0
  169. package/dist/sql.d.ts +54 -0
  170. package/dist/sql.d.ts.map +1 -0
  171. package/dist/sql.js +131 -0
  172. package/dist/sql.js.map +1 -0
  173. package/dist/tax.d.ts +13 -9
  174. package/dist/tax.d.ts.map +1 -1
  175. package/dist/tax.js +20 -4
  176. package/dist/tax.js.map +1 -1
  177. package/dist/testing/arbitraries.d.ts +43 -0
  178. package/dist/testing/arbitraries.d.ts.map +1 -0
  179. package/dist/testing/arbitraries.js +98 -0
  180. package/dist/testing/arbitraries.js.map +1 -0
  181. package/dist/testing/index.d.ts +67 -0
  182. package/dist/testing/index.d.ts.map +1 -0
  183. package/dist/testing/index.js +159 -0
  184. package/dist/testing/index.js.map +1 -0
  185. package/dist/testing/jest.d.ts +2 -0
  186. package/dist/testing/jest.d.ts.map +1 -0
  187. package/dist/testing/jest.js +15 -0
  188. package/dist/testing/jest.js.map +1 -0
  189. package/dist/testing/vitest.d.ts +7 -0
  190. package/dist/testing/vitest.d.ts.map +1 -0
  191. package/dist/testing/vitest.js +12 -0
  192. package/dist/testing/vitest.js.map +1 -0
  193. package/dist/valibot.d.ts +33 -0
  194. package/dist/valibot.d.ts.map +1 -0
  195. package/dist/valibot.js +61 -0
  196. package/dist/valibot.js.map +1 -0
  197. package/dist/zod.d.ts +36 -0
  198. package/dist/zod.d.ts.map +1 -0
  199. package/dist/zod.js +58 -0
  200. package/dist/zod.js.map +1 -0
  201. package/package.json +116 -5
  202. package/src/adyen-fx.ts +25 -4
  203. package/src/boc.ts +136 -0
  204. package/src/boe.ts +131 -0
  205. package/src/cash.ts +70 -2
  206. package/src/convert.ts +33 -15
  207. package/src/drizzle/mysql.ts +78 -0
  208. package/src/drizzle/pg.ts +78 -0
  209. package/src/drizzle/sqlite.ts +70 -0
  210. package/src/ecb.ts +179 -13
  211. package/src/fed.ts +146 -0
  212. package/src/fetch.ts +207 -28
  213. package/src/fn.ts +14 -0
  214. package/src/generated/cash.ts +103 -0
  215. package/src/generated/historic.ts +67 -0
  216. package/src/historic.ts +77 -0
  217. package/src/history.ts +333 -0
  218. package/src/hmrc.ts +125 -0
  219. package/src/index.ts +4 -0
  220. package/src/kysely.ts +588 -0
  221. package/src/money.ts +210 -12
  222. package/src/parse.ts +371 -4
  223. package/src/pg.ts +410 -0
  224. package/src/plugins/adyen.ts +3 -2
  225. package/src/plugins/all.ts +10 -0
  226. package/src/plugins/allocate.ts +3 -2
  227. package/src/plugins/braintree.ts +3 -2
  228. package/src/plugins/cash.ts +3 -2
  229. package/src/plugins/checkout.ts +3 -2
  230. package/src/plugins/convert.ts +6 -6
  231. package/src/plugins/exact.ts +3 -2
  232. package/src/plugins/fixed.ts +3 -2
  233. package/src/plugins/format.ts +3 -2
  234. package/src/plugins/klarna.ts +3 -2
  235. package/src/plugins/paypal.ts +3 -2
  236. package/src/plugins/prisma.ts +3 -2
  237. package/src/plugins/stripe.ts +3 -2
  238. package/src/plugins/tax.ts +11 -11
  239. package/src/policy.ts +124 -0
  240. package/src/prisma.ts +91 -10
  241. package/src/provider.ts +81 -16
  242. package/src/rate-text.ts +151 -0
  243. package/src/rate.ts +205 -36
  244. package/src/rounding.ts +1 -1
  245. package/src/schema.ts +119 -0
  246. package/src/sql.ts +178 -0
  247. package/src/tax.ts +40 -9
  248. package/src/testing/arbitraries.ts +134 -0
  249. package/src/testing/index.ts +247 -0
  250. package/src/testing/jest.ts +18 -0
  251. package/src/testing/vitest.ts +18 -0
  252. package/src/valibot.ts +87 -0
  253. package/src/zod.ts +78 -0
package/src/hmrc.ts ADDED
@@ -0,0 +1,125 @@
1
+ // `@ducatjs/money/convert/hmrc`: an offline parser for HMRC's monthly exchange rates, the UK's
2
+ // rates for customs and VAT. No I/O: download the CSV yourself.
3
+ import { rateTable, type RateTable } from './provider.js';
4
+ import { addDayRate, csvRows, isCalendarDay, newestFirst, readRateCell } from './rate-text.js';
5
+
6
+ const UK_DATE = /^(\d\d)\/(\d\d)\/(\d{4})$/;
7
+ const COLUMNS = ['Currency Code', 'Currency Units per £1', 'Start date', 'End date'] as const;
8
+
9
+ function notHmrc(why: string): never {
10
+ throw new SyntaxError(`Not an HMRC monthly exchange rates CSV file: ${why}`);
11
+ }
12
+
13
+ /** 'DD/MM/YYYY' as an ISO date, or SyntaxError. */
14
+ function isoDate(text: string): string {
15
+ const match = UK_DATE.exec(text.trim());
16
+ const day = match === null ? '' : `${match[3]}-${match[2]}-${match[1]}`;
17
+ if (!isCalendarDay(day)) {
18
+ notHmrc(`bad date ${JSON.stringify(text)}`);
19
+ }
20
+ return day;
21
+ }
22
+
23
+ /**
24
+ * When a month's rates are public: the end of the day HMRC publishes them, the Wednesday before the
25
+ * penultimate Thursday of the month before. The trade tariff service's API gives that Wednesday as
26
+ * the `publication_date` of every monthly file from January 2024 to November 2026; it gives no
27
+ * time of day, so the end of that UTC day is the earliest moment that is never too early. That
28
+ * instant is the start of the penultimate Thursday.
29
+ */
30
+ function publishedBy(start: string): string {
31
+ // The last day of the previous month, then back to its last Thursday, then a week before that.
32
+ const lastDay = new Date(Date.UTC(Number(start.slice(0, 4)), Number(start.slice(5, 7)) - 1, 0));
33
+ const backToThursday = (lastDay.getUTCDay() - 4 + 7) % 7;
34
+ lastDay.setUTCDate(lastDay.getUTCDate() - backToThursday - 7);
35
+ return lastDay.toISOString();
36
+ }
37
+
38
+ /** Options for parseHmrcMonthlyCsv. */
39
+ export interface HmrcOptions {
40
+ /**
41
+ * When each period's rates were published, or false for none. Default: the end (UTC) of the
42
+ * Wednesday before the penultimate Thursday of the month before, when HMRC publishes them.
43
+ */
44
+ readonly availableAt?: false | ((start: string) => string | Date);
45
+ }
46
+
47
+ /**
48
+ * Parses HMRC's monthly exchange rates CSV (trade-tariff.service.gov.uk, under the Open
49
+ * Government Licence) into one GBP-based table per period, newest first. Each is valid for its
50
+ * calendar month: `asOf` is the start date and `validUntil` the end date, so a `rateHistory` of
51
+ * them answers any day of the month (see @ducatjs/money/convert/history). Rates are currency units
52
+ * per £1, with `source: 'HMRC'` and `type: 'customs'`, four decimals as published. HMRC publishes
53
+ * each month's file in the month before, on the Wednesday before its penultimate Thursday (the
54
+ * `publication_date` that https://www.trade-tariff.service.gov.uk/api/v2/exchange_rates/
55
+ * period_lists/2026?filter[type]=monthly gives for each file); `availableAt` is the end of that
56
+ * UTC day. Several countries share a code (the CFA francs, AED, XCD): their rows must agree.
57
+ * HMRC never leaves a rate blank, so a blank or missing-value cell throws SyntaxError, as does any
58
+ * other row that can't be read. A file may hold several periods; they may overlap (a mid-month
59
+ * amendment, say), and `rateHistory` then uses the one that started last.
60
+ *
61
+ * const [november] = parseHmrcMonthlyCsv(await readFile('monthly_csv_2026-11.csv', 'utf8'));
62
+ * november.rate('GBP', 'EUR', { asOf: '2026-11-15' })?.toString(); // '1.1930'
63
+ */
64
+ export function parseHmrcMonthlyCsv(csv: string, options?: HmrcOptions): RateTable[] {
65
+ const [header, ...rows] = csvRows(csv.charCodeAt(0) === 0xfeff ? csv.slice(1) : csv);
66
+ const names = (header ?? []).map((name) => name.trim());
67
+ const index = COLUMNS.map((name) => names.indexOf(name));
68
+ if (index.some((i) => i === -1)) {
69
+ notHmrc(`the columns are ${names.join(', ')}`);
70
+ }
71
+ const [codeAt, rateAt, startAt, endAt] = index as [number, number, number, number];
72
+
73
+ if (rows.length === 0) {
74
+ notHmrc('no rates');
75
+ }
76
+
77
+ // Periods by their start and end; within one, rates by code.
78
+ const periods = new Map<string, Map<string, Record<string, string | null>>>();
79
+ for (const cells of rows) {
80
+ if (cells.length !== names.length) {
81
+ notHmrc(`a row has ${cells.length} cells, the header ${names.length}`);
82
+ }
83
+ const code = cells[codeAt]!.trim();
84
+ if (!/^[A-Z]{3}$/.test(code) || code === 'GBP') {
85
+ notHmrc(`bad currency code ${JSON.stringify(code)}`);
86
+ }
87
+ const start = isoDate(cells[startAt]!);
88
+ const end = isoDate(cells[endAt]!);
89
+ if (end < start) {
90
+ notHmrc(`${code} ends on ${end}, before it starts on ${start}`);
91
+ }
92
+ const key = `${start}/${end}`;
93
+ let codes = periods.get(key);
94
+ if (codes === undefined) {
95
+ codes = new Map();
96
+ periods.set(key, codes);
97
+ }
98
+ const where = `${code} from ${start}`;
99
+ const rate = readRateCell(cells[rateAt]!, where);
100
+ if (rate === null) {
101
+ notHmrc(`no rate for ${where}`);
102
+ }
103
+ addDayRate(codes, start, code, rate);
104
+ }
105
+
106
+ const tables: RateTable[] = [];
107
+ for (const [key, codes] of periods) {
108
+ const [start, end] = key.split('/') as [string, string];
109
+ const rates = codes.get(start)!;
110
+ if (!Object.values(rates).some((rate) => rate !== null)) {
111
+ continue;
112
+ }
113
+ let availableAt: string | Date | undefined;
114
+ if (options?.availableAt === undefined) {
115
+ availableAt = publishedBy(start);
116
+ } else if (options.availableAt !== false) {
117
+ availableAt = options.availableAt(start);
118
+ }
119
+ const info = { base: 'GBP', source: 'HMRC', type: 'customs', asOf: start, validUntil: end };
120
+ tables.push(
121
+ rateTable({ ...info, ...(availableAt === undefined ? {} : { availableAt }), rates }),
122
+ );
123
+ }
124
+ return newestFirst(tables);
125
+ }
package/src/index.ts CHANGED
@@ -11,11 +11,15 @@ export {
11
11
  zero,
12
12
  fromJSON,
13
13
  isMoney,
14
+ isMoneyIn,
15
+ assertCurrency,
14
16
  extendMoney,
15
17
  type ExtendMoneyOptions,
16
18
  sum,
17
19
  ratio,
18
20
  CurrencyMismatchError,
21
+ type RoundingPolicy,
22
+ type RoundingInput,
19
23
  type Money,
20
24
  type MoneyJSON,
21
25
  type MoneyOptions,