@ducatjs/money 0.0.0 → 0.2.0

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 (398) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +51 -0
  3. package/README.md +1034 -3
  4. package/assets/CREDITS.md +12 -0
  5. package/data/adyen-fx.json +19 -0
  6. package/data/adyen.json +171 -0
  7. package/data/braintree.json +223 -0
  8. package/data/cash-rounding.json +520 -0
  9. package/data/checkout.json +230 -0
  10. package/data/cldr-cash.json +101 -0
  11. package/data/historic.json +78 -0
  12. package/data/iso4217-list-one.xml +1956 -0
  13. package/data/iso4217-list-three.xml +1185 -0
  14. package/data/klarna.json +95 -0
  15. package/data/paypal.json +65 -0
  16. package/data/stripe-fx.json +17 -0
  17. package/data/stripe.json +285 -0
  18. package/dist/adyen-fx.d.ts +25 -0
  19. package/dist/adyen-fx.d.ts.map +1 -0
  20. package/dist/adyen-fx.js +140 -0
  21. package/dist/adyen-fx.js.map +1 -0
  22. package/dist/adyen.d.ts +25 -0
  23. package/dist/adyen.d.ts.map +1 -0
  24. package/dist/adyen.js +34 -0
  25. package/dist/adyen.js.map +1 -0
  26. package/dist/allocate.d.ts +72 -0
  27. package/dist/allocate.d.ts.map +1 -0
  28. package/dist/allocate.js +314 -0
  29. package/dist/allocate.js.map +1 -0
  30. package/dist/bag.d.ts +58 -0
  31. package/dist/bag.d.ts.map +1 -0
  32. package/dist/bag.js +122 -0
  33. package/dist/bag.js.map +1 -0
  34. package/dist/boc.d.ts +31 -0
  35. package/dist/boc.d.ts.map +1 -0
  36. package/dist/boc.js +114 -0
  37. package/dist/boc.js.map +1 -0
  38. package/dist/boe.d.ts +33 -0
  39. package/dist/boe.d.ts.map +1 -0
  40. package/dist/boe.js +114 -0
  41. package/dist/boe.js.map +1 -0
  42. package/dist/braintree.d.ts +34 -0
  43. package/dist/braintree.d.ts.map +1 -0
  44. package/dist/braintree.js +45 -0
  45. package/dist/braintree.js.map +1 -0
  46. package/dist/cash.d.ts +64 -0
  47. package/dist/cash.d.ts.map +1 -0
  48. package/dist/cash.js +134 -0
  49. package/dist/cash.js.map +1 -0
  50. package/dist/checkout.d.ts +42 -0
  51. package/dist/checkout.d.ts.map +1 -0
  52. package/dist/checkout.js +52 -0
  53. package/dist/checkout.js.map +1 -0
  54. package/dist/convert.d.ts +52 -0
  55. package/dist/convert.d.ts.map +1 -0
  56. package/dist/convert.js +65 -0
  57. package/dist/convert.js.map +1 -0
  58. package/dist/currency.d.ts +22 -0
  59. package/dist/currency.d.ts.map +1 -0
  60. package/dist/currency.js +38 -0
  61. package/dist/currency.js.map +1 -0
  62. package/dist/decimal.d.ts +52 -0
  63. package/dist/decimal.d.ts.map +1 -0
  64. package/dist/decimal.js +251 -0
  65. package/dist/decimal.js.map +1 -0
  66. package/dist/drizzle/mysql.d.ts +105 -0
  67. package/dist/drizzle/mysql.d.ts.map +1 -0
  68. package/dist/drizzle/mysql.js +60 -0
  69. package/dist/drizzle/mysql.js.map +1 -0
  70. package/dist/drizzle/pg.d.ts +103 -0
  71. package/dist/drizzle/pg.d.ts.map +1 -0
  72. package/dist/drizzle/pg.js +60 -0
  73. package/dist/drizzle/pg.js.map +1 -0
  74. package/dist/drizzle/sqlite.d.ts +102 -0
  75. package/dist/drizzle/sqlite.d.ts.map +1 -0
  76. package/dist/drizzle/sqlite.js +59 -0
  77. package/dist/drizzle/sqlite.js.map +1 -0
  78. package/dist/ecb.d.ts +52 -0
  79. package/dist/ecb.d.ts.map +1 -0
  80. package/dist/ecb.js +285 -0
  81. package/dist/ecb.js.map +1 -0
  82. package/dist/exact.d.ts +62 -0
  83. package/dist/exact.d.ts.map +1 -0
  84. package/dist/exact.js +147 -0
  85. package/dist/exact.js.map +1 -0
  86. package/dist/fed.d.ts +35 -0
  87. package/dist/fed.d.ts.map +1 -0
  88. package/dist/fed.js +134 -0
  89. package/dist/fed.js.map +1 -0
  90. package/dist/fetch.d.ts +103 -0
  91. package/dist/fetch.d.ts.map +1 -0
  92. package/dist/fetch.js +222 -0
  93. package/dist/fetch.js.map +1 -0
  94. package/dist/fixed.d.ts +121 -0
  95. package/dist/fixed.d.ts.map +1 -0
  96. package/dist/fixed.js +420 -0
  97. package/dist/fixed.js.map +1 -0
  98. package/dist/fn.d.ts +34 -0
  99. package/dist/fn.d.ts.map +1 -0
  100. package/dist/fn.js +37 -0
  101. package/dist/fn.js.map +1 -0
  102. package/dist/format.d.ts +13 -0
  103. package/dist/format.d.ts.map +1 -0
  104. package/dist/format.js +90 -0
  105. package/dist/format.js.map +1 -0
  106. package/dist/generated/adyen.d.ts +3 -0
  107. package/dist/generated/adyen.d.ts.map +1 -0
  108. package/dist/generated/adyen.js +144 -0
  109. package/dist/generated/adyen.js.map +1 -0
  110. package/dist/generated/braintree.d.ts +3 -0
  111. package/dist/generated/braintree.d.ts.map +1 -0
  112. package/dist/generated/braintree.js +140 -0
  113. package/dist/generated/braintree.js.map +1 -0
  114. package/dist/generated/cash.d.ts +14 -0
  115. package/dist/generated/cash.d.ts.map +1 -0
  116. package/dist/generated/cash.js +465 -0
  117. package/dist/generated/cash.js.map +1 -0
  118. package/dist/generated/checkout.d.ts +3 -0
  119. package/dist/generated/checkout.d.ts.map +1 -0
  120. package/dist/generated/checkout.js +151 -0
  121. package/dist/generated/checkout.js.map +1 -0
  122. package/dist/generated/historic.d.ts +337 -0
  123. package/dist/generated/historic.d.ts.map +1 -0
  124. package/dist/generated/historic.js +66 -0
  125. package/dist/generated/historic.js.map +1 -0
  126. package/dist/generated/iso4217.d.ts +174 -0
  127. package/dist/generated/iso4217.d.ts.map +1 -0
  128. package/dist/generated/iso4217.js +175 -0
  129. package/dist/generated/iso4217.js.map +1 -0
  130. package/dist/generated/kernel.d.ts +17 -0
  131. package/dist/generated/kernel.d.ts.map +1 -0
  132. package/dist/generated/kernel.js +208 -0
  133. package/dist/generated/kernel.js.map +1 -0
  134. package/dist/generated/klarna.d.ts +3 -0
  135. package/dist/generated/klarna.d.ts.map +1 -0
  136. package/dist/generated/klarna.js +21 -0
  137. package/dist/generated/klarna.js.map +1 -0
  138. package/dist/generated/paypal.d.ts +3 -0
  139. package/dist/generated/paypal.d.ts.map +1 -0
  140. package/dist/generated/paypal.js +31 -0
  141. package/dist/generated/paypal.js.map +1 -0
  142. package/dist/generated/stripe.d.ts +8 -0
  143. package/dist/generated/stripe.d.ts.map +1 -0
  144. package/dist/generated/stripe.js +150 -0
  145. package/dist/generated/stripe.js.map +1 -0
  146. package/dist/historic.d.ts +40 -0
  147. package/dist/historic.d.ts.map +1 -0
  148. package/dist/historic.js +43 -0
  149. package/dist/historic.js.map +1 -0
  150. package/dist/history.d.ts +96 -0
  151. package/dist/history.d.ts.map +1 -0
  152. package/dist/history.js +213 -0
  153. package/dist/history.js.map +1 -0
  154. package/dist/hmrc.d.ts +28 -0
  155. package/dist/hmrc.d.ts.map +1 -0
  156. package/dist/hmrc.js +108 -0
  157. package/dist/hmrc.js.map +1 -0
  158. package/dist/index.d.ts +11 -0
  159. package/dist/index.d.ts.map +1 -0
  160. package/dist/index.js +11 -0
  161. package/dist/index.js.map +1 -0
  162. package/dist/klarna.d.ts +31 -0
  163. package/dist/klarna.d.ts.map +1 -0
  164. package/dist/klarna.js +38 -0
  165. package/dist/klarna.js.map +1 -0
  166. package/dist/kysely.d.ts +120 -0
  167. package/dist/kysely.d.ts.map +1 -0
  168. package/dist/kysely.js +309 -0
  169. package/dist/kysely.js.map +1 -0
  170. package/dist/money.d.ts +302 -0
  171. package/dist/money.d.ts.map +1 -0
  172. package/dist/money.js +703 -0
  173. package/dist/money.js.map +1 -0
  174. package/dist/parse.d.ts +55 -0
  175. package/dist/parse.d.ts.map +1 -0
  176. package/dist/parse.js +419 -0
  177. package/dist/parse.js.map +1 -0
  178. package/dist/paypal.d.ts +41 -0
  179. package/dist/paypal.d.ts.map +1 -0
  180. package/dist/paypal.js +58 -0
  181. package/dist/paypal.js.map +1 -0
  182. package/dist/pg.d.ts +100 -0
  183. package/dist/pg.d.ts.map +1 -0
  184. package/dist/pg.js +343 -0
  185. package/dist/pg.js.map +1 -0
  186. package/dist/plugins/adyen.d.ts +15 -0
  187. package/dist/plugins/adyen.d.ts.map +1 -0
  188. package/dist/plugins/adyen.js +10 -0
  189. package/dist/plugins/adyen.js.map +1 -0
  190. package/dist/plugins/all.d.ts +29 -0
  191. package/dist/plugins/all.d.ts.map +1 -0
  192. package/dist/plugins/all.js +30 -0
  193. package/dist/plugins/all.js.map +1 -0
  194. package/dist/plugins/allocate.d.ts +20 -0
  195. package/dist/plugins/allocate.d.ts.map +1 -0
  196. package/dist/plugins/allocate.js +26 -0
  197. package/dist/plugins/allocate.js.map +1 -0
  198. package/dist/plugins/braintree.d.ts +12 -0
  199. package/dist/plugins/braintree.d.ts.map +1 -0
  200. package/dist/plugins/braintree.js +12 -0
  201. package/dist/plugins/braintree.js.map +1 -0
  202. package/dist/plugins/cash.d.ts +19 -0
  203. package/dist/plugins/cash.d.ts.map +1 -0
  204. package/dist/plugins/cash.js +16 -0
  205. package/dist/plugins/cash.js.map +1 -0
  206. package/dist/plugins/checkout.d.ts +21 -0
  207. package/dist/plugins/checkout.d.ts.map +1 -0
  208. package/dist/plugins/checkout.js +16 -0
  209. package/dist/plugins/checkout.js.map +1 -0
  210. package/dist/plugins/convert.d.ts +19 -0
  211. package/dist/plugins/convert.d.ts.map +1 -0
  212. package/dist/plugins/convert.js +13 -0
  213. package/dist/plugins/convert.js.map +1 -0
  214. package/dist/plugins/exact.d.ts +15 -0
  215. package/dist/plugins/exact.d.ts.map +1 -0
  216. package/dist/plugins/exact.js +13 -0
  217. package/dist/plugins/exact.js.map +1 -0
  218. package/dist/plugins/fixed.d.ts +17 -0
  219. package/dist/plugins/fixed.d.ts.map +1 -0
  220. package/dist/plugins/fixed.js +13 -0
  221. package/dist/plugins/fixed.js.map +1 -0
  222. package/dist/plugins/format.d.ts +9 -0
  223. package/dist/plugins/format.d.ts.map +1 -0
  224. package/dist/plugins/format.js +16 -0
  225. package/dist/plugins/format.js.map +1 -0
  226. package/dist/plugins/klarna.d.ts +10 -0
  227. package/dist/plugins/klarna.d.ts.map +1 -0
  228. package/dist/plugins/klarna.js +10 -0
  229. package/dist/plugins/klarna.js.map +1 -0
  230. package/dist/plugins/paypal.d.ts +14 -0
  231. package/dist/plugins/paypal.d.ts.map +1 -0
  232. package/dist/plugins/paypal.js +16 -0
  233. package/dist/plugins/paypal.js.map +1 -0
  234. package/dist/plugins/prisma.d.ts +8 -0
  235. package/dist/plugins/prisma.d.ts.map +1 -0
  236. package/dist/plugins/prisma.js +13 -0
  237. package/dist/plugins/prisma.js.map +1 -0
  238. package/dist/plugins/stripe.d.ts +14 -0
  239. package/dist/plugins/stripe.d.ts.map +1 -0
  240. package/dist/plugins/stripe.js +16 -0
  241. package/dist/plugins/stripe.js.map +1 -0
  242. package/dist/plugins/tax.d.ts +20 -0
  243. package/dist/plugins/tax.d.ts.map +1 -0
  244. package/dist/plugins/tax.js +18 -0
  245. package/dist/plugins/tax.js.map +1 -0
  246. package/dist/policy.d.ts +34 -0
  247. package/dist/policy.d.ts.map +1 -0
  248. package/dist/policy.js +90 -0
  249. package/dist/policy.js.map +1 -0
  250. package/dist/prisma.d.ts +77 -0
  251. package/dist/prisma.d.ts.map +1 -0
  252. package/dist/prisma.js +76 -0
  253. package/dist/prisma.js.map +1 -0
  254. package/dist/processor.d.ts +28 -0
  255. package/dist/processor.d.ts.map +1 -0
  256. package/dist/processor.js +118 -0
  257. package/dist/processor.js.map +1 -0
  258. package/dist/provider.d.ts +103 -0
  259. package/dist/provider.d.ts.map +1 -0
  260. package/dist/provider.js +280 -0
  261. package/dist/provider.js.map +1 -0
  262. package/dist/rate-text.d.ts +38 -0
  263. package/dist/rate-text.d.ts.map +1 -0
  264. package/dist/rate-text.js +143 -0
  265. package/dist/rate-text.js.map +1 -0
  266. package/dist/rate.d.ts +175 -0
  267. package/dist/rate.d.ts.map +1 -0
  268. package/dist/rate.js +371 -0
  269. package/dist/rate.js.map +1 -0
  270. package/dist/rounding-error.d.ts +9 -0
  271. package/dist/rounding-error.d.ts.map +1 -0
  272. package/dist/rounding-error.js +9 -0
  273. package/dist/rounding-error.js.map +1 -0
  274. package/dist/rounding.d.ts +50 -0
  275. package/dist/rounding.d.ts.map +1 -0
  276. package/dist/rounding.js +87 -0
  277. package/dist/rounding.js.map +1 -0
  278. package/dist/schema.d.ts +43 -0
  279. package/dist/schema.d.ts.map +1 -0
  280. package/dist/schema.js +77 -0
  281. package/dist/schema.js.map +1 -0
  282. package/dist/sql.d.ts +54 -0
  283. package/dist/sql.d.ts.map +1 -0
  284. package/dist/sql.js +131 -0
  285. package/dist/sql.js.map +1 -0
  286. package/dist/stripe-fx.d.ts +22 -0
  287. package/dist/stripe-fx.d.ts.map +1 -0
  288. package/dist/stripe-fx.js +30 -0
  289. package/dist/stripe-fx.js.map +1 -0
  290. package/dist/stripe.d.ts +36 -0
  291. package/dist/stripe.d.ts.map +1 -0
  292. package/dist/stripe.js +39 -0
  293. package/dist/stripe.js.map +1 -0
  294. package/dist/tax.d.ts +33 -0
  295. package/dist/tax.d.ts.map +1 -0
  296. package/dist/tax.js +55 -0
  297. package/dist/tax.js.map +1 -0
  298. package/dist/testing/arbitraries.d.ts +43 -0
  299. package/dist/testing/arbitraries.d.ts.map +1 -0
  300. package/dist/testing/arbitraries.js +98 -0
  301. package/dist/testing/arbitraries.js.map +1 -0
  302. package/dist/testing/index.d.ts +67 -0
  303. package/dist/testing/index.d.ts.map +1 -0
  304. package/dist/testing/index.js +159 -0
  305. package/dist/testing/index.js.map +1 -0
  306. package/dist/testing/jest.d.ts +2 -0
  307. package/dist/testing/jest.d.ts.map +1 -0
  308. package/dist/testing/jest.js +15 -0
  309. package/dist/testing/jest.js.map +1 -0
  310. package/dist/testing/vitest.d.ts +7 -0
  311. package/dist/testing/vitest.d.ts.map +1 -0
  312. package/dist/testing/vitest.js +12 -0
  313. package/dist/testing/vitest.js.map +1 -0
  314. package/dist/valibot.d.ts +33 -0
  315. package/dist/valibot.d.ts.map +1 -0
  316. package/dist/valibot.js +61 -0
  317. package/dist/valibot.js.map +1 -0
  318. package/dist/zod.d.ts +36 -0
  319. package/dist/zod.d.ts.map +1 -0
  320. package/dist/zod.js +58 -0
  321. package/dist/zod.js.map +1 -0
  322. package/package.json +280 -5
  323. package/src/adyen-fx.ts +175 -0
  324. package/src/adyen.ts +46 -0
  325. package/src/allocate.ts +393 -0
  326. package/src/bag.ts +170 -0
  327. package/src/boc.ts +136 -0
  328. package/src/boe.ts +131 -0
  329. package/src/braintree.ts +71 -0
  330. package/src/cash.ts +183 -0
  331. package/src/checkout.ts +77 -0
  332. package/src/convert.ts +177 -0
  333. package/src/currency.ts +61 -0
  334. package/src/decimal.ts +283 -0
  335. package/src/drizzle/mysql.ts +78 -0
  336. package/src/drizzle/pg.ts +78 -0
  337. package/src/drizzle/sqlite.ts +70 -0
  338. package/src/ecb.ts +328 -0
  339. package/src/exact.ts +182 -0
  340. package/src/fed.ts +146 -0
  341. package/src/fetch.ts +306 -0
  342. package/src/fixed.ts +558 -0
  343. package/src/fn.ts +94 -0
  344. package/src/format.ts +106 -0
  345. package/src/generated/adyen.ts +144 -0
  346. package/src/generated/braintree.ts +140 -0
  347. package/src/generated/cash.ts +478 -0
  348. package/src/generated/checkout.ts +151 -0
  349. package/src/generated/historic.ts +67 -0
  350. package/src/generated/iso4217.ts +179 -0
  351. package/src/generated/kernel.ts +230 -0
  352. package/src/generated/klarna.ts +21 -0
  353. package/src/generated/paypal.ts +31 -0
  354. package/src/generated/stripe.ts +150 -0
  355. package/src/historic.ts +77 -0
  356. package/src/history.ts +333 -0
  357. package/src/hmrc.ts +125 -0
  358. package/src/index.ts +41 -0
  359. package/src/klarna.ts +54 -0
  360. package/src/kysely.ts +588 -0
  361. package/src/money.ts +979 -0
  362. package/src/parse.ts +511 -0
  363. package/src/paypal.ts +93 -0
  364. package/src/pg.ts +410 -0
  365. package/src/plugins/adyen.ts +25 -0
  366. package/src/plugins/all.ts +57 -0
  367. package/src/plugins/allocate.ts +53 -0
  368. package/src/plugins/braintree.ts +23 -0
  369. package/src/plugins/cash.ts +33 -0
  370. package/src/plugins/checkout.ts +32 -0
  371. package/src/plugins/convert.ts +49 -0
  372. package/src/plugins/exact.ts +23 -0
  373. package/src/plugins/fixed.ts +28 -0
  374. package/src/plugins/format.ts +28 -0
  375. package/src/plugins/klarna.ts +21 -0
  376. package/src/plugins/paypal.ts +29 -0
  377. package/src/plugins/prisma.ts +24 -0
  378. package/src/plugins/stripe.ts +29 -0
  379. package/src/plugins/tax.ts +40 -0
  380. package/src/policy.ts +124 -0
  381. package/src/prisma.ts +199 -0
  382. package/src/processor.ts +181 -0
  383. package/src/provider.ts +389 -0
  384. package/src/rate-text.ts +151 -0
  385. package/src/rate.ts +546 -0
  386. package/src/rounding-error.ts +8 -0
  387. package/src/rounding.ts +122 -0
  388. package/src/schema.ts +119 -0
  389. package/src/sql.ts +178 -0
  390. package/src/stripe-fx.ts +49 -0
  391. package/src/stripe.ts +69 -0
  392. package/src/tax.ts +102 -0
  393. package/src/testing/arbitraries.ts +134 -0
  394. package/src/testing/index.ts +247 -0
  395. package/src/testing/jest.ts +18 -0
  396. package/src/testing/vitest.ts +18 -0
  397. package/src/valibot.ts +87 -0
  398. package/src/zod.ts +78 -0
@@ -0,0 +1,122 @@
1
+ import {
2
+ divideAndRoundBigint as provedDivideAndRoundBigint,
3
+ roundTruncatedQuotient,
4
+ } from './generated/kernel.js';
5
+ import { RoundingError } from './rounding-error.js';
6
+
7
+ /**
8
+ * Rounding modes. The string values are the ones `Intl.NumberFormat` uses for its `roundingMode`
9
+ * option (ES2023), so a mode can be passed straight through to formatting. `Rounding` gives the
10
+ * familiar names (HalfUp, HalfEven, ...) for the same strings.
11
+ *
12
+ * An invalid mode (possible only from untyped code) is reported, with a RangeError, only when
13
+ * rounding is actually needed: an exact result is returned without looking at the mode, which keeps
14
+ * the exact path as fast as it can be.
15
+ */
16
+ export type RoundingMode =
17
+ // ties to even ("banker's rounding"; IEEE 754 default, EU VAT practice in many states)
18
+ | 'halfEven'
19
+ | 'halfExpand' // ties away from zero ("half up" in commercial usage)
20
+ | 'halfTrunc' // ties towards zero ("half down")
21
+ | 'halfCeil' // ties towards +infinity
22
+ | 'halfFloor' // ties towards -infinity
23
+ | 'expand' // away from zero ("up")
24
+ | 'trunc' // towards zero ("down")
25
+ | 'ceil' // towards +infinity
26
+ | 'floor' // towards -infinity
27
+ // throw RoundingError if the result is inexact (the default for parsing and processor amounts)
28
+ | 'unnecessary';
29
+
30
+ /**
31
+ * Familiar names for the rounding modes (HalfUp, HalfEven, Bankers, ...), mapped to the mode
32
+ * strings.
33
+ */
34
+ export const Rounding = {
35
+ HalfEven: 'halfEven',
36
+ Bankers: 'halfEven',
37
+ HalfUp: 'halfExpand',
38
+ HalfDown: 'halfTrunc',
39
+ HalfCeil: 'halfCeil',
40
+ HalfFloor: 'halfFloor',
41
+ Up: 'expand',
42
+ Down: 'trunc',
43
+ Ceil: 'ceil',
44
+ Floor: 'floor',
45
+ Unnecessary: 'unnecessary',
46
+ } as const satisfies Record<string, RoundingMode>;
47
+
48
+ /**
49
+ * The rounding mode arithmetic uses when none is given: ties away from zero ('half up'). It is the
50
+ * default of decimal.js, big.js, bignumber.js, Prisma.Decimal and Intl.NumberFormat, and what
51
+ * Math.round-based money libraries (currency.js, js-money, ts-money) intend. Parsing
52
+ * (`money('1.005', 'EUR')`, `fromDecimal`) and processor amounts (`toStripeAmount`, `toAdyen`) stay
53
+ * strict: they throw unless a mode is passed.
54
+ */
55
+ export const DEFAULT_ROUNDING: RoundingMode = 'halfExpand';
56
+
57
+ export { RoundingError };
58
+
59
+ const MAX_SAFE_MINOR = Number.MAX_SAFE_INTEGER;
60
+
61
+ /**
62
+ * dividend / divisor rounded with `mode`, for safe-integer dividends and divisor > 0. Exact: the
63
+ * float quotient is only a first guess, corrected until the remainder has the dividend's sign and
64
+ * is smaller than the divisor.
65
+ */
66
+ export function divideAndRound(dividend: number, divisor: number, mode: RoundingMode): number {
67
+ if (divisor === 1) {
68
+ return dividend + 0; // + 0 normalises -0 (0 * -1 from a caller)
69
+ }
70
+ // Beyond MAX_SAFE_MINOR the product quotient * divisor below could round, so take the bigint
71
+ // path.
72
+ if (!(Math.abs(dividend) + divisor <= MAX_SAFE_MINOR)) {
73
+ return Number(divideAndRoundBigint(BigInt(dividend), BigInt(divisor), mode));
74
+ }
75
+
76
+ let quotient = Math.trunc(dividend / divisor);
77
+ // exact: |quotient * divisor| <= |dividend| + divisor <= MAX_SAFE_MINOR
78
+ let remainder = dividend - quotient * divisor;
79
+
80
+ // The float quotient can be off by one ulp; fix it so the remainder has the dividend's sign and
81
+ // is smaller than the divisor in magnitude.
82
+ if (dividend >= 0) {
83
+ if (remainder < 0) {
84
+ quotient -= 1;
85
+ remainder += divisor;
86
+ } else if (remainder >= divisor) {
87
+ quotient += 1;
88
+ remainder -= divisor;
89
+ }
90
+ } else if (remainder > 0) {
91
+ quotient += 1;
92
+ remainder -= divisor;
93
+ } else if (remainder <= -divisor) {
94
+ quotient -= 1;
95
+ remainder += divisor;
96
+ }
97
+
98
+ // The generated switch handles a zero remainder too; returning here saves the call on the hottest
99
+ // path.
100
+ if (remainder === 0) {
101
+ return quotient + 0; // + 0 normalises -0
102
+ }
103
+ // quotient and remainder are now the exact truncated quotient and remainder, as the generated
104
+ // switch expects
105
+ return roundTruncatedQuotient(dividend, divisor, quotient, remainder, mode) + 0;
106
+ }
107
+
108
+ /**
109
+ * dividend / divisor rounded with `mode`, for divisor > 0n. The bigint kernel is generated from the
110
+ * Lean proofs in proofs/Ducat/Rounding.lean (`src/generated/kernel.ts`), and so is the mode switch
111
+ * `divideAndRound` uses.
112
+ */
113
+ export function divideAndRoundBigint(
114
+ dividend: bigint,
115
+ divisor: bigint,
116
+ mode: RoundingMode,
117
+ ): bigint {
118
+ if (divisor === 1n) {
119
+ return dividend;
120
+ }
121
+ return provedDivideAndRoundBigint(dividend, divisor, mode);
122
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { isCurrency } from './currency.js';
2
+ import { MAX_DECIMAL_SCALE } from './decimal.js';
3
+ import { money, type Money, type MoneyJSON } from './money.js';
4
+ import { Rounding, type RoundingMode } from './rounding.js';
5
+
6
+ /** Options for the Zod and Valibot money schemas. */
7
+ export interface MoneySchemaOptions<C extends string = string> {
8
+ /**
9
+ * The currency, or the currencies, to accept. Without it, any ISO 4217 code or code registered
10
+ * with `defineCurrency` is accepted, and nothing else.
11
+ */
12
+ readonly currency?: C | readonly C[];
13
+ /** Decimal places to keep (default: the currency's exponent), as in `money()`. */
14
+ readonly scale?: number;
15
+ /**
16
+ * How to round an amount with more decimals than the scale. The default, 'unnecessary', rejects
17
+ * such amounts, as `fromJSON` does.
18
+ */
19
+ readonly rounding?: RoundingMode;
20
+ /**
21
+ * The most digits the amount may have, not counting the sign and the decimal point (default
22
+ * 50). It keeps an oversized request body from building a huge bigint.
23
+ */
24
+ readonly maxDigits?: number;
25
+ }
26
+
27
+ /** The default `maxDigits`: far beyond any real amount, small enough to be cheap to reject. */
28
+ export const DEFAULT_MAX_DIGITS = 50;
29
+
30
+ const MODES = new Set<unknown>(Object.values(Rounding));
31
+
32
+ /**
33
+ * @internal Checks the options when the schema is built, so a mistake there throws at once
34
+ * instead of turning every input into a validation error.
35
+ */
36
+ export function checkSchemaOptions(options: MoneySchemaOptions | undefined): void {
37
+ if (options === undefined) {
38
+ return;
39
+ }
40
+ const allowed = options.currency;
41
+ const codes = typeof allowed === 'string' || allowed === undefined ? [allowed] : allowed;
42
+ for (const code of codes) {
43
+ if (code !== undefined && !isCurrency(code)) {
44
+ throw new RangeError(`Unknown currency ${String(code)}`);
45
+ }
46
+ }
47
+ const scale = options.scale;
48
+ if (
49
+ scale !== undefined &&
50
+ !(Number.isInteger(scale) && scale >= 0 && scale <= MAX_DECIMAL_SCALE)
51
+ ) {
52
+ throw new RangeError(`Invalid scale ${scale}`);
53
+ }
54
+ if (options.rounding !== undefined && !MODES.has(options.rounding)) {
55
+ throw new RangeError(`Invalid rounding mode ${String(options.rounding)}`);
56
+ }
57
+ const maxDigits = options.maxDigits;
58
+ if (maxDigits !== undefined && !(Number.isSafeInteger(maxDigits) && maxDigits >= 1)) {
59
+ throw new RangeError(`Invalid maxDigits ${maxDigits}`);
60
+ }
61
+ }
62
+
63
+ /** The Money type a schema produces: one Money per allowed currency, not a mixed-currency Money. */
64
+ export type MoneyFor<C extends string> = C extends string ? Money<C> : never;
65
+
66
+ /** @internal A problem with one field of the input, for the schema libraries' issue paths. */
67
+ export interface MoneyIssue {
68
+ readonly field: 'amount' | 'currency';
69
+ readonly message: string;
70
+ }
71
+
72
+ // The exact decimal strings `toJSON` writes: an optional minus, digits, and an optional fraction.
73
+ // No exponent, no plus sign, no whitespace and no grouping, so the wire format stays unambiguous.
74
+ const DECIMAL = /^-?\d+(?:\.\d+)?$/;
75
+
76
+ /**
77
+ * @internal Reads `{ amount, currency }` (already known to have two string fields) into Money, or
78
+ * says what is wrong with it. Shared by the Zod and Valibot entries, so both accept exactly the
79
+ * same inputs.
80
+ */
81
+ export function readMoneyJSON<C extends string>(
82
+ json: MoneyJSON,
83
+ options: MoneySchemaOptions<C> | undefined,
84
+ ): Money | MoneyIssue {
85
+ const currency = json.currency;
86
+ const allowed = options?.currency;
87
+
88
+ if (allowed === undefined) {
89
+ if (!isCurrency(currency)) {
90
+ return { field: 'currency', message: `Unknown currency "${currency}"` };
91
+ }
92
+ } else if (
93
+ typeof allowed === 'string' ? currency !== allowed : !allowed.includes(currency as C)
94
+ ) {
95
+ const expected = typeof allowed === 'string' ? allowed : allowed.join(', ');
96
+ return { field: 'currency', message: `Expected currency ${expected}, got "${currency}"` };
97
+ }
98
+
99
+ const amount = json.amount;
100
+ // Checked before the pattern, so a huge string is rejected without scanning it.
101
+ const maxLength = (options?.maxDigits ?? DEFAULT_MAX_DIGITS) + 2;
102
+ if (amount.length > maxLength || !DECIMAL.test(amount)) {
103
+ return { field: 'amount', message: 'Expected a decimal string such as "12.34"' };
104
+ }
105
+ const digitCount = amount.length - (amount[0] === '-' ? 1 : 0) - (amount.includes('.') ? 1 : 0);
106
+ if (digitCount > (options?.maxDigits ?? DEFAULT_MAX_DIGITS)) {
107
+ return {
108
+ field: 'amount',
109
+ message: `More than ${options?.maxDigits ?? DEFAULT_MAX_DIGITS} digits`,
110
+ };
111
+ }
112
+
113
+ try {
114
+ return money(amount, currency, options);
115
+ } catch (error) {
116
+ // the options are checked up front, so this is too many decimals for the scale
117
+ return { field: 'amount', message: (error as Error).message };
118
+ }
119
+ }
package/src/sql.ts ADDED
@@ -0,0 +1,178 @@
1
+ // Conversions between Money and the values SQL drivers send and receive, shared by the ORM and
2
+ // query-builder integrations (`/drizzle/*`, `/kysely`). Every reader is strict: a driver value that
3
+ // may already have lost digits (an unsafe number, a float for a DECIMAL) is rejected rather than
4
+ // trusted, and every writer refuses a value the column would change.
5
+ import { currencyExponent } from './currency.js';
6
+ import {
7
+ CurrencyMismatchError,
8
+ fromJSON,
9
+ fromMinor,
10
+ money as parseMoney,
11
+ toDecimal,
12
+ toMinor,
13
+ type Money,
14
+ type MoneyJSON,
15
+ } from './money.js';
16
+
17
+ const INTEGER_TEXT = /^-?\d+$/;
18
+ // a BIGINT (SQLite INTEGER) column's range: MySQL outside strict mode clips anything beyond it
19
+ const BIGINT_MIN = -(2n ** 63n);
20
+ const BIGINT_MAX = 2n ** 63n - 1n;
21
+
22
+ /** The declared size of a NUMERIC/DECIMAL column. */
23
+ export interface DecimalColumnSize {
24
+ /** Total number of significant digits the column holds. */
25
+ readonly precision: number;
26
+ /** Digits after the decimal point. */
27
+ readonly scale: number;
28
+ }
29
+
30
+ /** NUMERIC(19,4): 15 integer digits and 4 decimals, enough for every ISO 4217 exponent. */
31
+ export const DEFAULT_DECIMAL_SIZE: DecimalColumnSize = { precision: 19, scale: 4 };
32
+
33
+ /** Throws unless the money is in the column's currency (when the column has a fixed currency). */
34
+ export function checkCurrency(money: Money, currency: string | undefined): void {
35
+ if (currency !== undefined && money.currency !== currency) {
36
+ throw new CurrencyMismatchError(currency, money.currency);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Throws unless the money is at its currency's exponent. Columns store amounts at the currency's
42
+ * scale, so a value with sub-minor digits would have to be rounded, and the caller should choose how.
43
+ */
44
+ export function checkIsoScale(money: Money): void {
45
+ const exponent = currencyExponent(money.currency);
46
+ if (money.scale !== exponent) {
47
+ throw new RangeError(
48
+ `${money} has scale ${money.scale}; round() it to ${exponent} decimals before storing it`,
49
+ );
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Minor units for a BIGINT column, as a bigint. Throws beyond the column's range, ±9.2·10^18:
55
+ * MySQL outside strict mode would store the nearest limit instead.
56
+ */
57
+ export function minorToDriver(money: Money, currency: string | undefined): bigint {
58
+ checkCurrency(money, currency);
59
+ checkIsoScale(money);
60
+ const minor = toMinor(money);
61
+
62
+ if (minor < BIGINT_MIN || minor > BIGINT_MAX) {
63
+ throw new RangeError(`${money} is ${minor} minor units, beyond the range of a BIGINT column`);
64
+ }
65
+ return minor;
66
+ }
67
+
68
+ /**
69
+ * Money from a BIGINT driver value. Accepts a bigint, a safe-integer number, or an integer string
70
+ * (node-postgres returns int8 as a string). A number beyond 2^53 has already lost digits, so it
71
+ * throws.
72
+ */
73
+ export function minorFromDriver<C extends string>(value: unknown, currency: C): Money<C> {
74
+ if (typeof value === 'string') {
75
+ if (!INTEGER_TEXT.test(value)) {
76
+ throw new SyntaxError(`Expected an integer from a BIGINT column, got "${value}"`);
77
+ }
78
+ return fromMinor(BigInt(value), currency);
79
+ }
80
+ if (typeof value === 'number' || typeof value === 'bigint') {
81
+ return fromMinor(value, currency);
82
+ }
83
+ throw new TypeError(`Expected an integer from a BIGINT column, got ${typeof value}`);
84
+ }
85
+
86
+ /** Throws if a decimal column of this size would round amounts in this currency. */
87
+ export function checkDecimalSize(currency: string, size: DecimalColumnSize): void {
88
+ const { precision, scale } = size;
89
+ const isValidSize =
90
+ Number.isInteger(precision) &&
91
+ Number.isInteger(scale) &&
92
+ precision >= 1 &&
93
+ scale >= 0 &&
94
+ scale <= precision;
95
+ if (!isValidSize) {
96
+ throw new RangeError(`NUMERIC(${precision},${scale}) is not a valid column size`);
97
+ }
98
+
99
+ const exponent = currencyExponent(currency);
100
+ if (size.scale < exponent) {
101
+ throw new RangeError(
102
+ `NUMERIC(${size.precision},${size.scale}) would round ${currency}, which has ${exponent} decimals`,
103
+ );
104
+ }
105
+ }
106
+
107
+ /**
108
+ * The exact decimal string for a NUMERIC/DECIMAL column. Throws if the column could not hold it
109
+ * unchanged: MySQL outside strict mode clamps an out-of-range value instead of failing.
110
+ */
111
+ export function decimalToDriver(
112
+ money: Money,
113
+ currency: string | undefined,
114
+ size: DecimalColumnSize | undefined,
115
+ ): string {
116
+ checkCurrency(money, currency);
117
+ checkIsoScale(money);
118
+ const decimal = toDecimal(money);
119
+
120
+ if (size !== undefined) {
121
+ checkIntegerDigits(money, decimal, size);
122
+ }
123
+ return decimal;
124
+ }
125
+
126
+ /**
127
+ * Throws if the decimal (the money's `toDecimal`) has more integer digits than a column of this
128
+ * size holds: Postgres rejects it, and MySQL outside strict mode clamps it.
129
+ */
130
+ export function checkIntegerDigits(money: Money, decimal: string, size: DecimalColumnSize): void {
131
+ const integerPart = decimal.replace('-', '').split('.')[0]!;
132
+ // "0.12" has no integer digits: NUMERIC(4,4) holds it
133
+ const integerDigits = integerPart === '0' ? 0 : integerPart.length;
134
+ if (integerDigits > size.precision - size.scale) {
135
+ throw new RangeError(
136
+ `${money} does not fit NUMERIC(${size.precision},${size.scale}), which holds ` +
137
+ `${size.precision - size.scale} integer digits`,
138
+ );
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Money from a NUMERIC/DECIMAL driver value, which must be a string: a number has been through a
144
+ * float. Trailing zeros (`12.3400` from NUMERIC(19,4)) are fine; more decimals than the currency
145
+ * has, `NaN` and `Infinity` throw.
146
+ */
147
+ export function decimalFromDriver<C extends string>(value: unknown, currency: C): Money<C> {
148
+ if (typeof value !== 'string') {
149
+ throw new TypeError(
150
+ `Expected a string from a DECIMAL column, got ${typeof value}; ` +
151
+ 'configure the driver to return decimals as strings',
152
+ );
153
+ }
154
+ return parseMoney(value, currency);
155
+ }
156
+
157
+ /** `{"amount":"12.34","currency":"EUR"}` for a JSON, JSONB or TEXT column. */
158
+ export function jsonToDriver(money: Money, currency: string | undefined): string {
159
+ checkCurrency(money, currency);
160
+ checkIsoScale(money);
161
+ return JSON.stringify(money);
162
+ }
163
+
164
+ /**
165
+ * Money from a JSON column: the parsed object (JSONB in node-postgres, JSON in mysql2) or the
166
+ * text (SQLite).
167
+ */
168
+ export function jsonFromDriver<C extends string>(
169
+ value: unknown,
170
+ currency: C | undefined,
171
+ ): Money<C> {
172
+ if (typeof value !== 'string' && (typeof value !== 'object' || value === null)) {
173
+ throw new TypeError(`Expected JSON from a money column, got ${String(value)}`);
174
+ }
175
+ const parsed = fromJSON(value as MoneyJSON | string) as Money<C>;
176
+ checkCurrency(parsed, currency);
177
+ return parsed;
178
+ }
@@ -0,0 +1,49 @@
1
+ // Stripe exchange rates as exact Rates, for converting and reconciling balance transactions.
2
+ import type { Money } from './money.js';
3
+ import { exchangeRate, impliedRate, type Rate, type RateInfo } from './rate.js';
4
+ import { fromStripe } from './stripe.js';
5
+
6
+ /** The fields of a Stripe BalanceTransaction used here. */
7
+ export interface StripeBalanceTransaction {
8
+ readonly amount: number;
9
+ readonly currency: string;
10
+ readonly exchange_rate?: number | null;
11
+ /** Seconds since the Unix epoch. */
12
+ readonly created?: number;
13
+ }
14
+
15
+ function stripeInfo(transaction: StripeBalanceTransaction): RateInfo {
16
+ if (transaction.created === undefined) {
17
+ return { source: 'stripe' };
18
+ }
19
+ return { source: 'stripe', asOf: new Date(transaction.created * 1000) };
20
+ }
21
+
22
+ /**
23
+ * The rate Stripe quotes on a balance transaction, from the charge currency `from` to the balance
24
+ * currency: "the amount in currency A, multiplied by the exchange_rate, equals the amount in
25
+ * currency B". The JSON number is read at its shortest decimal form, so 1.234 is exactly 1234/1000.
26
+ * `asOf` is the balance transaction's `created` time, the closest timestamp Stripe gives for the
27
+ * conversion; it is not the charge's creation time, and Stripe doesn't say when the rate was
28
+ * quoted. `undefined` when nothing was converted.
29
+ */
30
+ export function stripeRate<From extends string>(
31
+ from: From,
32
+ transaction: StripeBalanceTransaction,
33
+ ): Rate<Uppercase<From>, Uppercase<string>> | undefined {
34
+ if (transaction.exchange_rate === undefined || transaction.exchange_rate === null) {
35
+ return undefined;
36
+ }
37
+ const charged = from.toUpperCase() as Uppercase<From>;
38
+ const balance = transaction.currency.toUpperCase() as Uppercase<string>;
39
+ return exchangeRate(charged, balance, transaction.exchange_rate, stripeInfo(transaction));
40
+ }
41
+
42
+ /** The exact rate Stripe applied: the balance transaction's gross amount ÷ the charged amount. */
43
+ export function stripeImpliedRate<From extends string>(
44
+ charged: Money<From>,
45
+ transaction: StripeBalanceTransaction,
46
+ ): Rate<From, Uppercase<string>> {
47
+ const settled = fromStripe(transaction.amount, transaction.currency.toUpperCase());
48
+ return impliedRate(charged, settled, stripeInfo(transaction));
49
+ }
package/src/stripe.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Stripe's minor-unit view. Stripe uses ISO exponents except where documented otherwise
3
+ * (https://docs.stripe.com/currencies, encoded in data/stripe.json):
4
+ * - zero-decimal: BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF VND VUV XAF XOF XPF (note MGA: ISO says
5
+ * 2)
6
+ * - ISK, UGX: sent as two-decimal values whose last two digits are 00 (5 ISK -> 500)
7
+ * - HUF, TWD: two decimals for charges, but payouts must be whole units (multiple of 100)
8
+ * - BHD JOD KWD OMR TND: three decimals, last digit must be 0 (UAE accounts only)
9
+ */
10
+ import { STRIPE_RULES } from './generated/stripe.js';
11
+ import { type CurrencyInput, type Money } from './money.js';
12
+ import { fromProcessorAmount, toProcessorAmount } from './processor.js';
13
+ import type { RoundingMode } from './rounding.js';
14
+
15
+ export { STRIPE_RULES };
16
+
17
+ function rule(currency: string) {
18
+ const found = STRIPE_RULES[currency.toUpperCase()];
19
+ if (!found) {
20
+ throw new RangeError(`${currency} is not a Stripe presentment currency`);
21
+ }
22
+ return found;
23
+ }
24
+
25
+ /** Whether Stripe supports the currency as a presentment currency. */
26
+ export const isStripeCurrency = (currency: string): boolean =>
27
+ STRIPE_RULES[currency.toUpperCase()] !== undefined;
28
+
29
+ /** The exponent Stripe's API uses for a currency (ISK 2, JPY 0, KWD 3, MGA 0). */
30
+ export const stripeExponent = (currency: string): number => rule(currency)[0];
31
+
32
+ /**
33
+ * Options for the Stripe amount: payout rules and how to round when Stripe needs a coarser amount.
34
+ */
35
+ export interface StripeAmountOptions {
36
+ /** Payout amounts have stricter rules (HUF, TWD: whole units). */
37
+ readonly payout?: boolean;
38
+ /** How to round when Stripe needs a coarser amount (default 'unnecessary': throw). */
39
+ readonly rounding?: RoundingMode;
40
+ }
41
+
42
+ /** The integer `amount` to send to Stripe, e.g. toStripeAmount(money('5', 'ISK')) === 500. */
43
+ export function toStripeAmount(money: Money, options?: StripeAmountOptions): number {
44
+ const [exp, charge, payout] = rule(money.currency);
45
+ return toProcessorAmount(
46
+ 'Stripe',
47
+ money,
48
+ exp,
49
+ options?.payout ? payout : charge,
50
+ options?.rounding ?? 'unnecessary',
51
+ );
52
+ }
53
+
54
+ /** `{ amount, currency }` with Stripe's lowercase currency code, ready for a PaymentIntent. */
55
+ export function toStripe(
56
+ money: Money,
57
+ options?: StripeAmountOptions,
58
+ ): { amount: number; currency: string } {
59
+ return { amount: toStripeAmount(money, options), currency: money.currency.toLowerCase() };
60
+ }
61
+
62
+ /** Reads a Stripe amount (e.g. from a webhook): fromStripe(500, 'isk') is 5 ISK. */
63
+ export function fromStripe<C extends CurrencyInput>(
64
+ amount: number | bigint,
65
+ currency: C,
66
+ ): Money<Uppercase<C>> {
67
+ const code = currency.toUpperCase() as Uppercase<C>;
68
+ return fromProcessorAmount('Stripe', amount, code, rule(code)[0]);
69
+ }
package/src/tax.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { ratio, type Factor, type Ratio } from './decimal.js';
2
+ import {
3
+ add,
4
+ multiplyByRatio,
5
+ roundWithPolicy,
6
+ subtract,
7
+ type Money,
8
+ type RoundingInput,
9
+ } from './money.js';
10
+ import { DEFAULT_ROUNDING } from './rounding.js';
11
+
12
+ const percentRatio = (percent: Factor): Ratio => {
13
+ const asRatio = ratio(percent);
14
+ return ratio(asRatio.num, asRatio.den * 100n);
15
+ };
16
+
17
+ /**
18
+ * percent% of m: percentage(EUR 80, '12.5', 'halfEven') = EUR 10.00. Rounding defaults to
19
+ * 'halfExpand'.
20
+ */
21
+ export function percentage<C extends string>(
22
+ money: Money<C>,
23
+ percent: Factor,
24
+ rounding: RoundingInput = DEFAULT_ROUNDING,
25
+ ): Money<C> {
26
+ return multiplyByRatio(money, percentRatio(percent), rounding);
27
+ }
28
+
29
+ /** A tax split: net + tax is always exactly gross. */
30
+ export interface TaxBreakdown<C extends string> {
31
+ readonly net: Money<C>;
32
+ readonly tax: Money<C>;
33
+ /** Always exactly net + tax. */
34
+ readonly gross: Money<C>;
35
+ }
36
+
37
+ /**
38
+ * Tax-exclusive: adds percent% tax to a net amount. addTax(EUR 10, '20', 'halfEven') -> net 10, tax
39
+ * 2, gross 12. A rounding mode rounds the tax; a rounding policy rounds the gross (the amount
40
+ * charged) to its scale and increment. The other part is always the exact difference.
41
+ */
42
+ export function addTax<C extends string>(
43
+ net: Money<C>,
44
+ percent: Factor,
45
+ rounding: RoundingInput = DEFAULT_ROUNDING,
46
+ ): TaxBreakdown<C> {
47
+ // A rounding mode rounds the tax; a policy rounds the gross, the amount charged, so it lands on
48
+ // the policy's increment (CHF 0.05 for cash). Either way the other part is the exact difference.
49
+ if (typeof rounding === 'function') {
50
+ const percentAsRatio = ratio(percent);
51
+ const gross = roundWithPolicy(
52
+ net,
53
+ ratio(100n * percentAsRatio.den + percentAsRatio.num, 100n * percentAsRatio.den),
54
+ rounding,
55
+ );
56
+ return { net, tax: subtract(gross, net), gross };
57
+ }
58
+ const tax = multiplyByRatio(net, percentRatio(percent), rounding);
59
+ const gross = add(net, tax);
60
+ return { net, tax, gross };
61
+ }
62
+
63
+ /**
64
+ * Tax-inclusive: extracts percent% tax contained in a gross amount (VAT extraction).
65
+ * net = gross / (1 + percent/100) rounded; tax = gross - net, so the parts always add up.
66
+ * extractTax(EUR 12, '20', 'halfEven') -> net 10.00, tax 2.00. A rounding policy rounds the net; the
67
+ * tax is on the policy's increment too whenever the gross is.
68
+ */
69
+ export function extractTax<C extends string>(
70
+ gross: Money<C>,
71
+ percent: Factor,
72
+ rounding: RoundingInput = DEFAULT_ROUNDING,
73
+ ): TaxBreakdown<C> {
74
+ const percentAsRatio = ratio(percent);
75
+ // 1 / (1 + p/100) = 100 den / (100 den + num)
76
+ const net = multiplyByRatio(
77
+ gross,
78
+ ratio(100n * percentAsRatio.den, 100n * percentAsRatio.den + percentAsRatio.num),
79
+ rounding,
80
+ );
81
+ return { net, tax: subtract(gross, net), gross };
82
+ }
83
+
84
+ /**
85
+ * Price after a percent% discount. A rounding mode rounds the discount amount; a rounding policy
86
+ * rounds the discounted price itself, so the price lands on the policy's scale and increment.
87
+ */
88
+ export function discount<C extends string>(
89
+ price: Money<C>,
90
+ percent: Factor,
91
+ rounding: RoundingInput = DEFAULT_ROUNDING,
92
+ ): Money<C> {
93
+ if (typeof rounding === 'function') {
94
+ const percentAsRatio = ratio(percent);
95
+ return roundWithPolicy(
96
+ price,
97
+ ratio(100n * percentAsRatio.den - percentAsRatio.num, 100n * percentAsRatio.den),
98
+ rounding,
99
+ );
100
+ }
101
+ return subtract(price, multiplyByRatio(price, percentRatio(percent), rounding));
102
+ }