@amboras-dev/money 0.0.0-stage → 0.1.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.
- package/README.md +393 -2
- package/dist/ai.cjs +185 -0
- package/dist/ai.d.cts +97 -0
- package/dist/ai.d.ts +97 -0
- package/dist/ai.js +175 -0
- package/dist/allocate-CdnhvAda.js +199 -0
- package/dist/allocate-D7tm_t_o.cjs +222 -0
- package/dist/amount-D7dj76nH.js +200 -0
- package/dist/amount-DHtvZSSm.cjs +241 -0
- package/dist/arithmetic-Bq-8mSal.cjs +233 -0
- package/dist/arithmetic-C-Q_aI49.js +132 -0
- package/dist/billing.cjs +184 -0
- package/dist/billing.d.cts +81 -0
- package/dist/billing.d.ts +81 -0
- package/dist/billing.js +175 -0
- package/dist/commerce.cjs +536 -0
- package/dist/commerce.d.cts +293 -0
- package/dist/commerce.d.ts +293 -0
- package/dist/commerce.js +516 -0
- package/dist/format-BdB7LEIp.js +90 -0
- package/dist/format-BnA1aLJk.cjs +113 -0
- package/dist/fx.cjs +407 -0
- package/dist/fx.d.cts +157 -0
- package/dist/fx.d.ts +157 -0
- package/dist/fx.js +374 -0
- package/dist/index.cjs +349 -0
- package/dist/index.d.cts +288 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +276 -0
- package/dist/input.cjs +163 -0
- package/dist/input.d.cts +203 -0
- package/dist/input.d.ts +203 -0
- package/dist/input.js +153 -0
- package/dist/iso-minor--gQWI1f2.cjs +65 -0
- package/dist/iso-minor-CtaiIifs.js +48 -0
- package/dist/medusa-CmxCni-G.js +101 -0
- package/dist/medusa-DuQ3-KoD.d.cts +78 -0
- package/dist/medusa-OJ2TJOxt.cjs +154 -0
- package/dist/medusa-RwqxaDGq.d.ts +78 -0
- package/dist/money-CIgELekG.js +3571 -0
- package/dist/money-CbhsT0fB.d.cts +232 -0
- package/dist/money-CbhsT0fB.d.ts +232 -0
- package/dist/money-D5-vlv0h.cjs +3960 -0
- package/dist/number-text-BpvN6NAo.cjs +145 -0
- package/dist/number-text-C82oh6JB.js +134 -0
- package/dist/own-D7544D3k.js +4 -0
- package/dist/own-oP9DXFWv.cjs +9 -0
- package/dist/percent-CKVI_c-m.js +20 -0
- package/dist/percent-Dt8vNIAW.cjs +43 -0
- package/dist/rate-CmeTGNex.d.ts +63 -0
- package/dist/rate-DGPfB_jc.cjs +217 -0
- package/dist/rate-lLPViBuM.js +152 -0
- package/dist/rate-toyS3EMf.d.cts +63 -0
- package/dist/reporting-5xby-Zas.d.ts +31 -0
- package/dist/reporting-DEM97DhQ.d.cts +31 -0
- package/dist/retail-BsvpNv1F.js +69 -0
- package/dist/retail-BwcFJcVN.d.ts +31 -0
- package/dist/retail-DTk2XAMM.cjs +86 -0
- package/dist/retail-Mqd-sPMF.d.cts +31 -0
- package/dist/rounding-CVAPEfq_.js +50 -0
- package/dist/rounding-oZ832yTD.cjs +79 -0
- package/dist/sql.cjs +290 -0
- package/dist/sql.d.cts +147 -0
- package/dist/sql.d.ts +147 -0
- package/dist/sql.js +271 -0
- package/dist/testing.cjs +28518 -0
- package/dist/testing.d.cts +97 -0
- package/dist/testing.d.ts +97 -0
- package/dist/testing.js +28510 -0
- package/dist/wire-CI3Sm1iC.js +1675 -0
- package/dist/wire-Cd4Q1Jmm.d.ts +269 -0
- package/dist/wire-DQZ6fA3c.cjs +1872 -0
- package/dist/wire-hWuixv3w.d.cts +269 -0
- package/dist/wire.cjs +35 -0
- package/dist/wire.d.cts +2 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +2 -0
- package/package.json +158 -3
|
@@ -0,0 +1,1675 @@
|
|
|
1
|
+
import { C as medusaCurrencyCode, Q as describeValue, Y as mulDiv, Z as MoneyError, c as toMajorDecimal, ct as pow10, dt as toDecimalText, k as storageExponent, lt as roundToScale, n as fromMajor, nt as divRound, o as readStored, rt as formatDecimal, s as storedDecimal, st as parseDecimal, t as boundedMoney, ut as strictNumberDecimal, y as findCurrency } from "./money-CIgELekG.js";
|
|
2
|
+
import { n as allocateUnits } from "./allocate-CdnhvAda.js";
|
|
3
|
+
import { t as hasOwnKey } from "./own-D7544D3k.js";
|
|
4
|
+
import { n as isoMinorFactor } from "./iso-minor-CtaiIifs.js";
|
|
5
|
+
import { n as readNumberText } from "./number-text-C82oh6JB.js";
|
|
6
|
+
//#region src/wire/anet.ts
|
|
7
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8
|
+
/**
|
|
9
|
+
* The first `"<field>": <decimal>` (quoted or not) in a raw JSON body, verbatim: `JSON.parse`
|
|
10
|
+
* would turn the gateway's `43.00` into `43`. The value is the whole JSON token, held to
|
|
11
|
+
* `parseDecimal`'s `'plain'` grammar (`1e3` or `"43.10abc"` is not a decimal literal).
|
|
12
|
+
* Undefined when absent; there is no lossy fallback.
|
|
13
|
+
*/
|
|
14
|
+
function jsonDecimalLiteral(rawBody, field) {
|
|
15
|
+
if (typeof rawBody !== "string" || typeof field !== "string") return void 0;
|
|
16
|
+
const tokens = rawBody.matchAll(new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*("?)([^"\\s,}\\]]+)\\1`, "g"));
|
|
17
|
+
for (const [, , token] of tokens) if (parseDecimal(token, "plain")) return token;
|
|
18
|
+
}
|
|
19
|
+
const isPositiveLiteral = (text) => {
|
|
20
|
+
const value = parseDecimal(text.trim());
|
|
21
|
+
return value !== null && value.units > 0n;
|
|
22
|
+
};
|
|
23
|
+
function authorizeNetReversibleAmount(details) {
|
|
24
|
+
const settle = details.settleAmount ?? void 0;
|
|
25
|
+
if (settle !== void 0 && isPositiveLiteral(settle)) return settle;
|
|
26
|
+
return details.authAmount ?? settle;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/data/psp.ts
|
|
30
|
+
/** developer.paypal.com/reference/currency-codes ("Last updated: August 17, 2026"). */
|
|
31
|
+
const PAYPAL_CURRENCY_DIGITS = {
|
|
32
|
+
0: [
|
|
33
|
+
"HUF",
|
|
34
|
+
"JPY",
|
|
35
|
+
"TWD"
|
|
36
|
+
],
|
|
37
|
+
2: [
|
|
38
|
+
"AUD",
|
|
39
|
+
"BRL",
|
|
40
|
+
"CAD",
|
|
41
|
+
"CHF",
|
|
42
|
+
"CNY",
|
|
43
|
+
"CZK",
|
|
44
|
+
"DKK",
|
|
45
|
+
"EUR",
|
|
46
|
+
"GBP",
|
|
47
|
+
"HKD",
|
|
48
|
+
"ILS",
|
|
49
|
+
"MXN",
|
|
50
|
+
"MYR",
|
|
51
|
+
"NOK",
|
|
52
|
+
"NZD",
|
|
53
|
+
"PHP",
|
|
54
|
+
"PLN",
|
|
55
|
+
"SEK",
|
|
56
|
+
"SGD",
|
|
57
|
+
"THB",
|
|
58
|
+
"USD"
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
/** docs.mollie.com/docs/multicurrency (2026-07-29). */
|
|
62
|
+
const MOLLIE_CURRENCY_DIGITS = {
|
|
63
|
+
0: ["ISK", "JPY"],
|
|
64
|
+
2: [
|
|
65
|
+
"AED",
|
|
66
|
+
"AUD",
|
|
67
|
+
"BRL",
|
|
68
|
+
"CAD",
|
|
69
|
+
"CHF",
|
|
70
|
+
"CZK",
|
|
71
|
+
"DKK",
|
|
72
|
+
"EUR",
|
|
73
|
+
"GBP",
|
|
74
|
+
"HKD",
|
|
75
|
+
"HUF",
|
|
76
|
+
"ILS",
|
|
77
|
+
"MXN",
|
|
78
|
+
"MYR",
|
|
79
|
+
"NOK",
|
|
80
|
+
"NZD",
|
|
81
|
+
"PHP",
|
|
82
|
+
"PLN",
|
|
83
|
+
"RON",
|
|
84
|
+
"RUB",
|
|
85
|
+
"SEK",
|
|
86
|
+
"SGD",
|
|
87
|
+
"THB",
|
|
88
|
+
"TWD",
|
|
89
|
+
"USD",
|
|
90
|
+
"ZAR"
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
const AIRWALLEX_CURRENCY_DIGITS = {
|
|
94
|
+
0: [
|
|
95
|
+
"BIF",
|
|
96
|
+
"CLP",
|
|
97
|
+
"DJF",
|
|
98
|
+
"GNF",
|
|
99
|
+
"ISK",
|
|
100
|
+
"JPY",
|
|
101
|
+
"KMF",
|
|
102
|
+
"KRW",
|
|
103
|
+
"PYG",
|
|
104
|
+
"RWF",
|
|
105
|
+
"SLE",
|
|
106
|
+
"UGX",
|
|
107
|
+
"VND",
|
|
108
|
+
"VUV",
|
|
109
|
+
"XAF",
|
|
110
|
+
"XOF",
|
|
111
|
+
"XPF"
|
|
112
|
+
],
|
|
113
|
+
2: [
|
|
114
|
+
"AED",
|
|
115
|
+
"ALL",
|
|
116
|
+
"AMD",
|
|
117
|
+
"ANG",
|
|
118
|
+
"AOA",
|
|
119
|
+
"ARS",
|
|
120
|
+
"AUD",
|
|
121
|
+
"AWG",
|
|
122
|
+
"AZN",
|
|
123
|
+
"BAM",
|
|
124
|
+
"BBD",
|
|
125
|
+
"BDT",
|
|
126
|
+
"BGN",
|
|
127
|
+
"BMD",
|
|
128
|
+
"BND",
|
|
129
|
+
"BOB",
|
|
130
|
+
"BRL",
|
|
131
|
+
"BSD",
|
|
132
|
+
"BTN",
|
|
133
|
+
"BWP",
|
|
134
|
+
"BZD",
|
|
135
|
+
"CAD",
|
|
136
|
+
"CHF",
|
|
137
|
+
"CNY",
|
|
138
|
+
"COP",
|
|
139
|
+
"CRC",
|
|
140
|
+
"CVE",
|
|
141
|
+
"CZK",
|
|
142
|
+
"DKK",
|
|
143
|
+
"DOP",
|
|
144
|
+
"DZD",
|
|
145
|
+
"EGP",
|
|
146
|
+
"ERN",
|
|
147
|
+
"ETB",
|
|
148
|
+
"EUR",
|
|
149
|
+
"FJD",
|
|
150
|
+
"FKP",
|
|
151
|
+
"GBP",
|
|
152
|
+
"GEL",
|
|
153
|
+
"GHS",
|
|
154
|
+
"GIP",
|
|
155
|
+
"GMD",
|
|
156
|
+
"GTQ",
|
|
157
|
+
"GYD",
|
|
158
|
+
"HKD",
|
|
159
|
+
"HNL",
|
|
160
|
+
"HUF",
|
|
161
|
+
"IDR",
|
|
162
|
+
"ILS",
|
|
163
|
+
"INR",
|
|
164
|
+
"JMD",
|
|
165
|
+
"KES",
|
|
166
|
+
"KGS",
|
|
167
|
+
"KHR",
|
|
168
|
+
"KYD",
|
|
169
|
+
"KZT",
|
|
170
|
+
"LAK",
|
|
171
|
+
"LBP",
|
|
172
|
+
"LKR",
|
|
173
|
+
"LRD",
|
|
174
|
+
"LSL",
|
|
175
|
+
"MAD",
|
|
176
|
+
"MDL",
|
|
177
|
+
"MGA",
|
|
178
|
+
"MKD",
|
|
179
|
+
"MNT",
|
|
180
|
+
"MOP",
|
|
181
|
+
"MUR",
|
|
182
|
+
"MVR",
|
|
183
|
+
"MWK",
|
|
184
|
+
"MXN",
|
|
185
|
+
"MYR",
|
|
186
|
+
"NAD",
|
|
187
|
+
"NGN",
|
|
188
|
+
"NIO",
|
|
189
|
+
"NOK",
|
|
190
|
+
"NPR",
|
|
191
|
+
"NZD",
|
|
192
|
+
"PAB",
|
|
193
|
+
"PEN",
|
|
194
|
+
"PGK",
|
|
195
|
+
"PHP",
|
|
196
|
+
"PKR",
|
|
197
|
+
"PLN",
|
|
198
|
+
"QAR",
|
|
199
|
+
"RON",
|
|
200
|
+
"RSD",
|
|
201
|
+
"SAR",
|
|
202
|
+
"SBD",
|
|
203
|
+
"SCR",
|
|
204
|
+
"SEK",
|
|
205
|
+
"SGD",
|
|
206
|
+
"SHP",
|
|
207
|
+
"SRD",
|
|
208
|
+
"STD",
|
|
209
|
+
"SVC",
|
|
210
|
+
"SZL",
|
|
211
|
+
"THB",
|
|
212
|
+
"TJS",
|
|
213
|
+
"TMT",
|
|
214
|
+
"TOP",
|
|
215
|
+
"TRY",
|
|
216
|
+
"TTD",
|
|
217
|
+
"TWD",
|
|
218
|
+
"TZS",
|
|
219
|
+
"UAH",
|
|
220
|
+
"USD",
|
|
221
|
+
"UYU",
|
|
222
|
+
"UZS",
|
|
223
|
+
"WST",
|
|
224
|
+
"XCD",
|
|
225
|
+
"ZAR",
|
|
226
|
+
"ZMW",
|
|
227
|
+
"ZWL"
|
|
228
|
+
],
|
|
229
|
+
3: [
|
|
230
|
+
"BHD",
|
|
231
|
+
"JOD",
|
|
232
|
+
"KWD",
|
|
233
|
+
"LYD",
|
|
234
|
+
"OMR",
|
|
235
|
+
"TND"
|
|
236
|
+
]
|
|
237
|
+
};
|
|
238
|
+
/** docs.adyen.com/development-resources/currency-codes. Not read by the codec (overrides below); held to it by a test. */
|
|
239
|
+
const ADYEN_CURRENCY_DIGITS = {
|
|
240
|
+
0: [
|
|
241
|
+
"CVE",
|
|
242
|
+
"DJF",
|
|
243
|
+
"GNF",
|
|
244
|
+
"IDR",
|
|
245
|
+
"JPY",
|
|
246
|
+
"KMF",
|
|
247
|
+
"KRW",
|
|
248
|
+
"PYG",
|
|
249
|
+
"RWF",
|
|
250
|
+
"UGX",
|
|
251
|
+
"VND",
|
|
252
|
+
"VUV",
|
|
253
|
+
"XAF",
|
|
254
|
+
"XOF",
|
|
255
|
+
"XPF"
|
|
256
|
+
],
|
|
257
|
+
2: [
|
|
258
|
+
"AED",
|
|
259
|
+
"ALL",
|
|
260
|
+
"AMD",
|
|
261
|
+
"AOA",
|
|
262
|
+
"ARS",
|
|
263
|
+
"AUD",
|
|
264
|
+
"AWG",
|
|
265
|
+
"AZN",
|
|
266
|
+
"BAM",
|
|
267
|
+
"BBD",
|
|
268
|
+
"BDT",
|
|
269
|
+
"BMD",
|
|
270
|
+
"BND",
|
|
271
|
+
"BOB",
|
|
272
|
+
"BRL",
|
|
273
|
+
"BSD",
|
|
274
|
+
"BWP",
|
|
275
|
+
"BYN",
|
|
276
|
+
"BZD",
|
|
277
|
+
"CAD",
|
|
278
|
+
"CHF",
|
|
279
|
+
"CLP",
|
|
280
|
+
"CNH",
|
|
281
|
+
"CNY",
|
|
282
|
+
"COP",
|
|
283
|
+
"CRC",
|
|
284
|
+
"CUP",
|
|
285
|
+
"CZK",
|
|
286
|
+
"DKK",
|
|
287
|
+
"DOP",
|
|
288
|
+
"DZD",
|
|
289
|
+
"EGP",
|
|
290
|
+
"ETB",
|
|
291
|
+
"EUR",
|
|
292
|
+
"FJD",
|
|
293
|
+
"FKP",
|
|
294
|
+
"GBP",
|
|
295
|
+
"GEL",
|
|
296
|
+
"GHS",
|
|
297
|
+
"GIP",
|
|
298
|
+
"GMD",
|
|
299
|
+
"GTQ",
|
|
300
|
+
"GYD",
|
|
301
|
+
"HKD",
|
|
302
|
+
"HNL",
|
|
303
|
+
"HTG",
|
|
304
|
+
"HUF",
|
|
305
|
+
"ILS",
|
|
306
|
+
"INR",
|
|
307
|
+
"ISK",
|
|
308
|
+
"JMD",
|
|
309
|
+
"KES",
|
|
310
|
+
"KGS",
|
|
311
|
+
"KHR",
|
|
312
|
+
"KYD",
|
|
313
|
+
"KZT",
|
|
314
|
+
"LAK",
|
|
315
|
+
"LKR",
|
|
316
|
+
"MAD",
|
|
317
|
+
"MDL",
|
|
318
|
+
"MKD",
|
|
319
|
+
"MMK",
|
|
320
|
+
"MNT",
|
|
321
|
+
"MOP",
|
|
322
|
+
"MRU",
|
|
323
|
+
"MUR",
|
|
324
|
+
"MVR",
|
|
325
|
+
"MWK",
|
|
326
|
+
"MXN",
|
|
327
|
+
"MYR",
|
|
328
|
+
"MZN",
|
|
329
|
+
"NAD",
|
|
330
|
+
"NGN",
|
|
331
|
+
"NIO",
|
|
332
|
+
"NOK",
|
|
333
|
+
"NPR",
|
|
334
|
+
"NZD",
|
|
335
|
+
"PAB",
|
|
336
|
+
"PEN",
|
|
337
|
+
"PGK",
|
|
338
|
+
"PHP",
|
|
339
|
+
"PKR",
|
|
340
|
+
"PLN",
|
|
341
|
+
"QAR",
|
|
342
|
+
"RON",
|
|
343
|
+
"RSD",
|
|
344
|
+
"RUB",
|
|
345
|
+
"SAR",
|
|
346
|
+
"SBD",
|
|
347
|
+
"SCR",
|
|
348
|
+
"SEK",
|
|
349
|
+
"SGD",
|
|
350
|
+
"SHP",
|
|
351
|
+
"SLE",
|
|
352
|
+
"SOS",
|
|
353
|
+
"SRD",
|
|
354
|
+
"STN",
|
|
355
|
+
"SVC",
|
|
356
|
+
"SZL",
|
|
357
|
+
"THB",
|
|
358
|
+
"TOP",
|
|
359
|
+
"TRY",
|
|
360
|
+
"TTD",
|
|
361
|
+
"TWD",
|
|
362
|
+
"TZS",
|
|
363
|
+
"UAH",
|
|
364
|
+
"USD",
|
|
365
|
+
"UYU",
|
|
366
|
+
"UZS",
|
|
367
|
+
"VEF",
|
|
368
|
+
"WST",
|
|
369
|
+
"XCD",
|
|
370
|
+
"XCG",
|
|
371
|
+
"YER",
|
|
372
|
+
"ZAR",
|
|
373
|
+
"ZMW"
|
|
374
|
+
],
|
|
375
|
+
3: [
|
|
376
|
+
"BHD",
|
|
377
|
+
"IQD",
|
|
378
|
+
"JOD",
|
|
379
|
+
"KWD",
|
|
380
|
+
"LYD",
|
|
381
|
+
"OMR",
|
|
382
|
+
"TND"
|
|
383
|
+
]
|
|
384
|
+
};
|
|
385
|
+
/** razorpay.com/docs international currency exponents. */
|
|
386
|
+
const RAZORPAY_CURRENCY_EXPONENTS = {
|
|
387
|
+
0: [
|
|
388
|
+
"BIF",
|
|
389
|
+
"CLP",
|
|
390
|
+
"DJF",
|
|
391
|
+
"GNF",
|
|
392
|
+
"ISK",
|
|
393
|
+
"JPY",
|
|
394
|
+
"KMF",
|
|
395
|
+
"KRW",
|
|
396
|
+
"PYG",
|
|
397
|
+
"RWF",
|
|
398
|
+
"UGX",
|
|
399
|
+
"VND",
|
|
400
|
+
"VUV",
|
|
401
|
+
"XAF",
|
|
402
|
+
"XOF",
|
|
403
|
+
"XPF"
|
|
404
|
+
],
|
|
405
|
+
2: [
|
|
406
|
+
"AED",
|
|
407
|
+
"ALL",
|
|
408
|
+
"AMD",
|
|
409
|
+
"AUD",
|
|
410
|
+
"AWG",
|
|
411
|
+
"AZN",
|
|
412
|
+
"BAM",
|
|
413
|
+
"BBD",
|
|
414
|
+
"BDT",
|
|
415
|
+
"BGN",
|
|
416
|
+
"BMD",
|
|
417
|
+
"BND",
|
|
418
|
+
"BOB",
|
|
419
|
+
"BRL",
|
|
420
|
+
"BSD",
|
|
421
|
+
"BTN",
|
|
422
|
+
"BWP",
|
|
423
|
+
"BZD",
|
|
424
|
+
"CAD",
|
|
425
|
+
"CHF",
|
|
426
|
+
"CNY",
|
|
427
|
+
"COP",
|
|
428
|
+
"CRC",
|
|
429
|
+
"CUP",
|
|
430
|
+
"CVE",
|
|
431
|
+
"CZK",
|
|
432
|
+
"DKK",
|
|
433
|
+
"DOP",
|
|
434
|
+
"DZD",
|
|
435
|
+
"EGP",
|
|
436
|
+
"ETB",
|
|
437
|
+
"EUR",
|
|
438
|
+
"FJD",
|
|
439
|
+
"GBP",
|
|
440
|
+
"GHS",
|
|
441
|
+
"GIP",
|
|
442
|
+
"GMD",
|
|
443
|
+
"GTQ",
|
|
444
|
+
"GYD",
|
|
445
|
+
"HKD",
|
|
446
|
+
"HNL",
|
|
447
|
+
"HRK",
|
|
448
|
+
"HTG",
|
|
449
|
+
"HUF",
|
|
450
|
+
"IDR",
|
|
451
|
+
"ILS",
|
|
452
|
+
"INR",
|
|
453
|
+
"JMD",
|
|
454
|
+
"KES",
|
|
455
|
+
"KGS",
|
|
456
|
+
"KHR",
|
|
457
|
+
"KYD",
|
|
458
|
+
"KZT",
|
|
459
|
+
"LAK",
|
|
460
|
+
"LKR",
|
|
461
|
+
"LRD",
|
|
462
|
+
"LSL",
|
|
463
|
+
"MAD",
|
|
464
|
+
"MDL",
|
|
465
|
+
"MGA",
|
|
466
|
+
"MKD",
|
|
467
|
+
"MMK",
|
|
468
|
+
"MNT",
|
|
469
|
+
"MOP",
|
|
470
|
+
"MUR",
|
|
471
|
+
"MVR",
|
|
472
|
+
"MWK",
|
|
473
|
+
"MXN",
|
|
474
|
+
"MYR",
|
|
475
|
+
"MZN",
|
|
476
|
+
"NAD",
|
|
477
|
+
"NGN",
|
|
478
|
+
"NIO",
|
|
479
|
+
"NOK",
|
|
480
|
+
"NPR",
|
|
481
|
+
"NZD",
|
|
482
|
+
"PEN",
|
|
483
|
+
"PGK",
|
|
484
|
+
"PHP",
|
|
485
|
+
"PKR",
|
|
486
|
+
"PLN",
|
|
487
|
+
"QAR",
|
|
488
|
+
"RON",
|
|
489
|
+
"RSD",
|
|
490
|
+
"RUB",
|
|
491
|
+
"SAR",
|
|
492
|
+
"SCR",
|
|
493
|
+
"SEK",
|
|
494
|
+
"SGD",
|
|
495
|
+
"SLL",
|
|
496
|
+
"SOS",
|
|
497
|
+
"SVC",
|
|
498
|
+
"SZL",
|
|
499
|
+
"THB",
|
|
500
|
+
"TRY",
|
|
501
|
+
"TTD",
|
|
502
|
+
"TWD",
|
|
503
|
+
"TZS",
|
|
504
|
+
"UAH",
|
|
505
|
+
"USD",
|
|
506
|
+
"UYU",
|
|
507
|
+
"UZS",
|
|
508
|
+
"XCD",
|
|
509
|
+
"YER",
|
|
510
|
+
"ZAR",
|
|
511
|
+
"ZMW"
|
|
512
|
+
],
|
|
513
|
+
3: [
|
|
514
|
+
"BHD",
|
|
515
|
+
"IQD",
|
|
516
|
+
"JOD",
|
|
517
|
+
"KWD",
|
|
518
|
+
"OMR",
|
|
519
|
+
"TND"
|
|
520
|
+
]
|
|
521
|
+
};
|
|
522
|
+
/** Paystack: "If we notice a fractional part during processing, we ignore the fractional part." */
|
|
523
|
+
const PAYSTACK_CURRENCY_DIGITS = { 2: [
|
|
524
|
+
"GHS",
|
|
525
|
+
"KES",
|
|
526
|
+
"NGN",
|
|
527
|
+
"USD",
|
|
528
|
+
"XOF",
|
|
529
|
+
"ZAR"
|
|
530
|
+
] };
|
|
531
|
+
/** Factor-1 overrides: ISO zero-decimal codes the PSP wants at two decimals (storage and wire agree). */
|
|
532
|
+
const STRIPE_SENT_AS_TWO_DECIMAL = ["ISK", "UGX"];
|
|
533
|
+
const ADYEN_SENT_AS_TWO_DECIMAL = ["ISK", "CLP"];
|
|
534
|
+
const RAZORPAY_SENT_AS_TWO_DECIMAL = ["MGA"];
|
|
535
|
+
const PAYSTACK_SENT_AS_TWO_DECIMAL = ["XOF"];
|
|
536
|
+
const VNPAY_SENT_AS_TWO_DECIMAL = ["VND"];
|
|
537
|
+
/** Factor-100 override: ISO two-decimal codes Adyen wants in whole units. */
|
|
538
|
+
const ADYEN_SENT_AS_ZERO_DECIMAL = ["IDR", "CVE"];
|
|
539
|
+
/** docs.stripe.com/currencies retrieved 2026-10-07 (union over 45 countries; IQD, LYD absent). */
|
|
540
|
+
const STRIPE_PRESENTMENT_CURRENCIES = [
|
|
541
|
+
"AED",
|
|
542
|
+
"AFN",
|
|
543
|
+
"ALL",
|
|
544
|
+
"AMD",
|
|
545
|
+
"ANG",
|
|
546
|
+
"AOA",
|
|
547
|
+
"ARS",
|
|
548
|
+
"AUD",
|
|
549
|
+
"AWG",
|
|
550
|
+
"AZN",
|
|
551
|
+
"BAM",
|
|
552
|
+
"BBD",
|
|
553
|
+
"BDT",
|
|
554
|
+
"BHD",
|
|
555
|
+
"BIF",
|
|
556
|
+
"BMD",
|
|
557
|
+
"BND",
|
|
558
|
+
"BOB",
|
|
559
|
+
"BRL",
|
|
560
|
+
"BSD",
|
|
561
|
+
"BWP",
|
|
562
|
+
"BYN",
|
|
563
|
+
"BZD",
|
|
564
|
+
"CAD",
|
|
565
|
+
"CDF",
|
|
566
|
+
"CHF",
|
|
567
|
+
"CLP",
|
|
568
|
+
"CNY",
|
|
569
|
+
"COP",
|
|
570
|
+
"CRC",
|
|
571
|
+
"CVE",
|
|
572
|
+
"CZK",
|
|
573
|
+
"DJF",
|
|
574
|
+
"DKK",
|
|
575
|
+
"DOP",
|
|
576
|
+
"DZD",
|
|
577
|
+
"EGP",
|
|
578
|
+
"ETB",
|
|
579
|
+
"EUR",
|
|
580
|
+
"FJD",
|
|
581
|
+
"FKP",
|
|
582
|
+
"GBP",
|
|
583
|
+
"GEL",
|
|
584
|
+
"GIP",
|
|
585
|
+
"GMD",
|
|
586
|
+
"GNF",
|
|
587
|
+
"GTQ",
|
|
588
|
+
"GYD",
|
|
589
|
+
"HKD",
|
|
590
|
+
"HNL",
|
|
591
|
+
"HTG",
|
|
592
|
+
"HUF",
|
|
593
|
+
"IDR",
|
|
594
|
+
"ILS",
|
|
595
|
+
"INR",
|
|
596
|
+
"ISK",
|
|
597
|
+
"JMD",
|
|
598
|
+
"JOD",
|
|
599
|
+
"JPY",
|
|
600
|
+
"KES",
|
|
601
|
+
"KGS",
|
|
602
|
+
"KHR",
|
|
603
|
+
"KMF",
|
|
604
|
+
"KRW",
|
|
605
|
+
"KWD",
|
|
606
|
+
"KYD",
|
|
607
|
+
"KZT",
|
|
608
|
+
"LAK",
|
|
609
|
+
"LBP",
|
|
610
|
+
"LKR",
|
|
611
|
+
"LRD",
|
|
612
|
+
"LSL",
|
|
613
|
+
"MAD",
|
|
614
|
+
"MDL",
|
|
615
|
+
"MGA",
|
|
616
|
+
"MKD",
|
|
617
|
+
"MMK",
|
|
618
|
+
"MNT",
|
|
619
|
+
"MOP",
|
|
620
|
+
"MUR",
|
|
621
|
+
"MVR",
|
|
622
|
+
"MWK",
|
|
623
|
+
"MXN",
|
|
624
|
+
"MYR",
|
|
625
|
+
"MZN",
|
|
626
|
+
"NAD",
|
|
627
|
+
"NGN",
|
|
628
|
+
"NIO",
|
|
629
|
+
"NOK",
|
|
630
|
+
"NPR",
|
|
631
|
+
"NZD",
|
|
632
|
+
"OMR",
|
|
633
|
+
"PAB",
|
|
634
|
+
"PEN",
|
|
635
|
+
"PGK",
|
|
636
|
+
"PHP",
|
|
637
|
+
"PKR",
|
|
638
|
+
"PLN",
|
|
639
|
+
"PYG",
|
|
640
|
+
"QAR",
|
|
641
|
+
"RON",
|
|
642
|
+
"RSD",
|
|
643
|
+
"RUB",
|
|
644
|
+
"RWF",
|
|
645
|
+
"SAR",
|
|
646
|
+
"SBD",
|
|
647
|
+
"SCR",
|
|
648
|
+
"SEK",
|
|
649
|
+
"SGD",
|
|
650
|
+
"SHP",
|
|
651
|
+
"SLE",
|
|
652
|
+
"SOS",
|
|
653
|
+
"SRD",
|
|
654
|
+
"STD",
|
|
655
|
+
"SZL",
|
|
656
|
+
"THB",
|
|
657
|
+
"TJS",
|
|
658
|
+
"TND",
|
|
659
|
+
"TOP",
|
|
660
|
+
"TRY",
|
|
661
|
+
"TTD",
|
|
662
|
+
"TWD",
|
|
663
|
+
"TZS",
|
|
664
|
+
"UAH",
|
|
665
|
+
"UGX",
|
|
666
|
+
"USD",
|
|
667
|
+
"UYU",
|
|
668
|
+
"UZS",
|
|
669
|
+
"VND",
|
|
670
|
+
"VUV",
|
|
671
|
+
"WST",
|
|
672
|
+
"XAF",
|
|
673
|
+
"XCD",
|
|
674
|
+
"XCG",
|
|
675
|
+
"XOF",
|
|
676
|
+
"XPF",
|
|
677
|
+
"YER",
|
|
678
|
+
"ZAR",
|
|
679
|
+
"ZMW"
|
|
680
|
+
];
|
|
681
|
+
/** Stripe's not-two-decimal set (capabilities/stripe-connect.ts:47), incl. the UAE-only five. */
|
|
682
|
+
const STRIPE_NOT_TWO_DECIMAL = [
|
|
683
|
+
"BIF",
|
|
684
|
+
"CLP",
|
|
685
|
+
"DJF",
|
|
686
|
+
"GNF",
|
|
687
|
+
"JPY",
|
|
688
|
+
"KMF",
|
|
689
|
+
"KRW",
|
|
690
|
+
"MGA",
|
|
691
|
+
"PYG",
|
|
692
|
+
"RWF",
|
|
693
|
+
"UGX",
|
|
694
|
+
"VND",
|
|
695
|
+
"VUV",
|
|
696
|
+
"XAF",
|
|
697
|
+
"XOF",
|
|
698
|
+
"XPF",
|
|
699
|
+
"ISK",
|
|
700
|
+
"BHD",
|
|
701
|
+
"JOD",
|
|
702
|
+
"KWD",
|
|
703
|
+
"OMR",
|
|
704
|
+
"TND"
|
|
705
|
+
];
|
|
706
|
+
/**
|
|
707
|
+
* The ISO 4217 set Stripe's sendable rule is derived over (medusa common/money/iso-currencies.ts,
|
|
708
|
+
* 156 codes). Kept verbatim rather than read from the registry's active list: it still holds BGN,
|
|
709
|
+
* which the registry retired, and Stripe still accepts it.
|
|
710
|
+
*/
|
|
711
|
+
const STRIPE_SENDABLE_ISO_BASE = [
|
|
712
|
+
"AED",
|
|
713
|
+
"AFN",
|
|
714
|
+
"ALL",
|
|
715
|
+
"AMD",
|
|
716
|
+
"AOA",
|
|
717
|
+
"ARS",
|
|
718
|
+
"AUD",
|
|
719
|
+
"AWG",
|
|
720
|
+
"AZN",
|
|
721
|
+
"BAM",
|
|
722
|
+
"BBD",
|
|
723
|
+
"BDT",
|
|
724
|
+
"BGN",
|
|
725
|
+
"BHD",
|
|
726
|
+
"BIF",
|
|
727
|
+
"BMD",
|
|
728
|
+
"BND",
|
|
729
|
+
"BOB",
|
|
730
|
+
"BRL",
|
|
731
|
+
"BSD",
|
|
732
|
+
"BTN",
|
|
733
|
+
"BWP",
|
|
734
|
+
"BYN",
|
|
735
|
+
"BZD",
|
|
736
|
+
"CAD",
|
|
737
|
+
"CDF",
|
|
738
|
+
"CHF",
|
|
739
|
+
"CLP",
|
|
740
|
+
"CNY",
|
|
741
|
+
"COP",
|
|
742
|
+
"CRC",
|
|
743
|
+
"CUP",
|
|
744
|
+
"CVE",
|
|
745
|
+
"CZK",
|
|
746
|
+
"DJF",
|
|
747
|
+
"DKK",
|
|
748
|
+
"DOP",
|
|
749
|
+
"DZD",
|
|
750
|
+
"EGP",
|
|
751
|
+
"ERN",
|
|
752
|
+
"ETB",
|
|
753
|
+
"EUR",
|
|
754
|
+
"FJD",
|
|
755
|
+
"FKP",
|
|
756
|
+
"GBP",
|
|
757
|
+
"GEL",
|
|
758
|
+
"GHS",
|
|
759
|
+
"GIP",
|
|
760
|
+
"GMD",
|
|
761
|
+
"GNF",
|
|
762
|
+
"GTQ",
|
|
763
|
+
"GYD",
|
|
764
|
+
"HKD",
|
|
765
|
+
"HNL",
|
|
766
|
+
"HTG",
|
|
767
|
+
"HUF",
|
|
768
|
+
"IDR",
|
|
769
|
+
"ILS",
|
|
770
|
+
"INR",
|
|
771
|
+
"IQD",
|
|
772
|
+
"IRR",
|
|
773
|
+
"ISK",
|
|
774
|
+
"JMD",
|
|
775
|
+
"JOD",
|
|
776
|
+
"JPY",
|
|
777
|
+
"KES",
|
|
778
|
+
"KGS",
|
|
779
|
+
"KHR",
|
|
780
|
+
"KMF",
|
|
781
|
+
"KPW",
|
|
782
|
+
"KRW",
|
|
783
|
+
"KWD",
|
|
784
|
+
"KYD",
|
|
785
|
+
"KZT",
|
|
786
|
+
"LAK",
|
|
787
|
+
"LBP",
|
|
788
|
+
"LKR",
|
|
789
|
+
"LRD",
|
|
790
|
+
"LSL",
|
|
791
|
+
"LYD",
|
|
792
|
+
"MAD",
|
|
793
|
+
"MDL",
|
|
794
|
+
"MGA",
|
|
795
|
+
"MKD",
|
|
796
|
+
"MMK",
|
|
797
|
+
"MNT",
|
|
798
|
+
"MOP",
|
|
799
|
+
"MRU",
|
|
800
|
+
"MUR",
|
|
801
|
+
"MVR",
|
|
802
|
+
"MWK",
|
|
803
|
+
"MXN",
|
|
804
|
+
"MYR",
|
|
805
|
+
"MZN",
|
|
806
|
+
"NAD",
|
|
807
|
+
"NGN",
|
|
808
|
+
"NIO",
|
|
809
|
+
"NOK",
|
|
810
|
+
"NPR",
|
|
811
|
+
"NZD",
|
|
812
|
+
"OMR",
|
|
813
|
+
"PAB",
|
|
814
|
+
"PEN",
|
|
815
|
+
"PGK",
|
|
816
|
+
"PHP",
|
|
817
|
+
"PKR",
|
|
818
|
+
"PLN",
|
|
819
|
+
"PYG",
|
|
820
|
+
"QAR",
|
|
821
|
+
"RON",
|
|
822
|
+
"RSD",
|
|
823
|
+
"RUB",
|
|
824
|
+
"RWF",
|
|
825
|
+
"SAR",
|
|
826
|
+
"SBD",
|
|
827
|
+
"SCR",
|
|
828
|
+
"SDG",
|
|
829
|
+
"SEK",
|
|
830
|
+
"SGD",
|
|
831
|
+
"SHP",
|
|
832
|
+
"SLE",
|
|
833
|
+
"SOS",
|
|
834
|
+
"SRD",
|
|
835
|
+
"SSP",
|
|
836
|
+
"STN",
|
|
837
|
+
"SVC",
|
|
838
|
+
"SYP",
|
|
839
|
+
"SZL",
|
|
840
|
+
"THB",
|
|
841
|
+
"TJS",
|
|
842
|
+
"TMT",
|
|
843
|
+
"TND",
|
|
844
|
+
"TOP",
|
|
845
|
+
"TRY",
|
|
846
|
+
"TTD",
|
|
847
|
+
"TWD",
|
|
848
|
+
"TZS",
|
|
849
|
+
"UAH",
|
|
850
|
+
"UGX",
|
|
851
|
+
"USD",
|
|
852
|
+
"UYU",
|
|
853
|
+
"UZS",
|
|
854
|
+
"VED",
|
|
855
|
+
"VES",
|
|
856
|
+
"VND",
|
|
857
|
+
"VUV",
|
|
858
|
+
"WST",
|
|
859
|
+
"XAF",
|
|
860
|
+
"XCD",
|
|
861
|
+
"XCG",
|
|
862
|
+
"XOF",
|
|
863
|
+
"XPF",
|
|
864
|
+
"YER",
|
|
865
|
+
"ZAR",
|
|
866
|
+
"ZMW",
|
|
867
|
+
"ZWG"
|
|
868
|
+
];
|
|
869
|
+
const IYZICO_CURRENCIES = [
|
|
870
|
+
"TRY",
|
|
871
|
+
"USD",
|
|
872
|
+
"EUR",
|
|
873
|
+
"GBP",
|
|
874
|
+
"NOK",
|
|
875
|
+
"CHF"
|
|
876
|
+
];
|
|
877
|
+
/** developer.authorize.net createTransactionRequest `currencyCode`, retrieved 2026-10-07. */
|
|
878
|
+
const AUTHORIZE_NET_CURRENCIES = [
|
|
879
|
+
"USD",
|
|
880
|
+
"CAD",
|
|
881
|
+
"GBP",
|
|
882
|
+
"EUR",
|
|
883
|
+
"AUD",
|
|
884
|
+
"NZD",
|
|
885
|
+
"DKK",
|
|
886
|
+
"NOK",
|
|
887
|
+
"SEK",
|
|
888
|
+
"PLN"
|
|
889
|
+
];
|
|
890
|
+
const VND_ONLY = ["VND"];
|
|
891
|
+
/** orch `MINIMUM_AMOUNT_BY_CURRENCY`, majors. */
|
|
892
|
+
const PSP_MINIMUM_MAJOR = { paystack: { USD: "2.00" } };
|
|
893
|
+
/** RBI e-mandate AFA-free cap: INR 15,000 (razorpay-vault `15_000_00` paise, seam `1_500_000`). */
|
|
894
|
+
const RAZORPAY_AFA_FREE_CAP_MAJOR = "15000";
|
|
895
|
+
/**
|
|
896
|
+
* Provider-only codes: currencies a PSP table lists that are not ISO 4217 registry entries (and
|
|
897
|
+
* not Medusa store currencies), so no Amboras store can sell in them and no Money can carry them.
|
|
898
|
+
* /wire knows them only to refuse them by name: `pspSupportsCurrency` is false, `pspCurrencies`
|
|
899
|
+
* leaves them out, and decoding a provider-reported amount in one throws `wire_unsupported`.
|
|
900
|
+
*/
|
|
901
|
+
const PROVIDER_ONLY_CURRENCIES = { CNH: {
|
|
902
|
+
name: "Chinese Yuan (offshore)",
|
|
903
|
+
psps: ["adyen"]
|
|
904
|
+
} };
|
|
905
|
+
//#endregion
|
|
906
|
+
//#region src/wire/currency.ts
|
|
907
|
+
/** The registry entry (any case, historic included) for `input`, else null. Never throws. */
|
|
908
|
+
function wireCurrency(input) {
|
|
909
|
+
return findCurrency(input, { allowHistoric: true });
|
|
910
|
+
}
|
|
911
|
+
/** The upper-cased code when `input` is a provider-only currency (Adyen CNH), else null. */
|
|
912
|
+
function providerOnlyCode(input) {
|
|
913
|
+
if (typeof input !== "string") return null;
|
|
914
|
+
const code = input.trim().toUpperCase();
|
|
915
|
+
return hasOwnKey(PROVIDER_ONLY_CURRENCIES, code) ? code : null;
|
|
916
|
+
}
|
|
917
|
+
/** `[<Tag>] CNH is a provider-only currency code ...`: why a provider-only amount is refused. */
|
|
918
|
+
function providerOnlyRefusal(tag, code) {
|
|
919
|
+
return `[${tag}] ${code} is a provider-only currency code (${PROVIDER_ONLY_CURRENCIES[code].name}): ${tag} can report it, but it is not an ISO 4217 currency, no Amboras store sells in it, and no Money can hold it.`;
|
|
920
|
+
}
|
|
921
|
+
/** `wireCurrency`, throwing `currency_invalid` for an unknown code. */
|
|
922
|
+
function resolveWireCurrency(input) {
|
|
923
|
+
const info = wireCurrency(input);
|
|
924
|
+
if (!info) throw new MoneyError("currency_invalid", `Unknown currency code: ${describeValue(input)}`);
|
|
925
|
+
return info;
|
|
926
|
+
}
|
|
927
|
+
/** A Money's currency entry and its stored units, validated like the core's `storedDecimal`. */
|
|
928
|
+
function wireStored(m) {
|
|
929
|
+
const info = resolveWireCurrency(m.currency);
|
|
930
|
+
if (!Number.isSafeInteger(m.amount)) throw new MoneyError("amount_invalid", `Stored amount must be a safe integer, got ${describeValue(m.amount)}`);
|
|
931
|
+
return {
|
|
932
|
+
info,
|
|
933
|
+
units: BigInt(m.amount)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/wire/descriptors.ts
|
|
938
|
+
function digitTable(groups) {
|
|
939
|
+
const table = /* @__PURE__ */ new Map();
|
|
940
|
+
for (const digits of [
|
|
941
|
+
0,
|
|
942
|
+
2,
|
|
943
|
+
3
|
|
944
|
+
]) for (const code of groups[digits] ?? []) table.set(code, digits);
|
|
945
|
+
return table;
|
|
946
|
+
}
|
|
947
|
+
function genericUnsupported(tag) {
|
|
948
|
+
return (code) => `[${tag}] ${code} is not in ${tag}'s supported currency list.`;
|
|
949
|
+
}
|
|
950
|
+
function vndOnly(tag, subject, negation) {
|
|
951
|
+
return (code) => {
|
|
952
|
+
return `[${tag}] ${tag} only supports VND - ${subject} ${code}. Switch the region's currency to VND, or ${negation} offer ${tag} in this region.`;
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
const scalesAnyCurrency = () => true;
|
|
956
|
+
const vndOnlyFacade = (code) => code === "VND";
|
|
957
|
+
const NONE = /* @__PURE__ */ new Set();
|
|
958
|
+
const NO_DIGITS = /* @__PURE__ */ new Map();
|
|
959
|
+
function describe(id, input) {
|
|
960
|
+
return Object.freeze({
|
|
961
|
+
id,
|
|
962
|
+
tag: input.tag,
|
|
963
|
+
family: input.family,
|
|
964
|
+
gated: input.gated ?? false,
|
|
965
|
+
encodeGated: input.encodeGated ?? false,
|
|
966
|
+
preCutoverNeedsFactorOne: input.preCutoverNeedsFactorOne ?? false,
|
|
967
|
+
currencies: new Set(input.currencies),
|
|
968
|
+
digits: input.digits ?? NO_DIGITS,
|
|
969
|
+
sentAsTwoDecimal: input.sentAsTwoDecimal ? new Set(input.sentAsTwoDecimal) : NONE,
|
|
970
|
+
sentAsZeroDecimal: input.sentAsZeroDecimal ? new Set(input.sentAsZeroDecimal) : NONE,
|
|
971
|
+
threeDecimalStepOfTen: input.threeDecimalStepOfTen ?? false,
|
|
972
|
+
wholeUnit: input.wholeUnit ?? null,
|
|
973
|
+
singleCurrency: input.singleCurrency ?? null,
|
|
974
|
+
unsupported: input.unsupported ?? genericUnsupported(input.tag),
|
|
975
|
+
facadeAccepts: input.facadeAccepts ?? scalesAnyCurrency,
|
|
976
|
+
inboundWholePositive: input.inboundWholePositive ?? false
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
const notTwoDecimalAtStripe = new Set(STRIPE_NOT_TWO_DECIMAL);
|
|
980
|
+
/**
|
|
981
|
+
* Stripe's presentment list plus every ISO code that is two-decimal in both storage and display
|
|
982
|
+
* and not on Stripe's not-two-decimal list (capabilities/stripe-connect.ts:60).
|
|
983
|
+
*/
|
|
984
|
+
const STRIPE_SENDABLE = [...STRIPE_PRESENTMENT_CURRENCIES, ...STRIPE_SENDABLE_ISO_BASE.filter((code) => {
|
|
985
|
+
const info = findCurrency(code, { allowHistoric: true });
|
|
986
|
+
return info !== null && !notTwoDecimalAtStripe.has(code) && info.storageDigits === 2 && info.displayDigits === 2;
|
|
987
|
+
})];
|
|
988
|
+
const whole = (info, digits) => info.storageDigits === digits;
|
|
989
|
+
function paystackZeroDecimal(code) {
|
|
990
|
+
return `[Paystack] ${code} is zero-decimal and Amboras stores it at a x100 scale, so it cannot be sent to Paystack without a currency-aware amount codec. Charging it would overcharge the buyer 100x. Verify the currency against Paystack docs and add an override set before enabling it.`;
|
|
991
|
+
}
|
|
992
|
+
const RAZORPAY_DIGITS = digitTable(RAZORPAY_CURRENCY_EXPONENTS);
|
|
993
|
+
const PSP_DESCRIPTORS = Object.freeze({
|
|
994
|
+
stripe: describe("stripe", {
|
|
995
|
+
tag: "StripeConnect",
|
|
996
|
+
family: "iso-minor",
|
|
997
|
+
gated: true,
|
|
998
|
+
encodeGated: true,
|
|
999
|
+
currencies: STRIPE_SENDABLE,
|
|
1000
|
+
sentAsTwoDecimal: STRIPE_SENT_AS_TWO_DECIMAL
|
|
1001
|
+
}),
|
|
1002
|
+
adyen: describe("adyen", {
|
|
1003
|
+
tag: "Adyen",
|
|
1004
|
+
family: "iso-minor",
|
|
1005
|
+
gated: true,
|
|
1006
|
+
encodeGated: true,
|
|
1007
|
+
currencies: digitTable(ADYEN_CURRENCY_DIGITS).keys(),
|
|
1008
|
+
digits: digitTable(ADYEN_CURRENCY_DIGITS),
|
|
1009
|
+
sentAsTwoDecimal: ADYEN_SENT_AS_TWO_DECIMAL,
|
|
1010
|
+
sentAsZeroDecimal: ADYEN_SENT_AS_ZERO_DECIMAL
|
|
1011
|
+
}),
|
|
1012
|
+
razorpay: describe("razorpay", {
|
|
1013
|
+
tag: "Razorpay",
|
|
1014
|
+
family: "iso-minor",
|
|
1015
|
+
gated: true,
|
|
1016
|
+
encodeGated: true,
|
|
1017
|
+
preCutoverNeedsFactorOne: true,
|
|
1018
|
+
threeDecimalStepOfTen: true,
|
|
1019
|
+
currencies: RAZORPAY_DIGITS.keys(),
|
|
1020
|
+
digits: RAZORPAY_DIGITS,
|
|
1021
|
+
sentAsTwoDecimal: RAZORPAY_SENT_AS_TWO_DECIMAL,
|
|
1022
|
+
facadeAccepts: (code) => RAZORPAY_DIGITS.has(code),
|
|
1023
|
+
unsupported: (code) => {
|
|
1024
|
+
return `[Razorpay] ${code} is not in Razorpay's published currency table, so it has no verified minor-unit exponent and cannot be charged through Razorpay.`;
|
|
1025
|
+
}
|
|
1026
|
+
}),
|
|
1027
|
+
paystack: describe("paystack", {
|
|
1028
|
+
tag: "Paystack",
|
|
1029
|
+
family: "iso-minor",
|
|
1030
|
+
gated: true,
|
|
1031
|
+
currencies: digitTable(PAYSTACK_CURRENCY_DIGITS).keys(),
|
|
1032
|
+
digits: digitTable(PAYSTACK_CURRENCY_DIGITS),
|
|
1033
|
+
sentAsTwoDecimal: PAYSTACK_SENT_AS_TWO_DECIMAL,
|
|
1034
|
+
facadeAccepts: (code, info) => info === null || !whole(info, 0) || PAYSTACK_SENT_AS_TWO_DECIMAL.includes(code),
|
|
1035
|
+
wholeUnit: {
|
|
1036
|
+
applies: (info) => PAYSTACK_SENT_AS_TWO_DECIMAL.includes(info.code),
|
|
1037
|
+
message: (info, _stored, _label, major) => {
|
|
1038
|
+
return `[Paystack] ${info.code} ${major} cannot be charged: Paystack takes a whole number of francs and ignores any fraction.`;
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
unsupported: (code, info) => info !== null && whole(info, 0) ? paystackZeroDecimal(code) : genericUnsupported("Paystack")(code)
|
|
1042
|
+
}),
|
|
1043
|
+
momo: describe("momo", {
|
|
1044
|
+
tag: "MoMo",
|
|
1045
|
+
family: "iso-minor",
|
|
1046
|
+
gated: true,
|
|
1047
|
+
currencies: VND_ONLY,
|
|
1048
|
+
singleCurrency: "VND",
|
|
1049
|
+
facadeAccepts: vndOnlyFacade,
|
|
1050
|
+
wholeUnit: {
|
|
1051
|
+
applies: () => true,
|
|
1052
|
+
message: (_info, stored, label) => {
|
|
1053
|
+
return `[MoMo] VND ${label} must resolve to a whole dong value after division by 100 (Medusa stores VND at that scale): ${stored}`;
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
unsupported: vndOnly("MoMo", "this amount is", "do not")
|
|
1057
|
+
}),
|
|
1058
|
+
vnpay: describe("vnpay", {
|
|
1059
|
+
tag: "VNPay",
|
|
1060
|
+
family: "iso-minor",
|
|
1061
|
+
currencies: VND_ONLY,
|
|
1062
|
+
singleCurrency: "VND",
|
|
1063
|
+
facadeAccepts: vndOnlyFacade,
|
|
1064
|
+
sentAsTwoDecimal: VNPAY_SENT_AS_TWO_DECIMAL,
|
|
1065
|
+
inboundWholePositive: true,
|
|
1066
|
+
wholeUnit: {
|
|
1067
|
+
applies: () => true,
|
|
1068
|
+
message: (_info, stored, label) => `[VNPay] VND ${label} must resolve to a whole dong value: ${stored}`
|
|
1069
|
+
},
|
|
1070
|
+
unsupported: vndOnly("VNPay", "this store's region currency is", "don't")
|
|
1071
|
+
}),
|
|
1072
|
+
paypal: describe("paypal", {
|
|
1073
|
+
tag: "PayPal",
|
|
1074
|
+
family: "major-string",
|
|
1075
|
+
currencies: digitTable(PAYPAL_CURRENCY_DIGITS).keys(),
|
|
1076
|
+
digits: digitTable(PAYPAL_CURRENCY_DIGITS)
|
|
1077
|
+
}),
|
|
1078
|
+
mollie: describe("mollie", {
|
|
1079
|
+
tag: "Mollie",
|
|
1080
|
+
family: "major-string",
|
|
1081
|
+
currencies: digitTable(MOLLIE_CURRENCY_DIGITS).keys(),
|
|
1082
|
+
digits: digitTable(MOLLIE_CURRENCY_DIGITS)
|
|
1083
|
+
}),
|
|
1084
|
+
airwallex: describe("airwallex", {
|
|
1085
|
+
tag: "Airwallex",
|
|
1086
|
+
family: "major-number",
|
|
1087
|
+
currencies: digitTable(AIRWALLEX_CURRENCY_DIGITS).keys(),
|
|
1088
|
+
digits: digitTable(AIRWALLEX_CURRENCY_DIGITS)
|
|
1089
|
+
}),
|
|
1090
|
+
iyzico: describe("iyzico", {
|
|
1091
|
+
tag: "iyzico",
|
|
1092
|
+
family: "major-string",
|
|
1093
|
+
currencies: IYZICO_CURRENCIES,
|
|
1094
|
+
unsupported: (code) => {
|
|
1095
|
+
return `[iyzico] Unsupported currency ${code}. Supported currencies: ${IYZICO_CURRENCIES.join(", ")}.`;
|
|
1096
|
+
}
|
|
1097
|
+
}),
|
|
1098
|
+
"authorize-net": describe("authorize-net", {
|
|
1099
|
+
tag: "authorize-net",
|
|
1100
|
+
family: "major-string",
|
|
1101
|
+
currencies: AUTHORIZE_NET_CURRENCIES,
|
|
1102
|
+
unsupported: (code) => `Authorize.Net does not settle ${code}.`
|
|
1103
|
+
}),
|
|
1104
|
+
payoo: describe("payoo", {
|
|
1105
|
+
tag: "Payoo",
|
|
1106
|
+
family: "major-number",
|
|
1107
|
+
currencies: VND_ONLY,
|
|
1108
|
+
singleCurrency: "VND",
|
|
1109
|
+
facadeAccepts: vndOnlyFacade,
|
|
1110
|
+
wholeUnit: {
|
|
1111
|
+
applies: () => true,
|
|
1112
|
+
message: (_info, stored) => {
|
|
1113
|
+
return `[Payoo] VND amount must resolve to a whole dong value after /100 (Medusa stores VND x100): ${stored}`;
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
unsupported: vndOnly("Payoo", "this store's region currency is", "don't")
|
|
1117
|
+
})
|
|
1118
|
+
});
|
|
1119
|
+
/** Every PSP id, in a stable order. */
|
|
1120
|
+
const PSP_IDS = Object.freeze(Object.keys(PSP_DESCRIPTORS));
|
|
1121
|
+
/** The descriptor for `psp`; an unknown id throws `wire_unsupported`. */
|
|
1122
|
+
function pspDescriptor(psp) {
|
|
1123
|
+
if (typeof psp === "string" && hasOwnKey(PSP_DESCRIPTORS, psp)) return PSP_DESCRIPTORS[psp];
|
|
1124
|
+
throw new MoneyError("wire_unsupported", `Unknown payment provider: ${describeValue(psp)}`);
|
|
1125
|
+
}
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/wire/marker.ts
|
|
1128
|
+
const WIRE_SCALE_KEY = "amboras_wire_scale";
|
|
1129
|
+
const WIRE_SCALE_ISO_MINOR_V2 = "iso-minor-v2";
|
|
1130
|
+
const WIRE_SCALE_MARKER = Object.freeze({ [WIRE_SCALE_KEY]: WIRE_SCALE_ISO_MINOR_V2 });
|
|
1131
|
+
const ISO_MINOR_V2 = Object.freeze({ kind: "iso-minor-v2" });
|
|
1132
|
+
const PRE_CUTOVER = Object.freeze({ kind: "pre-cutover" });
|
|
1133
|
+
/**
|
|
1134
|
+
* `ISO_MINOR_V2` iff ANY record is a non-null object whose `amboras_wire_scale` is strictly
|
|
1135
|
+
* `'iso-minor-v2'` (a Stripe charge OR its PaymentIntent); else `PRE_CUTOVER`. Reads already
|
|
1136
|
+
* fetched records only: it never queries (FORCE RLS would hide the row and the marker with it).
|
|
1137
|
+
*/
|
|
1138
|
+
function conventionOf(...records) {
|
|
1139
|
+
return records.some((r) => typeof r === "object" && r !== null && r["amboras_wire_scale"] === "iso-minor-v2") ? ISO_MINOR_V2 : PRE_CUTOVER;
|
|
1140
|
+
}
|
|
1141
|
+
/** Internal: the convention's kind, or null when `value` is not a Convention. */
|
|
1142
|
+
function conventionKind(value) {
|
|
1143
|
+
const kind = typeof value === "object" && value !== null ? value.kind : void 0;
|
|
1144
|
+
return kind === "iso-minor-v2" || kind === "pre-cutover" ? kind : null;
|
|
1145
|
+
}
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/wire/codec.ts
|
|
1148
|
+
const isBlank = (value) => value === null || value === void 0 || typeof value === "string" && value.trim() === "";
|
|
1149
|
+
const shownCode = (currency) => typeof currency === "string" ? currency.trim().toUpperCase() : describeValue(currency);
|
|
1150
|
+
function missingCurrency(d) {
|
|
1151
|
+
const message = `[${d.tag}] Cannot scale an amount without a currency code; session data is missing currency_code.`;
|
|
1152
|
+
return new MoneyError("currency_missing", message);
|
|
1153
|
+
}
|
|
1154
|
+
function supported(d, currency) {
|
|
1155
|
+
const info = wireCurrency(currency);
|
|
1156
|
+
return info !== null && d.currencies.has(info.code) ? info : null;
|
|
1157
|
+
}
|
|
1158
|
+
/** Whether `psp` takes `currency` (any case, historic codes included). Never throws for a bad code. */
|
|
1159
|
+
function pspSupportsCurrency(psp, currency) {
|
|
1160
|
+
return supported(pspDescriptor(psp), currency) !== null;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Every currency `psp` takes. Provider-only codes a PSP table lists (Adyen offshore CNH) are left
|
|
1164
|
+
* out: they are not ISO 4217 currencies, no store sells in them and no Money can carry them.
|
|
1165
|
+
*/
|
|
1166
|
+
function pspCurrencies(psp) {
|
|
1167
|
+
const d = pspDescriptor(psp);
|
|
1168
|
+
const codes = [];
|
|
1169
|
+
for (const code of d.currencies) {
|
|
1170
|
+
const info = wireCurrency(code);
|
|
1171
|
+
if (info) codes.push(info.code);
|
|
1172
|
+
}
|
|
1173
|
+
return new Set(codes.sort());
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* `currency` as a CurrencyCode `psp` takes. Empty throws `currency_missing` (`[<Tag>] Cannot
|
|
1177
|
+
* scale an amount without a currency code; ...`); anything the PSP does not take throws
|
|
1178
|
+
* `wire_unsupported` with the provider's own message.
|
|
1179
|
+
*/
|
|
1180
|
+
function assertPspCurrency(psp, currency) {
|
|
1181
|
+
const d = pspDescriptor(psp);
|
|
1182
|
+
if (isBlank(currency)) throw missingCurrency(d);
|
|
1183
|
+
const info = supported(d, currency);
|
|
1184
|
+
if (info) return info.code;
|
|
1185
|
+
const providerOnly = providerOnlyCode(currency);
|
|
1186
|
+
if (providerOnly !== null) throw new MoneyError("wire_unsupported", providerOnlyRefusal(d.tag, providerOnly));
|
|
1187
|
+
throw new MoneyError("wire_unsupported", d.unsupported(shownCode(currency), wireCurrency(currency)));
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The gate Medusa's amount facade for `psp` applies, on encode and decode alike: most scale any
|
|
1191
|
+
* currency (the PSP's list is eligibility's job: `assertPspCurrency`); Razorpay, Paystack, MoMo,
|
|
1192
|
+
* Payoo and VNPay refuse with their own message (provider-only CNH included, as in Medusa).
|
|
1193
|
+
* Returns the registry entry, else throws (`wire_unsupported` for a facade refusal or a
|
|
1194
|
+
* provider-only code, `currency_invalid` for an unknown code).
|
|
1195
|
+
*/
|
|
1196
|
+
function facadeCurrency(d, currency) {
|
|
1197
|
+
const info = wireCurrency(currency);
|
|
1198
|
+
const code = info?.code ?? shownCode(currency);
|
|
1199
|
+
if (!d.facadeAccepts(code, info)) throw new MoneyError("wire_unsupported", d.unsupported(code, info));
|
|
1200
|
+
const providerOnly = providerOnlyCode(currency);
|
|
1201
|
+
if (providerOnly !== null) throw new MoneyError("wire_unsupported", providerOnlyRefusal(d.tag, providerOnly));
|
|
1202
|
+
return info ?? resolveWireCurrency(currency);
|
|
1203
|
+
}
|
|
1204
|
+
/** `'VND'` for the single-currency providers (MoMo, Payoo, VNPay), else null. */
|
|
1205
|
+
function pspDefaultCurrency(psp) {
|
|
1206
|
+
const single = pspDescriptor(psp).singleCurrency;
|
|
1207
|
+
return single === null ? null : single;
|
|
1208
|
+
}
|
|
1209
|
+
function factorOf(d, info) {
|
|
1210
|
+
if (d.sentAsTwoDecimal.has(info.code)) return 1n;
|
|
1211
|
+
if (d.sentAsZeroDecimal.has(info.code)) return 100n;
|
|
1212
|
+
return isoMinorFactor(info, info.storageDigits);
|
|
1213
|
+
}
|
|
1214
|
+
function requireIsoMinor(d) {
|
|
1215
|
+
if (d.family !== "iso-minor") throw new MoneyError("wire_unsupported", `[${d.tag}] ${d.tag} does not take ISO minor units`);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Stored units per wire unit for an ISO-minor PSP: the PSP's override (Stripe ISK/UGX, Adyen
|
|
1219
|
+
* ISK/CLP, Razorpay MGA, Paystack XOF, VNPay VND give 1; Adyen IDR/CVE give 100), else
|
|
1220
|
+
* `storageScale / 10^storageDigits` (100 for a zero-decimal currency, else 1).
|
|
1221
|
+
*/
|
|
1222
|
+
function wireFactor(psp, currency) {
|
|
1223
|
+
const d = pspDescriptor(psp);
|
|
1224
|
+
requireIsoMinor(d);
|
|
1225
|
+
return Number(factorOf(d, resolveWireCurrency(currency)));
|
|
1226
|
+
}
|
|
1227
|
+
function wireDigitsOf(d, info) {
|
|
1228
|
+
const table = d.digits.get(info.code);
|
|
1229
|
+
if (table !== void 0) return table;
|
|
1230
|
+
if (d.family === "iso-minor") return factorOf(d, info) === 1n ? storageExponent(info) : 0;
|
|
1231
|
+
return info.displayDigits;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Fraction digits of `psp`'s wire amount: its digit table, else (ISO-minor PSPs) the digits
|
|
1235
|
+
* its factor implies, else (iyzico, Authorize.Net, Payoo) the display digits. Null when the
|
|
1236
|
+
* PSP does not take the currency.
|
|
1237
|
+
*/
|
|
1238
|
+
function pspWireDigits(psp, currency) {
|
|
1239
|
+
const d = pspDescriptor(psp);
|
|
1240
|
+
const info = supported(d, currency);
|
|
1241
|
+
return info === null ? null : wireDigitsOf(d, info);
|
|
1242
|
+
}
|
|
1243
|
+
/** The PSP's minimum charge in `currency` (Paystack USD 2.00), or null when it has none. */
|
|
1244
|
+
function pspMinimumAmount(psp, currency) {
|
|
1245
|
+
const d = pspDescriptor(psp);
|
|
1246
|
+
const info = wireCurrency(currency);
|
|
1247
|
+
if (info === null) return null;
|
|
1248
|
+
const major = PSP_MINIMUM_MAJOR[d.id]?.[info.code];
|
|
1249
|
+
return major === void 0 ? null : fromMajor(major, info.code);
|
|
1250
|
+
}
|
|
1251
|
+
function requireConvention(d, convention) {
|
|
1252
|
+
const kind = conventionKind(convention);
|
|
1253
|
+
if (kind === null) {
|
|
1254
|
+
const message = `[${d.tag}] A wire convention is required: pass conventionOf(record) for this payment.`;
|
|
1255
|
+
throw new MoneyError("wire_refused", message);
|
|
1256
|
+
}
|
|
1257
|
+
return kind;
|
|
1258
|
+
}
|
|
1259
|
+
function encodeWire(psp, m, options = {}) {
|
|
1260
|
+
const d = pspDescriptor(psp);
|
|
1261
|
+
const { info, units } = wireStored(m);
|
|
1262
|
+
const value = {
|
|
1263
|
+
units,
|
|
1264
|
+
scale: storageExponent(info)
|
|
1265
|
+
};
|
|
1266
|
+
facadeCurrency(d, info.code);
|
|
1267
|
+
const label = options.label ?? "amount";
|
|
1268
|
+
const stored = value.units;
|
|
1269
|
+
if (stored < 0n || stored === 0n && !options.allowZero) throw new MoneyError("wire_refused", `[${d.tag}] Invalid ${label}: ${m.amount}`);
|
|
1270
|
+
const factor = d.family === "iso-minor" ? factorOf(d, info) : 1n;
|
|
1271
|
+
if (d.encodeGated && requireConvention(d, options.convention) === "pre-cutover") {
|
|
1272
|
+
if (d.preCutoverNeedsFactorOne && divRound(stored, factor, "half-up") !== stored) throw new MoneyError("wire_refused", preCutoverRefusal(d.tag, info.code));
|
|
1273
|
+
return m.amount;
|
|
1274
|
+
}
|
|
1275
|
+
if (d.wholeUnit?.applies(info) && stored % BigInt(info.storageScale) !== 0n) throw new MoneyError("wire_refused", d.wholeUnit.message(info, m.amount, label, formatDecimal(value)));
|
|
1276
|
+
const digits = wireDigitsOf(d, info);
|
|
1277
|
+
if (d.family === "iso-minor") {
|
|
1278
|
+
const exact = divRound(stored, factor, "half-up");
|
|
1279
|
+
if (exact === 0n && stored > 0n) throw subUnitRefusal(d, info, formatDecimal(value), digits, label);
|
|
1280
|
+
if (!d.threeDecimalStepOfTen || d.digits.get(info.code) !== 3) return Number(exact);
|
|
1281
|
+
const stepped = exact - exact % 10n;
|
|
1282
|
+
if (stepped === 0n && exact > 0n) throw new MoneyError("wire_refused", roundsToNothing(d.tag, info.code, formatDecimal(value)));
|
|
1283
|
+
return Number(stepped);
|
|
1284
|
+
}
|
|
1285
|
+
const rounded = roundToScale(value, digits, "half-up");
|
|
1286
|
+
if (rounded.units === 0n && stored > 0n) throw subUnitRefusal(d, info, formatDecimal(value), digits, label);
|
|
1287
|
+
const text = formatDecimal(rounded);
|
|
1288
|
+
return d.family === "major-number" ? Number(text) : text;
|
|
1289
|
+
}
|
|
1290
|
+
function subUnitRefusal(d, info, major, digits, label) {
|
|
1291
|
+
const unit = formatDecimal({
|
|
1292
|
+
units: 1n,
|
|
1293
|
+
scale: digits
|
|
1294
|
+
});
|
|
1295
|
+
const message = `[${d.tag}] Invalid ${label}: ${info.code} ${major} rounds to 0 at ${d.tag}, whose smallest ${info.code} unit is ${unit}; refusing to send a zero amount.`;
|
|
1296
|
+
return new MoneyError("wire_refused", message);
|
|
1297
|
+
}
|
|
1298
|
+
function preCutoverRefusal(tag, code) {
|
|
1299
|
+
return `[${tag}] This ${code} session predates the wire-scale marker, so ${tag} may hold storage units for it; converting would move 1/100th of the amount. Refusing rather than guessing.`;
|
|
1300
|
+
}
|
|
1301
|
+
function roundsToNothing(tag, code, major) {
|
|
1302
|
+
return `[${tag}] ${code} ${major} rounds to nothing at ${tag}, which takes three-decimal amounts in steps of 0.010.`;
|
|
1303
|
+
}
|
|
1304
|
+
/** A wire value as an exact Decimal: a finite number (a whole one must be safe) or strict decimal text. Else null. */
|
|
1305
|
+
function wireDecimal(wire) {
|
|
1306
|
+
return typeof wire === "number" ? strictNumberDecimal(wire) : parseDecimal(wire, "plain");
|
|
1307
|
+
}
|
|
1308
|
+
function decodeWith(d, w, info, kind) {
|
|
1309
|
+
const scale = pow10(w.scale);
|
|
1310
|
+
let multiplier;
|
|
1311
|
+
if (d.family !== "iso-minor") multiplier = BigInt(info.storageScale);
|
|
1312
|
+
else multiplier = kind === "pre-cutover" ? 1n : factorOf(d, info);
|
|
1313
|
+
return boundedMoney(mulDiv(w.units, multiplier, scale, "half-up"), info);
|
|
1314
|
+
}
|
|
1315
|
+
/** VNPay: a positive whole safe integer, as Medusa's `vnpayWireAmountToStorage` reads one. */
|
|
1316
|
+
function isWholePositive(w) {
|
|
1317
|
+
const scale = pow10(w.scale);
|
|
1318
|
+
return w.units > 0n && w.units % scale === 0n && w.units / scale <= BigInt(Number.MAX_SAFE_INTEGER);
|
|
1319
|
+
}
|
|
1320
|
+
/** Every inbound check: facade currency gate, convention, strict wire text, VNPay's "no amount", the sign. */
|
|
1321
|
+
function decodeChecked(d, wire, currency, convention, allowNegative) {
|
|
1322
|
+
if (isBlank(currency)) throw missingCurrency(d);
|
|
1323
|
+
const info = facadeCurrency(d, currency);
|
|
1324
|
+
const kind = d.gated ? requireConvention(d, convention) : null;
|
|
1325
|
+
const w = wireDecimal(wire);
|
|
1326
|
+
if (!w) throw new MoneyError("amount_invalid", `[${d.tag}] Not a wire amount: ${describeValue(wire)}`);
|
|
1327
|
+
if (d.inboundWholePositive && !isWholePositive(w)) throw new MoneyError("amount_invalid", `[${d.tag}] No amount: ${d.tag} amounts are positive whole numbers, got ${describeValue(wire)}`);
|
|
1328
|
+
if (w.units < 0n && !allowNegative) throw new MoneyError("amount_invalid", `[${d.tag}] Negative wire amount: ${describeValue(wire)}`);
|
|
1329
|
+
return decodeWith(d, w, info, kind);
|
|
1330
|
+
}
|
|
1331
|
+
function decodeWire(psp, wire, currency, convention) {
|
|
1332
|
+
return decodeChecked(pspDescriptor(psp), wire, currency, convention, true);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* `decodeWire` that never throws: undefined wherever it would throw (null, undefined, `''`,
|
|
1336
|
+
* whitespace, unparseable or unsafe input, a missing, unknown or provider-only currency, one the
|
|
1337
|
+
* PSP's inbound gate refuses, VNPay's "no amount", an unknown PSP, a gated PSP without
|
|
1338
|
+
* `convention`: no hidden default) and for a negative amount unless `allowNegative`.
|
|
1339
|
+
*/
|
|
1340
|
+
function tryDecodeWire(psp, wire, currency, options = {}) {
|
|
1341
|
+
try {
|
|
1342
|
+
return decodeChecked(pspDescriptor(psp), wire, currency, options.convention, options.allowNegative === true);
|
|
1343
|
+
} catch {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
/** Internal: the major-unit text of `m` at its display digits (the pre-PSP-table ISO spelling). */
|
|
1348
|
+
function legacyIsoMajor(m) {
|
|
1349
|
+
const { info, units } = wireStored(m);
|
|
1350
|
+
return formatDecimal(roundToScale({
|
|
1351
|
+
units,
|
|
1352
|
+
scale: storageExponent(info)
|
|
1353
|
+
}, info.displayDigits, "half-up"));
|
|
1354
|
+
}
|
|
1355
|
+
/** Internal: one unit of the PSP's last wire digit as Money. */
|
|
1356
|
+
function wireUnit(psp, currency, convention) {
|
|
1357
|
+
const d = pspDescriptor(psp);
|
|
1358
|
+
const info = resolveWireCurrency(currency);
|
|
1359
|
+
if (d.family === "iso-minor") return decodeWith(d, {
|
|
1360
|
+
units: 1n,
|
|
1361
|
+
scale: 0
|
|
1362
|
+
}, info, d.gated ? requireConvention(d, convention) : null);
|
|
1363
|
+
return boundedMoney(isoMinorFactor(info, wireDigitsOf(d, info)), info);
|
|
1364
|
+
}
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/wire/breakdown.ts
|
|
1367
|
+
/**
|
|
1368
|
+
* `total` and its `parts` (same currency, non-negative, adding up to `total`) as integer wire
|
|
1369
|
+
* amounts whose sum is exactly `encodeWire(psp, total)`: the wire total split by largest
|
|
1370
|
+
* remainder over the stored parts. Throws `currency_mismatch`, `amount_invalid` (parts that do not
|
|
1371
|
+
* add up, or a negative part) or whatever `encodeWire` refuses.
|
|
1372
|
+
*/
|
|
1373
|
+
function wireBreakdown(psp, total, parts, convention) {
|
|
1374
|
+
const { info } = wireStored(total);
|
|
1375
|
+
const names = Object.keys(parts);
|
|
1376
|
+
const weights = names.map((name) => {
|
|
1377
|
+
const part = wireStored(parts[name]);
|
|
1378
|
+
if (part.info.code !== info.code) throw new MoneyError("currency_mismatch", `Part ${name} is ${part.info.code}, total is ${info.code}`);
|
|
1379
|
+
return part.units;
|
|
1380
|
+
});
|
|
1381
|
+
const partsTotal = weights.reduce((a, b) => a + b, 0n);
|
|
1382
|
+
if (partsTotal !== BigInt(total.amount)) throw new MoneyError("amount_invalid", `Parts add up to ${partsTotal}, not the total ${total.amount}`);
|
|
1383
|
+
const wireTotal = encodeWire(psp, total, {
|
|
1384
|
+
convention,
|
|
1385
|
+
allowZero: true
|
|
1386
|
+
});
|
|
1387
|
+
const shares = allocateUnits(BigInt(wireTotal), weights);
|
|
1388
|
+
const out = {};
|
|
1389
|
+
names.forEach((name, i) => {
|
|
1390
|
+
out[name] = Number(shares[i]);
|
|
1391
|
+
});
|
|
1392
|
+
return {
|
|
1393
|
+
total: wireTotal,
|
|
1394
|
+
parts: out
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/wire/compare.ts
|
|
1399
|
+
/**
|
|
1400
|
+
* Whether a PSP-reported amount is `expected`. The expected wire is `encodeWire(psp, expected,
|
|
1401
|
+
* { convention, allowZero: true })` (a refusal means false); the reported one must decode
|
|
1402
|
+
* (`tryDecodeWire`: strict, never `''` or an exponent, no negatives) to the same stored amount,
|
|
1403
|
+
* so `'44.00'` matches a sent `'44'`. Never throws.
|
|
1404
|
+
*/
|
|
1405
|
+
function sameWireAmount(psp, reported, expected, options = {}) {
|
|
1406
|
+
const { convention } = options;
|
|
1407
|
+
if (options.reportedCurrency !== void 0) {
|
|
1408
|
+
const reportedInfo = wireCurrency(options.reportedCurrency);
|
|
1409
|
+
if (reportedInfo === null || reportedInfo !== wireCurrency(expected.currency)) return false;
|
|
1410
|
+
}
|
|
1411
|
+
const targets = expectedTargets(psp, expected, convention, options.acceptLegacyIsoDigits === true);
|
|
1412
|
+
if (targets === null) return false;
|
|
1413
|
+
const { target, legacy } = targets;
|
|
1414
|
+
const decode = (value) => tryDecodeWire(psp, value, expected.currency, { convention });
|
|
1415
|
+
if (options.policy === "requested-exact-collected-covers" && options.requested !== void 0 && options.requested !== null) {
|
|
1416
|
+
const requested = decode(options.requested);
|
|
1417
|
+
const collected = decode(reported);
|
|
1418
|
+
if (requested === void 0 || collected === void 0) return false;
|
|
1419
|
+
return requested.amount === target.amount && collected.amount >= target.amount;
|
|
1420
|
+
}
|
|
1421
|
+
const got = decode(reported);
|
|
1422
|
+
if (got === void 0) return false;
|
|
1423
|
+
return got.amount === target.amount || got.amount === legacy?.amount;
|
|
1424
|
+
}
|
|
1425
|
+
/** What `expected` decodes to once sent (and in its legacy ISO-digit spelling), or null when it cannot be sent. */
|
|
1426
|
+
function expectedTargets(psp, expected, convention, acceptLegacy) {
|
|
1427
|
+
try {
|
|
1428
|
+
const d = pspDescriptor(psp);
|
|
1429
|
+
const wire = encodeWire(psp, expected, {
|
|
1430
|
+
convention,
|
|
1431
|
+
allowZero: true
|
|
1432
|
+
});
|
|
1433
|
+
const target = decodeAt(psp, wire, expected, convention);
|
|
1434
|
+
if (!acceptLegacy || d.family === "iso-minor") return {
|
|
1435
|
+
target,
|
|
1436
|
+
legacy: void 0
|
|
1437
|
+
};
|
|
1438
|
+
const spelling = legacyIsoMajor(expected);
|
|
1439
|
+
return {
|
|
1440
|
+
target,
|
|
1441
|
+
legacy: spelling === String(wire) ? void 0 : decodeAt(psp, spelling, expected, convention)
|
|
1442
|
+
};
|
|
1443
|
+
} catch {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function decodeAt(psp, wire, expected, convention) {
|
|
1448
|
+
return decodeWire(psp, wire, expected.currency, convention);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* One unit of the PSP's last wire digit, as Money: for ISO-minor PSPs `decodeWire(psp, 1)` (the
|
|
1452
|
+
* factor on an iso-minor-v2 payment, 1 storage unit on a pre-cutover one; gated PSPs need the
|
|
1453
|
+
* convention), for major PSPs `10^-wireDigits` major. The tolerance for `excessOver`.
|
|
1454
|
+
*/
|
|
1455
|
+
function oneWireUnit(psp, currency, convention) {
|
|
1456
|
+
return wireUnit(psp, currency, convention);
|
|
1457
|
+
}
|
|
1458
|
+
/** The wire amount as it goes into Redis skip keys and idempotency keys: `String(wire)`, byte for byte. */
|
|
1459
|
+
function wireKeyPart(wire) {
|
|
1460
|
+
return String(wire);
|
|
1461
|
+
}
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/data/destinations.ts
|
|
1464
|
+
const pixel = (currencyCase, unit = "major-number") => ({
|
|
1465
|
+
unit,
|
|
1466
|
+
currencyCase,
|
|
1467
|
+
accepts: "non-negative",
|
|
1468
|
+
verified: true
|
|
1469
|
+
});
|
|
1470
|
+
const marketing = (currencyCase) => ({
|
|
1471
|
+
unit: "major-number",
|
|
1472
|
+
currencyCase,
|
|
1473
|
+
accepts: "non-negative",
|
|
1474
|
+
verified: true
|
|
1475
|
+
});
|
|
1476
|
+
const ledger = (unit = "major-number") => ({
|
|
1477
|
+
unit,
|
|
1478
|
+
currencyCase: "upper",
|
|
1479
|
+
accepts: "any",
|
|
1480
|
+
verified: true
|
|
1481
|
+
});
|
|
1482
|
+
const DESTINATIONS = {
|
|
1483
|
+
meta: {
|
|
1484
|
+
unit: "major-number",
|
|
1485
|
+
currencyCase: "lower",
|
|
1486
|
+
accepts: "positive",
|
|
1487
|
+
verified: true
|
|
1488
|
+
},
|
|
1489
|
+
ga4: pixel("upper"),
|
|
1490
|
+
"google-ads": pixel("upper"),
|
|
1491
|
+
tiktok: pixel("upper"),
|
|
1492
|
+
snapchat: pixel("upper"),
|
|
1493
|
+
pinterest: pixel("upper", "major-string"),
|
|
1494
|
+
klaviyo: marketing("lower"),
|
|
1495
|
+
omnisend: marketing("lower"),
|
|
1496
|
+
"omnisend-viewed-product": {
|
|
1497
|
+
unit: "cents",
|
|
1498
|
+
cents: "two-decimal",
|
|
1499
|
+
currencyCase: "lower",
|
|
1500
|
+
accepts: "non-negative",
|
|
1501
|
+
verified: true
|
|
1502
|
+
},
|
|
1503
|
+
mailchimp: marketing("upper"),
|
|
1504
|
+
goaffpro: marketing("lower"),
|
|
1505
|
+
profitmetrics: marketing("upper"),
|
|
1506
|
+
pixi: marketing("lower"),
|
|
1507
|
+
flow: ledger(),
|
|
1508
|
+
sendcloud: ledger(),
|
|
1509
|
+
shipstation: ledger(),
|
|
1510
|
+
shiprocket: ledger(),
|
|
1511
|
+
shipbob: ledger(),
|
|
1512
|
+
printful: ledger("major-string"),
|
|
1513
|
+
ahamove: ledger(),
|
|
1514
|
+
"amazon-mcf": ledger(),
|
|
1515
|
+
skydropx: ledger(),
|
|
1516
|
+
"royal-mail": ledger(),
|
|
1517
|
+
sevdesk: ledger(),
|
|
1518
|
+
"shopify-ajax": {
|
|
1519
|
+
unit: "cents",
|
|
1520
|
+
cents: "x100-storage",
|
|
1521
|
+
currencyCase: "upper",
|
|
1522
|
+
accepts: "any",
|
|
1523
|
+
verified: true
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/wire/destinations.ts
|
|
1528
|
+
function descriptorOf(dest) {
|
|
1529
|
+
if (typeof dest === "string" && hasOwnKey(DESTINATIONS, dest)) return DESTINATIONS[dest];
|
|
1530
|
+
throw new MoneyError("wire_unsupported", `Unknown destination: ${describeValue(dest)}`);
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* `m` as `dest` wants it: major units at the display digits (half-up) as a number or text per
|
|
1534
|
+
* destination (`as` overrides), or integer "cents" for Shopify AJAX and Omnisend `viewed product`;
|
|
1535
|
+
* the currency upper- or lower-case per destination. Undefined when the destination must not get
|
|
1536
|
+
* this amount (Meta: not positive; GA4, Google Ads, TikTok, Snapchat, Pinterest and marketing:
|
|
1537
|
+
* negative). Throws `wire_unsupported` for a currency the destination's unit is not defined for.
|
|
1538
|
+
*/
|
|
1539
|
+
function destinationAmount(dest, m, options = {}) {
|
|
1540
|
+
const d = descriptorOf(dest);
|
|
1541
|
+
const { info, value } = storedDecimal(m);
|
|
1542
|
+
if (d.accepts === "positive" ? value.units <= 0n : d.accepts === "non-negative" && value.units < 0n) return void 0;
|
|
1543
|
+
let amount;
|
|
1544
|
+
if (d.unit === "cents") {
|
|
1545
|
+
if (!(d.cents === "two-decimal" ? info.displayDigits === 2 : info.storageScale === 100)) throw new MoneyError("wire_unsupported", `${dest} has no documented integer unit for ${info.code}`);
|
|
1546
|
+
const cents = mulDiv(value.units, 100n, BigInt(info.storageScale), "half-up");
|
|
1547
|
+
amount = options.as === "string" ? cents.toString() : Number(cents);
|
|
1548
|
+
} else {
|
|
1549
|
+
const text = toMajorDecimal(m, { digits: "display" });
|
|
1550
|
+
amount = (options.as ? options.as === "string" : d.unit === "major-string") ? text : Number(text);
|
|
1551
|
+
}
|
|
1552
|
+
return {
|
|
1553
|
+
value: amount,
|
|
1554
|
+
currency: d.currencyCase === "lower" ? medusaCurrencyCode(info.code) : info.code
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
/** A shopping-feed price: `'12.345 KWD'`, `'1200 JPY'`, `'19.99 USD'` (display digits, half-up). */
|
|
1558
|
+
function feedPrice(m) {
|
|
1559
|
+
return `${toMajorDecimal(m, { digits: "display" })} ${storedDecimal(m).info.code}`;
|
|
1560
|
+
}
|
|
1561
|
+
/** Google micros (major x 10^6) as integer text, exactly: USD 1999 gives `'19990000'`. */
|
|
1562
|
+
function googleMicros(m) {
|
|
1563
|
+
const { info, value } = storedDecimal(m);
|
|
1564
|
+
return mulDiv(value.units, 1000000n, BigInt(info.storageScale), "half-up").toString();
|
|
1565
|
+
}
|
|
1566
|
+
/** First-party analytics money: the stored integer and the lower-case code (the ClickHouse `Int64` contract). */
|
|
1567
|
+
function eventMoney(m) {
|
|
1568
|
+
const { info } = storedDecimal(m);
|
|
1569
|
+
return {
|
|
1570
|
+
value: m.amount,
|
|
1571
|
+
currency: medusaCurrencyCode(info.code)
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
//#endregion
|
|
1575
|
+
//#region src/wire/provider-amount.ts
|
|
1576
|
+
/**
|
|
1577
|
+
* A provider's major-unit amount (finite number, or text read by the separator engine, which
|
|
1578
|
+
* strips the currency's symbol and code) as Money, rounded once to the storage digits. Null on
|
|
1579
|
+
* anything invalid (empty, unparseable, non-finite, negative without `allowNegative`, too large,
|
|
1580
|
+
* unknown currency), never 0.
|
|
1581
|
+
*/
|
|
1582
|
+
function parseProviderAmount(value, currency, options = {}) {
|
|
1583
|
+
let major;
|
|
1584
|
+
try {
|
|
1585
|
+
if (typeof value === "number") {
|
|
1586
|
+
if (!Number.isFinite(value)) return null;
|
|
1587
|
+
major = value;
|
|
1588
|
+
} else {
|
|
1589
|
+
const read = readNumberText(value, {
|
|
1590
|
+
decimalSeparator: options.decimalSeparator ?? ".",
|
|
1591
|
+
allowTrailingText: options.allowTrailingText,
|
|
1592
|
+
currency,
|
|
1593
|
+
rounding: options.rounding ?? "half-up"
|
|
1594
|
+
});
|
|
1595
|
+
if ("reason" in read) return null;
|
|
1596
|
+
major = formatDecimal({
|
|
1597
|
+
units: BigInt(read.sign) * read.units,
|
|
1598
|
+
scale: read.scale
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
if (!options.allowNegative && (typeof major === "number" ? major < 0 : major.startsWith("-"))) return null;
|
|
1602
|
+
return fromMajor(major, currency, {
|
|
1603
|
+
precision: "round",
|
|
1604
|
+
rounding: options.rounding ?? "half-up"
|
|
1605
|
+
});
|
|
1606
|
+
} catch {
|
|
1607
|
+
return null;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/wire/providers.ts
|
|
1612
|
+
/** RBI e-mandate cap Razorpay token registrations carry as `max_amount`: INR 15,000 (1,500,000 paise = stored). */
|
|
1613
|
+
const RAZORPAY_AFA_FREE_CAP = Object.freeze(fromMajor(RAZORPAY_AFA_FREE_CAP_MAJOR, "INR"));
|
|
1614
|
+
/**
|
|
1615
|
+
* The symbolic charge a saved-card setup makes: Paystack one major unit, Razorpay one INR (any
|
|
1616
|
+
* other currency is `wire_unsupported`), Mollie one smallest wire unit (`'0.01'`, `'1'` for ISK
|
|
1617
|
+
* and JPY), Adyen zero. Vault charges are always new, so the iso-minor-v2 convention applies.
|
|
1618
|
+
*/
|
|
1619
|
+
function verificationCharge(psp, currency) {
|
|
1620
|
+
const code = assertPspCurrency(psp, currency);
|
|
1621
|
+
let charge;
|
|
1622
|
+
if (psp === "paystack") charge = fromMajor("1", code);
|
|
1623
|
+
else if (psp === "razorpay") {
|
|
1624
|
+
if (code !== "INR") throw new MoneyError("wire_unsupported", `[Razorpay] Verification charges are made in INR only, not ${code}.`);
|
|
1625
|
+
charge = fromMajor("1", code);
|
|
1626
|
+
} else if (psp === "mollie") charge = wireUnit("mollie", code);
|
|
1627
|
+
else charge = boundedMoney(0n, resolveWireCurrency(code));
|
|
1628
|
+
return {
|
|
1629
|
+
wire: encodeWire(psp, charge, {
|
|
1630
|
+
convention: ISO_MINOR_V2,
|
|
1631
|
+
allowZero: true
|
|
1632
|
+
}),
|
|
1633
|
+
money: charge
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* iyzico's price spelling, on the text (no float): canonical decimal (no trailing fraction zeros),
|
|
1638
|
+
* then `'.0'` when there is no `.` (`'10.50'` gives `'10.5'`, `'10.00'` gives `'10.0'`). Text that
|
|
1639
|
+
* is not a plain decimal throws `parse`.
|
|
1640
|
+
*/
|
|
1641
|
+
function iyzicoPrice(wire) {
|
|
1642
|
+
const parsed = parseDecimal(wire, "plain");
|
|
1643
|
+
if (!parsed) throw new MoneyError("parse", `Not an iyzico price: ${describeValue(wire)}`);
|
|
1644
|
+
const text = toDecimalText(parsed);
|
|
1645
|
+
return text.includes(".") ? text : `${text}.0`;
|
|
1646
|
+
}
|
|
1647
|
+
//#endregion
|
|
1648
|
+
//#region src/wire/slot.ts
|
|
1649
|
+
/** `m` for a slot context's `money` key. */
|
|
1650
|
+
function slotMoney(m) {
|
|
1651
|
+
const { info } = storedDecimal(m);
|
|
1652
|
+
return {
|
|
1653
|
+
amount: m.amount,
|
|
1654
|
+
currency_code: medusaCurrencyCode(info.code)
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
1658
|
+
function read(amount, ...currencies) {
|
|
1659
|
+
const info = currencies.map((c) => findCurrency(c, { allowHistoric: true })).find((c) => c !== null);
|
|
1660
|
+
if (!info || amount === void 0 || amount === null) return void 0;
|
|
1661
|
+
const stored = readStored(amount, info.code, { mode: "lenient" });
|
|
1662
|
+
return stored ? slotMoney(stored) : void 0;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* The money a slot context carries: `context.money` when it is valid, else the legacy
|
|
1666
|
+
* `total ?? price` with the first known of `currency_code`, `currency`, read leniently (numbers, numeric text,
|
|
1667
|
+
* Medusa BigNumber shapes). Undefined when the amount or the currency is missing or unreadable.
|
|
1668
|
+
*/
|
|
1669
|
+
function readSlotMoney(context) {
|
|
1670
|
+
if (!isRecord(context)) return void 0;
|
|
1671
|
+
const money = context.money;
|
|
1672
|
+
return (isRecord(money) ? read(money.amount, money.currency_code) : void 0) ?? read(context.total ?? context.price, context.currency_code, context.currency);
|
|
1673
|
+
}
|
|
1674
|
+
//#endregion
|
|
1675
|
+
export { conventionOf as A, tryDecodeWire as C, WIRE_SCALE_ISO_MINOR_V2 as D, PRE_CUTOVER as E, authorizeNetReversibleAmount as M, jsonDecimalLiteral as N, WIRE_SCALE_KEY as O, pspWireDigits as S, ISO_MINOR_V2 as T, encodeWire as _, verificationCharge as a, pspMinimumAmount as b, eventMoney as c, oneWireUnit as d, sameWireAmount as f, decodeWire as g, assertPspCurrency as h, iyzicoPrice as i, PSP_IDS as j, WIRE_SCALE_MARKER as k, feedPrice as l, wireBreakdown as m, slotMoney as n, parseProviderAmount as o, wireKeyPart as p, RAZORPAY_AFA_FREE_CAP as r, destinationAmount as s, readSlotMoney as t, googleMicros as u, pspCurrencies as v, wireFactor as w, pspSupportsCurrency as x, pspDefaultCurrency as y };
|