@amboras-dev/money 0.1.0 → 0.3.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 (80) hide show
  1. package/README.md +25 -10
  2. package/dist/ai.cjs +12 -13
  3. package/dist/ai.d.cts +5 -9
  4. package/dist/ai.d.ts +5 -9
  5. package/dist/ai.js +12 -13
  6. package/dist/{allocate-D7tm_t_o.cjs → allocate-AusholrC.cjs} +3 -4
  7. package/dist/{allocate-CdnhvAda.js → allocate-BBRfdaSC.js} +3 -4
  8. package/dist/{amount-DHtvZSSm.cjs → amount-CwFzcxUl.cjs} +29 -12
  9. package/dist/{amount-D7dj76nH.js → amount-DHFOK2Yd.js} +29 -12
  10. package/dist/{arithmetic-C-Q_aI49.js → arithmetic-C97-YafD.js} +4 -5
  11. package/dist/{arithmetic-Bq-8mSal.cjs → arithmetic-D8A6IeZ0.cjs} +4 -5
  12. package/dist/billing.cjs +5 -8
  13. package/dist/billing.d.cts +1 -9
  14. package/dist/billing.d.ts +1 -9
  15. package/dist/billing.js +5 -8
  16. package/dist/commerce.cjs +15 -23
  17. package/dist/commerce.d.cts +2 -16
  18. package/dist/commerce.d.ts +2 -16
  19. package/dist/commerce.js +15 -23
  20. package/dist/{format-BnA1aLJk.cjs → format-92fzvTXY.cjs} +21 -6
  21. package/dist/{format-BdB7LEIp.js → format-BJC513u4.js} +16 -7
  22. package/dist/fx.cjs +12 -18
  23. package/dist/fx.d.cts +5 -15
  24. package/dist/fx.d.ts +5 -15
  25. package/dist/fx.js +12 -18
  26. package/dist/index.cjs +13 -12
  27. package/dist/index.d.cts +28 -22
  28. package/dist/index.d.ts +28 -22
  29. package/dist/index.js +11 -13
  30. package/dist/input.cjs +7 -10
  31. package/dist/input.d.cts +9 -16
  32. package/dist/input.d.ts +9 -16
  33. package/dist/input.js +7 -10
  34. package/dist/{iso-minor--gQWI1f2.cjs → iso-minor-DyNjT9A1.cjs} +2 -3
  35. package/dist/{iso-minor-CtaiIifs.js → iso-minor-odkY-e6I.js} +2 -3
  36. package/dist/{medusa-OJ2TJOxt.cjs → medusa-CzVK0bth.cjs} +24 -4
  37. package/dist/{medusa-DuQ3-KoD.d.cts → medusa-DofdvIgQ.d.cts} +11 -4
  38. package/dist/{medusa-RwqxaDGq.d.ts → medusa-DrbYmD2Y.d.ts} +11 -4
  39. package/dist/{medusa-CmxCni-G.js → medusa-pKGlImn9.js} +19 -5
  40. package/dist/{money-CbhsT0fB.d.cts → money-BkMeYDEW.d.cts} +19 -9
  41. package/dist/{money-CbhsT0fB.d.ts → money-BkMeYDEW.d.ts} +19 -9
  42. package/dist/{money-CIgELekG.js → money-BxbAw7BP.js} +25 -21
  43. package/dist/{money-D5-vlv0h.cjs → money-DWBjuuEK.cjs} +42 -20
  44. package/dist/{number-text-C82oh6JB.js → number-text-Bj-gZXiF.js} +3 -4
  45. package/dist/{number-text-BpvN6NAo.cjs → number-text-CWuVwrlg.cjs} +3 -4
  46. package/dist/{own-oP9DXFWv.cjs → own-C5imDkyB.cjs} +1 -2
  47. package/dist/{own-D7544D3k.js → own-DHxqIYqL.js} +1 -2
  48. package/dist/{percent-CKVI_c-m.js → percent-CB3Ixjc_.js} +2 -3
  49. package/dist/{percent-Dt8vNIAW.cjs → percent-DABHiSBs.cjs} +2 -3
  50. package/dist/{rate-CmeTGNex.d.ts → rate-BvvZUdSu.d.ts} +1 -3
  51. package/dist/{rate-toyS3EMf.d.cts → rate-CEakW3ia.d.cts} +1 -3
  52. package/dist/{rate-DGPfB_jc.cjs → rate-CEjWTATc.cjs} +3 -4
  53. package/dist/{rate-lLPViBuM.js → rate-D6S2_wgZ.js} +3 -4
  54. package/dist/{reporting-5xby-Zas.d.ts → reporting-DFNyDBdi.d.cts} +2 -4
  55. package/dist/{reporting-DEM97DhQ.d.cts → reporting-jIBILvnr.d.ts} +2 -4
  56. package/dist/{retail-BwcFJcVN.d.ts → retail-B04CjW3b.d.ts} +1 -3
  57. package/dist/{retail-DTk2XAMM.cjs → retail-CYGPU4ea.cjs} +4 -5
  58. package/dist/{retail-Mqd-sPMF.d.cts → retail-CmEyJQOH.d.cts} +1 -3
  59. package/dist/{retail-BsvpNv1F.js → retail-DXgs11ov.js} +4 -5
  60. package/dist/{rounding-oZ832yTD.cjs → rounding-4QKTRbMQ.cjs} +2 -3
  61. package/dist/{rounding-CVAPEfq_.js → rounding-CA1PVP3b.js} +2 -3
  62. package/dist/sql.cjs +9 -15
  63. package/dist/sql.d.cts +2 -12
  64. package/dist/sql.d.ts +2 -12
  65. package/dist/sql.js +9 -15
  66. package/dist/testing.cjs +72675 -12
  67. package/dist/testing.d.cts +46 -11
  68. package/dist/testing.d.ts +46 -11
  69. package/dist/testing.js +72674 -13
  70. package/dist/{wire-CI3Sm1iC.js → wire-B4QU8O3W.js} +584 -114
  71. package/dist/wire-CVNicirK.d.ts +428 -0
  72. package/dist/wire-CtNJmWVP.d.cts +428 -0
  73. package/dist/{wire-DQZ6fA3c.cjs → wire-u2tmHY0Y.cjs} +649 -113
  74. package/dist/wire.cjs +12 -1
  75. package/dist/wire.d.cts +2 -2
  76. package/dist/wire.d.ts +2 -2
  77. package/dist/wire.js +1 -2
  78. package/package.json +4 -3
  79. package/dist/wire-Cd4Q1Jmm.d.ts +0 -269
  80. package/dist/wire-hWuixv3w.d.cts +0 -269
@@ -1,32 +1,9 @@
1
- const require_money = require("./money-D5-vlv0h.cjs");
2
- const require_allocate = require("./allocate-D7tm_t_o.cjs");
3
- const require_own = require("./own-oP9DXFWv.cjs");
4
- const require_iso_minor = require("./iso-minor--gQWI1f2.cjs");
5
- const require_number_text = require("./number-text-BpvN6NAo.cjs");
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 (require_money.parseDecimal(token, "plain")) return token;
18
- }
19
- const isPositiveLiteral = (text) => {
20
- const value = require_money.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
1
+ const require_money = require("./money-DWBjuuEK.cjs");
2
+ const require_allocate = require("./allocate-AusholrC.cjs");
3
+ const require_own = require("./own-C5imDkyB.cjs");
4
+ const require_iso_minor = require("./iso-minor-DyNjT9A1.cjs");
5
+ const require_number_text = require("./number-text-CWuVwrlg.cjs");
6
+ // src/data/psp.ts
30
7
  /** developer.paypal.com/reference/currency-codes ("Last updated: August 17, 2026"). */
31
8
  const PAYPAL_CURRENCY_DIGITS = {
32
9
  0: [
@@ -519,15 +496,17 @@ const RAZORPAY_CURRENCY_EXPONENTS = {
519
496
  "TND"
520
497
  ]
521
498
  };
522
- /** Paystack: "If we notice a fractional part during processing, we ignore the fractional part." */
523
- const PAYSTACK_CURRENCY_DIGITS = { 2: [
499
+ /** paystack.com/docs/api/#supported-currency (retrieved 2026-10-07): every currency a Paystack account can charge. */
500
+ const PAYSTACK_CURRENCIES = [
524
501
  "GHS",
525
502
  "KES",
526
503
  "NGN",
527
504
  "USD",
528
505
  "XOF",
529
506
  "ZAR"
530
- ] };
507
+ ];
508
+ /** Paystack: "If we notice a fractional part during processing, we ignore the fractional part." */
509
+ const PAYSTACK_CURRENCY_DIGITS = { 2: PAYSTACK_CURRENCIES };
531
510
  /** Factor-1 overrides: ISO zero-decimal codes the PSP wants at two decimals (storage and wire agree). */
532
511
  const STRIPE_SENT_AS_TWO_DECIMAL = ["ISK", "UGX"];
533
512
  const ADYEN_SENT_AS_TWO_DECIMAL = ["ISK", "CLP"];
@@ -678,6 +657,118 @@ const STRIPE_PRESENTMENT_CURRENCIES = [
678
657
  "ZAR",
679
658
  "ZMW"
680
659
  ];
660
+ /**
661
+ * Presentment currencies only accounts registered in the named countries can charge.
662
+ * Stripe: support.stripe.com/questions/which-payments-methods-and-products-are-available-in-the-uae
663
+ * (retrieved 2026-10-07): "We also support charges in the following three-decimal currencies:
664
+ * Bahraini dinar (BHD), Jordanian dinar (JOD), Kuwaiti dinar (KWD), Omani rial (OMR), Tunisian
665
+ * dinar (TND)"; no other country's list on docs.stripe.com/currencies carries them.
666
+ * PayPal: developer.paypal.com/reference/currency-codes (retrieved 2026-10-07) footnotes BRL, CNY
667
+ * and MYR as supported "only for in-country PayPal accounts" (Medusa models the conservative
668
+ * reading: an out-of-country account cannot charge them; unverified by a sandbox probe).
669
+ */
670
+ const PRESENTMENT_REQUIRES_ACCOUNT_COUNTRY = {
671
+ stripe: {
672
+ BHD: ["AE"],
673
+ JOD: ["AE"],
674
+ KWD: ["AE"],
675
+ OMR: ["AE"],
676
+ TND: ["AE"]
677
+ },
678
+ paypal: {
679
+ BRL: ["BR"],
680
+ CNY: ["CN"],
681
+ MYR: ["MY"]
682
+ }
683
+ };
684
+ /**
685
+ * Account countries whose card processing takes only the listed currencies.
686
+ * Airwallex: airwallex.com/docs/payments/payment-methods/global/cards (retrieved 2026-10-26,
687
+ * matching Medusa's 2026-10-07 copy): "Supported processing currencies can differ based on where
688
+ * your business is registered and the card scheme." US and MX are listed below; AU, CA, SG, HK,
689
+ * NZ, JP, MY, the EEA, CH, UK and IL take "Visa, MasterCard: All currencies".
690
+ */
691
+ const PRESENTMENT_ACCOUNT_COUNTRY_ALLOWS_ONLY = { airwallex: {
692
+ US: [
693
+ "USD",
694
+ "EUR",
695
+ "GBP",
696
+ "AUD",
697
+ "SGD",
698
+ "JPY",
699
+ "CHF",
700
+ "NOK",
701
+ "DKK",
702
+ "CAD",
703
+ "NZD",
704
+ "HKD",
705
+ "KRW",
706
+ "PHP",
707
+ "MYR",
708
+ "TWD",
709
+ "VND",
710
+ "EGP",
711
+ "SAR",
712
+ "THB",
713
+ "MXN",
714
+ "TRY",
715
+ "INR",
716
+ "BRL",
717
+ "CNY",
718
+ "PKR",
719
+ "PLN",
720
+ "SEK",
721
+ "NGN",
722
+ "AED",
723
+ "IDR",
724
+ "BDT",
725
+ "ILS",
726
+ "ISK",
727
+ "MAD",
728
+ "ZAR",
729
+ "CZK",
730
+ "HUF",
731
+ "RON",
732
+ "CLP",
733
+ "QAR",
734
+ "COP",
735
+ "RUB",
736
+ "MWK",
737
+ "PEN",
738
+ "BGN",
739
+ "UAH",
740
+ "ARS",
741
+ "MOP",
742
+ "HRK",
743
+ "KZT",
744
+ "ALL",
745
+ "CRC",
746
+ "XOF",
747
+ "KES",
748
+ "UYU",
749
+ "LKR",
750
+ "BOB",
751
+ "PAB",
752
+ "GTQ",
753
+ "DOP",
754
+ "MMK",
755
+ "RSD",
756
+ "BZD",
757
+ "DZD",
758
+ "SHP",
759
+ "HNL",
760
+ "GEL",
761
+ "BAM",
762
+ "MKD",
763
+ "JMD",
764
+ "AWG",
765
+ "BBD",
766
+ "TTD",
767
+ "PYG",
768
+ "XAF"
769
+ ],
770
+ MX: ["MXN"]
771
+ } };
681
772
  /** Stripe's not-two-decimal set (capabilities/stripe-connect.ts:47), incl. the UAE-only five. */
682
773
  const STRIPE_NOT_TWO_DECIMAL = [
683
774
  "BIF",
@@ -888,6 +979,101 @@ const AUTHORIZE_NET_CURRENCIES = [
888
979
  "PLN"
889
980
  ];
890
981
  const VND_ONLY = ["VND"];
982
+ /**
983
+ * The account countries a PSP onboards, for PSPs whose accounts each charge a per-country list.
984
+ * Paystack: support.paystack.com/en/articles/2130690 "Enabling international payments for your
985
+ * business" (retrieved 2026-10-26): "Côte d'Ivoire XOF", "Ghana GHS", "Kenya KES, USD", "Nigeria
986
+ * NGN, USD", "South Africa ZAR", and "At the moment, only Kenya and Nigeria-based businesses have
987
+ * the ability to accept payments for their business in USD and be paid out in USD". USD has to be
988
+ * enabled on the account first (the article is how to enable it), and no Paystack API reports
989
+ * whether it is, so USD is `optIn`, never in `currencies`.
990
+ */
991
+ const PSP_ACCOUNT_COUNTRIES = { paystack: {
992
+ CI: { currencies: ["XOF"] },
993
+ GH: { currencies: ["GHS"] },
994
+ KE: {
995
+ currencies: ["KES"],
996
+ optIn: ["USD"]
997
+ },
998
+ NG: {
999
+ currencies: ["NGN"],
1000
+ optIn: ["USD"]
1001
+ },
1002
+ ZA: { currencies: ["ZAR"] }
1003
+ } };
1004
+ /**
1005
+ * The currency every account of a multi-currency PSP charges without enabling anything; any other
1006
+ * needs the account's international or multi-currency add-on. The single-currency PSPs (MoMo,
1007
+ * VNPay, Payoo: `VND_ONLY`) are not restated here: their home currency is their one currency.
1008
+ * Razorpay: razorpay.com/docs/payments/international-payments (retrieved 2026-10-26) says "The final
1009
+ * settlement is done in the bank accounts of Indian businesses in INR", with international
1010
+ * payments activated under Account & Settings, and razorpay.com/docs/sg/payments/international-payments/faqs
1011
+ * (retrieved 2026-10-27): "The Settlement currency is INR (Indian rupees) for all transactions made
1012
+ * using Razorpay". This is the settlement currency only; it says nothing about where the account is
1013
+ * registered (Razorpay has no entry in `PSP_HOME_COUNTRY`). iyzico help centre (quoted in Medusa
1014
+ * `capabilities/iyzico.ts`, 2026-10-07; re-read 2026-10-26): "payments you take in foreign
1015
+ * currencies are exchanged into Turkish Liras", taking them needs the paid Multi Currency add-on.
1016
+ */
1017
+ const PSP_HOME_CURRENCY = {
1018
+ razorpay: "INR",
1019
+ iyzico: "TRY"
1020
+ };
1021
+ /**
1022
+ * The one country every account of a PSP is registered in, for PSPs whose accounts are all
1023
+ * registered in a single country. Razorpay is left out: the same FAQ lists it as available in India,
1024
+ * Malaysia (Curlec), Singapore and the United States, so its accounts are not all registered in one
1025
+ * country, and a Singapore or US account must not read as IN. iyzico: consistent with orch `account-facts.ts`
1026
+ * `PROVIDER_CONSTANT_FACTS` (2026-10-26), which records TR, and with its Turkish-lira settlement
1027
+ * above. MoMo, VNPay, Payoo: domestic Vietnamese rails that take VND only (`VND_ONLY`), carried over
1028
+ * from the same `PROVIDER_CONSTANT_FACTS`.
1029
+ */
1030
+ const PSP_HOME_COUNTRY = {
1031
+ iyzico: "TR",
1032
+ momo: "VN",
1033
+ vnpay: "VN",
1034
+ payoo: "VN"
1035
+ };
1036
+ /**
1037
+ * The currency of a saved-method verification charge when the caller has none, where the PSP
1038
+ * documents one. `only`: the PSP verifies in that currency alone, so any other is refused.
1039
+ * Razorpay: recurring payment orders take a currency where "currently, only INR is allowed"
1040
+ * (razorpay.com/docs/api/payments/recurring-payments/emandate/create-subsequent-payments, retrieved
1041
+ * 2026-10-26), so its verification is INR only. Mollie: docs.mollie.com/docs/multicurrency
1042
+ * (retrieved 2026-10-27) lists EUR as supported by "All payment methods except BACS, Blik, Swish
1043
+ * and Twint", so every card method takes EUR and a card verification can fall back to it. Adyen
1044
+ * and Paystack publish no such currency: Adyen's zero-amount verification takes whatever the
1045
+ * merchant account processes, and a Paystack account charges its own country's currency, so their
1046
+ * callers must pass one.
1047
+ */
1048
+ const VERIFICATION_CURRENCY = {
1049
+ razorpay: {
1050
+ code: "INR",
1051
+ only: true
1052
+ },
1053
+ mollie: {
1054
+ code: "EUR",
1055
+ only: false
1056
+ }
1057
+ };
1058
+ /**
1059
+ * The PSPs whose wire amount Medusa computed in floating point before it adopted 0.2.0: major
1060
+ * units as `(stored / storageScale).toFixed(wireDigits)` on a double (Airwallex and Payoo then
1061
+ * `Number(...)` of that text). Source: Medusa `src/common/money/codec.ts` `majorStringCodec` /
1062
+ * `majorNumberCodec` and `psp-codecs.ts` (staging 785bd0420, 2026-10-27), and the same expression
1063
+ * in each provider's `amount.ts` before that codec existed (`03989fd4e^`). The stored amount was
1064
+ * `Number()` of Medusa's amount, unrounded. At an exact tie the double can sit just below it, so
1065
+ * USD 1072.5 went as `'10.72'` where 0.2.0 sends `'10.73'`; `sameWireAmount`'s
1066
+ * `acceptLegacyFloatRounding` reads that spelling. Payoo is listed because it used the same codec,
1067
+ * but its whole-dong rule leaves it no tie, so its float spelling is always the exact one.
1068
+ */
1069
+ const PSP_LEGACY_FLOAT_TO_FIXED = [
1070
+ "paypal",
1071
+ "mollie",
1072
+ "airwallex",
1073
+ "iyzico",
1074
+ "authorize-net",
1075
+ "payoo"
1076
+ ];
891
1077
  /** orch `MINIMUM_AMOUNT_BY_CURRENCY`, majors. */
892
1078
  const PSP_MINIMUM_MAJOR = { paystack: { USD: "2.00" } };
893
1079
  /** RBI e-mandate AFA-free cap: INR 15,000 (razorpay-vault `15_000_00` paise, seam `1_500_000`). */
@@ -902,8 +1088,29 @@ const PROVIDER_ONLY_CURRENCIES = { CNH: {
902
1088
  name: "Chinese Yuan (offshore)",
903
1089
  psps: ["adyen"]
904
1090
  } };
905
- //#endregion
906
- //#region src/wire/currency.ts
1091
+ // src/wire/anet.ts
1092
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1093
+ /**
1094
+ * The first `"<field>": <decimal>` (quoted or not) in a raw JSON body, verbatim: `JSON.parse`
1095
+ * would turn the gateway's `43.00` into `43`. The value is the whole JSON token, held to
1096
+ * `parseDecimal`'s `'plain'` grammar (`1e3` or `"43.10abc"` is not a decimal literal).
1097
+ * Undefined when absent; there is no lossy fallback.
1098
+ */
1099
+ function jsonDecimalLiteral(rawBody, field) {
1100
+ if (typeof rawBody !== "string" || typeof field !== "string") return void 0;
1101
+ const tokens = rawBody.matchAll(new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*("?)([^"\\s,}\\]]+)\\1`, "g"));
1102
+ for (const [, , token] of tokens) if (require_money.parseDecimal(token, "plain")) return token;
1103
+ }
1104
+ const isPositiveLiteral = (text) => {
1105
+ const value = require_money.parseDecimal(text.trim());
1106
+ return value !== null && value.units > 0n;
1107
+ };
1108
+ function authorizeNetReversibleAmount(details) {
1109
+ const settle = details.settleAmount ?? void 0;
1110
+ if (settle !== void 0 && isPositiveLiteral(settle)) return settle;
1111
+ return details.authAmount ?? settle;
1112
+ }
1113
+ // src/wire/currency.ts
907
1114
  /** The registry entry (any case, historic included) for `input`, else null. Never throws. */
908
1115
  function wireCurrency(input) {
909
1116
  return require_money.findCurrency(input, { allowHistoric: true });
@@ -933,8 +1140,42 @@ function wireStored(m) {
933
1140
  units: BigInt(m.amount)
934
1141
  };
935
1142
  }
936
- //#endregion
937
- //#region src/wire/descriptors.ts
1143
+ const SAFE_LIMIT = BigInt(Number.MAX_SAFE_INTEGER);
1144
+ /**
1145
+ * A Money's or FractionalMoney's currency entry and its exact stored amount, normalized (scale 0
1146
+ * when whole, so a whole amount reads exactly as `wireStored` reads it). A number must be finite
1147
+ * and safe when whole, text plain decimal; the amount must round within the safe integers.
1148
+ */
1149
+ function wireStoredDecimal(m) {
1150
+ const info = resolveWireCurrency(m.currency);
1151
+ const { amount } = m;
1152
+ if (typeof amount === "string") {
1153
+ const parsed = require_money.parseDecimal(amount, "plain");
1154
+ if (parsed === null) throw new require_money.MoneyError("amount_invalid", `Stored amount must be plain decimal text, got ${require_money.describeValue(amount)}`);
1155
+ if (!require_money.unitsWithin(require_money.roundToScale(parsed, 0, "up"), SAFE_LIMIT)) throw new require_money.MoneyError("amount_invalid", `Stored amount is beyond the safe integers: ${amount}`);
1156
+ return {
1157
+ info,
1158
+ stored: require_money.normalizeDecimal(parsed)
1159
+ };
1160
+ }
1161
+ const parsed = typeof amount === "number" ? require_money.strictNumberDecimal(amount) : null;
1162
+ if (parsed === null) throw new require_money.MoneyError("amount_invalid", `Stored amount must be a safe integer, got ${require_money.describeValue(amount)}`);
1163
+ return {
1164
+ info,
1165
+ stored: require_money.normalizeDecimal(parsed)
1166
+ };
1167
+ }
1168
+ // src/wire/descriptors.ts
1169
+ function accountCountries(id) {
1170
+ const table = PSP_ACCOUNT_COUNTRIES[id] ?? {};
1171
+ return new Map(Object.keys(table).map((country) => {
1172
+ const entry = {
1173
+ currencies: new Set(table[country].currencies),
1174
+ optIn: new Set(table[country].optIn ?? [])
1175
+ };
1176
+ return [country, Object.freeze(entry)];
1177
+ }));
1178
+ }
938
1179
  function digitTable(groups) {
939
1180
  const table = /* @__PURE__ */ new Map();
940
1181
  for (const digits of [
@@ -957,6 +1198,7 @@ const vndOnlyFacade = (code) => code === "VND";
957
1198
  const NONE = /* @__PURE__ */ new Set();
958
1199
  const NO_DIGITS = /* @__PURE__ */ new Map();
959
1200
  function describe(id, input) {
1201
+ const currencies = new Set(input.currencies);
960
1202
  return Object.freeze({
961
1203
  id,
962
1204
  tag: input.tag,
@@ -964,7 +1206,9 @@ function describe(id, input) {
964
1206
  gated: input.gated ?? false,
965
1207
  encodeGated: input.encodeGated ?? false,
966
1208
  preCutoverNeedsFactorOne: input.preCutoverNeedsFactorOne ?? false,
967
- currencies: new Set(input.currencies),
1209
+ roundsToStoredUnitFirst: input.roundsToStoredUnitFirst ?? false,
1210
+ currencies,
1211
+ presentmentCurrencies: input.presentmentCurrencies ? new Set(input.presentmentCurrencies) : currencies,
968
1212
  digits: input.digits ?? NO_DIGITS,
969
1213
  sentAsTwoDecimal: input.sentAsTwoDecimal ? new Set(input.sentAsTwoDecimal) : NONE,
970
1214
  sentAsZeroDecimal: input.sentAsZeroDecimal ? new Set(input.sentAsZeroDecimal) : NONE,
@@ -973,7 +1217,12 @@ function describe(id, input) {
973
1217
  singleCurrency: input.singleCurrency ?? null,
974
1218
  unsupported: input.unsupported ?? genericUnsupported(input.tag),
975
1219
  facadeAccepts: input.facadeAccepts ?? scalesAnyCurrency,
976
- inboundWholePositive: input.inboundWholePositive ?? false
1220
+ inboundWholePositive: input.inboundWholePositive ?? false,
1221
+ homeCurrency: PSP_HOME_CURRENCY[id] ?? input.singleCurrency ?? null,
1222
+ homeCountry: PSP_HOME_COUNTRY[id] ?? null,
1223
+ accountCountries: accountCountries(id),
1224
+ verificationCurrency: VERIFICATION_CURRENCY[id] ?? null,
1225
+ legacyFloatToFixed: PSP_LEGACY_FLOAT_TO_FIXED.includes(id)
977
1226
  });
978
1227
  }
979
1228
  const notTwoDecimalAtStripe = new Set(STRIPE_NOT_TWO_DECIMAL);
@@ -996,7 +1245,9 @@ const PSP_DESCRIPTORS = Object.freeze({
996
1245
  family: "iso-minor",
997
1246
  gated: true,
998
1247
  encodeGated: true,
1248
+ roundsToStoredUnitFirst: true,
999
1249
  currencies: STRIPE_SENDABLE,
1250
+ presentmentCurrencies: STRIPE_PRESENTMENT_CURRENCIES,
1000
1251
  sentAsTwoDecimal: STRIPE_SENT_AS_TWO_DECIMAL
1001
1252
  }),
1002
1253
  adyen: describe("adyen", {
@@ -1004,6 +1255,7 @@ const PSP_DESCRIPTORS = Object.freeze({
1004
1255
  family: "iso-minor",
1005
1256
  gated: true,
1006
1257
  encodeGated: true,
1258
+ roundsToStoredUnitFirst: true,
1007
1259
  currencies: digitTable(ADYEN_CURRENCY_DIGITS).keys(),
1008
1260
  digits: digitTable(ADYEN_CURRENCY_DIGITS),
1009
1261
  sentAsTwoDecimal: ADYEN_SENT_AS_TWO_DECIMAL,
@@ -1123,8 +1375,7 @@ function pspDescriptor(psp) {
1123
1375
  if (typeof psp === "string" && require_own.hasOwnKey(PSP_DESCRIPTORS, psp)) return PSP_DESCRIPTORS[psp];
1124
1376
  throw new require_money.MoneyError("wire_unsupported", `Unknown payment provider: ${require_money.describeValue(psp)}`);
1125
1377
  }
1126
- //#endregion
1127
- //#region src/wire/marker.ts
1378
+ // src/wire/marker.ts
1128
1379
  const WIRE_SCALE_KEY = "amboras_wire_scale";
1129
1380
  const WIRE_SCALE_ISO_MINOR_V2 = "iso-minor-v2";
1130
1381
  const WIRE_SCALE_MARKER = Object.freeze({ [WIRE_SCALE_KEY]: WIRE_SCALE_ISO_MINOR_V2 });
@@ -1143,8 +1394,19 @@ function conventionKind(value) {
1143
1394
  const kind = typeof value === "object" && value !== null ? value.kind : void 0;
1144
1395
  return kind === "iso-minor-v2" || kind === "pre-cutover" ? kind : null;
1145
1396
  }
1146
- //#endregion
1147
- //#region src/wire/codec.ts
1397
+ const WIRE_ROUNDING_KEY = "amboras_wire_rounding";
1398
+ const WIRE_ROUNDING_EXACT = "exact";
1399
+ const WIRE_ROUNDING_MARKER = Object.freeze({ [WIRE_ROUNDING_KEY]: WIRE_ROUNDING_EXACT });
1400
+ /**
1401
+ * Whether a payment may be held at the PSP in Medusa's pre-0.2.0 float spelling: no record is a
1402
+ * non-null object whose `amboras_wire_rounding` is strictly `'exact'`. Pass the result as
1403
+ * `sameWireAmount`'s `acceptLegacyFloatRounding`, so only sessions created before the deploy get
1404
+ * the legacy spelling. Reads already fetched records only.
1405
+ */
1406
+ function mayHoldLegacyFloatRounding(...records) {
1407
+ return !records.some((r) => typeof r === "object" && r !== null && r["amboras_wire_rounding"] === "exact");
1408
+ }
1409
+ // src/wire/codec.ts
1148
1410
  const isBlank = (value) => value === null || value === void 0 || typeof value === "string" && value.trim() === "";
1149
1411
  const shownCode = (currency) => typeof currency === "string" ? currency.trim().toUpperCase() : require_money.describeValue(currency);
1150
1412
  function missingCurrency(d) {
@@ -1175,7 +1437,8 @@ function pspCurrencies(psp) {
1175
1437
  /**
1176
1438
  * `currency` as a CurrencyCode `psp` takes. Empty throws `currency_missing` (`[<Tag>] Cannot
1177
1439
  * scale an amount without a currency code; ...`); anything the PSP does not take throws
1178
- * `wire_unsupported` with the provider's own message.
1440
+ * `wire_unsupported` with the provider's own message. Typed as the PSP's literal union where it
1441
+ * has a short fixed list (`assertPspCurrency('authorize-net', c)` is an `AuthorizeNetCurrency`).
1179
1442
  */
1180
1443
  function assertPspCurrency(psp, currency) {
1181
1444
  const d = pspDescriptor(psp);
@@ -1206,6 +1469,28 @@ function pspDefaultCurrency(psp) {
1206
1469
  const single = pspDescriptor(psp).singleCurrency;
1207
1470
  return single === null ? null : single;
1208
1471
  }
1472
+ /**
1473
+ * The currency every account of `psp` charges without enabling anything (any other needs the
1474
+ * account's international or multi-currency add-on, or is never taken): Razorpay INR, iyzico TRY,
1475
+ * and for the single-currency PSPs their one currency (MoMo, VNPay and Payoo VND). Null for PSPs
1476
+ * whose accounts have no single home currency (Stripe, Adyen, PayPal, Mollie, Airwallex, Authorize.Net, and Paystack, whose home currency is the
1477
+ * account country's: `pspPresentment('paystack').accountCountries`). Unlike `pspDefaultCurrency`,
1478
+ * which is non-null only for the single-currency PSPs, a home currency does not mean the PSP takes
1479
+ * nothing else.
1480
+ */
1481
+ function pspHomeCurrency(psp) {
1482
+ const home = pspDescriptor(psp).homeCurrency;
1483
+ return home === null ? null : home;
1484
+ }
1485
+ /**
1486
+ * The one country (ISO 3166 alpha-2) every account of `psp` is registered in, for PSPs whose
1487
+ * accounts are all registered in a single country: iyzico `'TR'`, MoMo, VNPay and Payoo `'VN'`. Null
1488
+ * for PSPs available in several countries: Razorpay (India, Malaysia, Singapore, the United States)
1489
+ * and Paystack (its country follows the account: `pspAccountCountryForCurrency`).
1490
+ */
1491
+ function pspHomeCountry(psp) {
1492
+ return pspDescriptor(psp).homeCountry;
1493
+ }
1209
1494
  function factorOf(d, info) {
1210
1495
  if (d.sentAsTwoDecimal.has(info.code)) return 1n;
1211
1496
  if (d.sentAsZeroDecimal.has(info.code)) return 100n;
@@ -1258,32 +1543,39 @@ function requireConvention(d, convention) {
1258
1543
  }
1259
1544
  function encodeWire(psp, m, options = {}) {
1260
1545
  const d = pspDescriptor(psp);
1261
- const { info, units } = wireStored(m);
1262
- const value = {
1263
- units,
1264
- scale: require_money.storageExponent(info)
1265
- };
1546
+ const { info, stored: held } = wireStoredDecimal(m);
1266
1547
  facadeCurrency(d, info.code);
1267
1548
  const label = options.label ?? "amount";
1268
- const stored = value.units;
1269
- if (stored < 0n || stored === 0n && !options.allowZero) throw new require_money.MoneyError("wire_refused", `[${d.tag}] Invalid ${label}: ${m.amount}`);
1549
+ const shown = require_money.formatDecimal(held);
1550
+ const exponent = require_money.storageExponent(info);
1551
+ const majorText = require_money.formatDecimal({
1552
+ units: held.units,
1553
+ scale: exponent + held.scale
1554
+ });
1555
+ if (held.units < 0n || held.units === 0n && !options.allowZero) throw new require_money.MoneyError("wire_refused", `[${d.tag}] Invalid ${label}: ${shown}`);
1556
+ const stored = d.roundsToStoredUnitFirst ? require_money.roundToScale(held, 0, "half-up") : held;
1557
+ const positive = held.units > 0n;
1270
1558
  const factor = d.family === "iso-minor" ? factorOf(d, info) : 1n;
1271
1559
  if (d.encodeGated && requireConvention(d, options.convention) === "pre-cutover") {
1272
- if (d.preCutoverNeedsFactorOne && require_money.divRound(stored, factor, "half-up") !== stored) throw new require_money.MoneyError("wire_refused", preCutoverRefusal(d.tag, info.code));
1273
- return m.amount;
1560
+ if (!(stored.scale === 0 && (!d.preCutoverNeedsFactorOne || require_money.divRound(stored.units, factor, "half-up") === stored.units))) throw new require_money.MoneyError("wire_refused", preCutoverRefusal(d.tag, info.code));
1561
+ if (stored.units === 0n && positive) throw subUnitRefusal(d, info, majorText, exponent, label);
1562
+ return Number(stored.units);
1274
1563
  }
1275
- if (d.wholeUnit?.applies(info) && stored % BigInt(info.storageScale) !== 0n) throw new require_money.MoneyError("wire_refused", d.wholeUnit.message(info, m.amount, label, require_money.formatDecimal(value)));
1564
+ if (d.wholeUnit?.applies(info) && stored.units % (BigInt(info.storageScale) * require_money.pow10(stored.scale)) !== 0n) throw new require_money.MoneyError("wire_refused", d.wholeUnit.message(info, shown, label, majorText));
1276
1565
  const digits = wireDigitsOf(d, info);
1277
1566
  if (d.family === "iso-minor") {
1278
- const exact = require_money.divRound(stored, factor, "half-up");
1279
- if (exact === 0n && stored > 0n) throw subUnitRefusal(d, info, require_money.formatDecimal(value), digits, label);
1567
+ const exact = require_money.divRound(stored.units, factor * require_money.pow10(stored.scale), "half-up");
1568
+ if (exact === 0n && positive) throw subUnitRefusal(d, info, majorText, digits, label);
1280
1569
  if (!d.threeDecimalStepOfTen || d.digits.get(info.code) !== 3) return Number(exact);
1281
1570
  const stepped = exact - exact % 10n;
1282
- if (stepped === 0n && exact > 0n) throw new require_money.MoneyError("wire_refused", roundsToNothing(d.tag, info.code, require_money.formatDecimal(value)));
1571
+ if (stepped === 0n && exact > 0n) throw new require_money.MoneyError("wire_refused", roundsToNothing(d.tag, info.code, majorText));
1283
1572
  return Number(stepped);
1284
1573
  }
1285
- const rounded = require_money.roundToScale(value, digits, "half-up");
1286
- if (rounded.units === 0n && stored > 0n) throw subUnitRefusal(d, info, require_money.formatDecimal(value), digits, label);
1574
+ const rounded = require_money.roundToScale({
1575
+ units: stored.units,
1576
+ scale: exponent + stored.scale
1577
+ }, digits, "half-up");
1578
+ if (rounded.units === 0n && positive) throw subUnitRefusal(d, info, majorText, digits, label);
1287
1579
  const text = require_money.formatDecimal(rounded);
1288
1580
  return d.family === "major-number" ? Number(text) : text;
1289
1581
  }
@@ -1305,11 +1597,13 @@ function roundsToNothing(tag, code, major) {
1305
1597
  function wireDecimal(wire) {
1306
1598
  return typeof wire === "number" ? require_money.strictNumberDecimal(wire) : require_money.parseDecimal(wire, "plain");
1307
1599
  }
1308
- function decodeWith(d, w, info, kind) {
1600
+ /** `w` at storage scale, half-up; with `exact`, a wire naming a fraction of a storage unit throws `amount_invalid`. */
1601
+ function decodeWith(d, w, info, kind, exact = false) {
1309
1602
  const scale = require_money.pow10(w.scale);
1310
1603
  let multiplier;
1311
1604
  if (d.family !== "iso-minor") multiplier = BigInt(info.storageScale);
1312
1605
  else multiplier = kind === "pre-cutover" ? 1n : factorOf(d, info);
1606
+ if (exact && w.units * multiplier % scale !== 0n) throw new require_money.MoneyError("amount_invalid", `[${d.tag}] ${require_money.formatDecimal(w)} is not a whole ${info.code} storage amount`);
1313
1607
  return require_money.boundedMoney(require_money.mulDiv(w.units, multiplier, scale, "half-up"), info);
1314
1608
  }
1315
1609
  /** VNPay: a positive whole safe integer, as Medusa's `vnpayWireAmountToStorage` reads one. */
@@ -1318,7 +1612,7 @@ function isWholePositive(w) {
1318
1612
  return w.units > 0n && w.units % scale === 0n && w.units / scale <= BigInt(Number.MAX_SAFE_INTEGER);
1319
1613
  }
1320
1614
  /** Every inbound check: facade currency gate, convention, strict wire text, VNPay's "no amount", the sign. */
1321
- function decodeChecked(d, wire, currency, convention, allowNegative) {
1615
+ function decodeChecked(d, wire, currency, convention, allowNegative, exact = false) {
1322
1616
  if (isBlank(currency)) throw missingCurrency(d);
1323
1617
  const info = facadeCurrency(d, currency);
1324
1618
  const kind = d.gated ? requireConvention(d, convention) : null;
@@ -1326,10 +1620,20 @@ function decodeChecked(d, wire, currency, convention, allowNegative) {
1326
1620
  if (!w) throw new require_money.MoneyError("amount_invalid", `[${d.tag}] Not a wire amount: ${require_money.describeValue(wire)}`);
1327
1621
  if (d.inboundWholePositive && !isWholePositive(w)) throw new require_money.MoneyError("amount_invalid", `[${d.tag}] No amount: ${d.tag} amounts are positive whole numbers, got ${require_money.describeValue(wire)}`);
1328
1622
  if (w.units < 0n && !allowNegative) throw new require_money.MoneyError("amount_invalid", `[${d.tag}] Negative wire amount: ${require_money.describeValue(wire)}`);
1329
- return decodeWith(d, w, info, kind);
1623
+ return decodeWith(d, w, info, kind, exact);
1330
1624
  }
1331
- function decodeWire(psp, wire, currency, convention) {
1332
- return decodeChecked(pspDescriptor(psp), wire, currency, convention, true);
1625
+ function decodeOptions(d, options, extra) {
1626
+ const notAnOptionsObject = options !== void 0 && (typeof options !== "object" || options === null || require_own.hasOwnKey(options, "kind"));
1627
+ if (extra.length > 0 || notAnOptionsObject) {
1628
+ const message = `[${d.tag}] decodeWire takes one options object ({ convention, allowNegative }) as its fourth argument.`;
1629
+ throw new require_money.MoneyError("wire_refused", message);
1630
+ }
1631
+ return options ?? {};
1632
+ }
1633
+ function decodeWire(psp, wire, currency, options, ...extra) {
1634
+ const d = pspDescriptor(psp);
1635
+ const { convention, allowNegative } = decodeOptions(d, options, extra);
1636
+ return decodeChecked(d, wire, currency, convention, allowNegative === true);
1333
1637
  }
1334
1638
  /**
1335
1639
  * `decodeWire` that never throws: undefined wherever it would throw (null, undefined, `''`,
@@ -1344,13 +1648,39 @@ function tryDecodeWire(psp, wire, currency, options = {}) {
1344
1648
  return;
1345
1649
  }
1346
1650
  }
1347
- /** Internal: the major-unit text of `m` at its display digits (the pre-PSP-table ISO spelling). */
1651
+ /**
1652
+ * Internal: `tryDecodeWire` (no negatives) that is also undefined for a wire naming a fraction of a
1653
+ * storage unit (Airwallex 43.004 is not USD 43.00): the comparators' reading of a reported amount.
1654
+ */
1655
+ function tryDecodeWireExactly(psp, wire, currency, convention) {
1656
+ try {
1657
+ return decodeChecked(pspDescriptor(psp), wire, currency, convention, false, true);
1658
+ } catch {
1659
+ return;
1660
+ }
1661
+ }
1662
+ /** Internal: the major-unit text of `m` at its display digits, half-up (the pre-PSP-table ISO spelling). */
1348
1663
  function legacyIsoMajor(m) {
1349
- const { info, units } = wireStored(m);
1350
- return require_money.formatDecimal(require_money.roundToScale({
1351
- units,
1352
- scale: require_money.storageExponent(info)
1353
- }, info.displayDigits, "half-up"));
1664
+ const { info, stored } = wireStoredDecimal(m);
1665
+ const major = {
1666
+ units: stored.units,
1667
+ scale: require_money.storageExponent(info) + stored.scale
1668
+ };
1669
+ return require_money.formatDecimal(require_money.roundToScale(major, info.displayDigits, "half-up"));
1670
+ }
1671
+ /**
1672
+ * Internal: `m` as Medusa's pre-0.2.0 float codec spelled it for a `legacyFloatToFixed` PSP, at
1673
+ * the PSP's wire digits, or with `isoDigits` at the display digits (the pre-PSP-table spelling):
1674
+ * `(Number(amount) / storageScale).toFixed(digits)`, computed on a double on purpose, because
1675
+ * that is what the PSP holds for a payment created before 0.2.0 (USD 1072.5 is `'10.72'`).
1676
+ * Null for a PSP that never encoded in floating point.
1677
+ */
1678
+ function legacyFloatMajor(psp, m, isoDigits) {
1679
+ const d = pspDescriptor(psp);
1680
+ if (!d.legacyFloatToFixed) return null;
1681
+ const { info } = wireStoredDecimal(m);
1682
+ const digits = isoDigits ? info.displayDigits : wireDigitsOf(d, info);
1683
+ return (Number(m.amount) / info.storageScale).toFixed(digits);
1354
1684
  }
1355
1685
  /** Internal: one unit of the PSP's last wire digit as Money. */
1356
1686
  function wireUnit(psp, currency, convention) {
@@ -1362,8 +1692,7 @@ function wireUnit(psp, currency, convention) {
1362
1692
  }, info, d.gated ? requireConvention(d, convention) : null);
1363
1693
  return require_money.boundedMoney(require_iso_minor.isoMinorFactor(info, wireDigitsOf(d, info)), info);
1364
1694
  }
1365
- //#endregion
1366
- //#region src/wire/breakdown.ts
1695
+ // src/wire/breakdown.ts
1367
1696
  /**
1368
1697
  * `total` and its `parts` (same currency, non-negative, adding up to `total`) as integer wire
1369
1698
  * amounts whose sum is exactly `encodeWire(psp, total)`: the wire total split by largest
@@ -1394,13 +1723,14 @@ function wireBreakdown(psp, total, parts, convention) {
1394
1723
  parts: out
1395
1724
  };
1396
1725
  }
1397
- //#endregion
1398
- //#region src/wire/compare.ts
1726
+ // src/wire/compare.ts
1399
1727
  /**
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.
1728
+ * Whether a PSP-reported amount is `expected`, a Money or a FractionalMoney (Medusa's unrounded
1729
+ * session total). The expected wire is `encodeWire(psp, expected, { convention, allowZero: true })`
1730
+ * (a refusal means false); the reported one must decode exactly (strict like `tryDecodeWire`:
1731
+ * never `''` or an exponent, no negatives, and never a fraction of a storage unit) to the same
1732
+ * stored amount, so `'44.00'` matches a sent `'44'` and Airwallex `43.004` does not match `43`.
1733
+ * Never throws.
1404
1734
  */
1405
1735
  function sameWireAmount(psp, reported, expected, options = {}) {
1406
1736
  const { convention } = options;
@@ -1408,10 +1738,13 @@ function sameWireAmount(psp, reported, expected, options = {}) {
1408
1738
  const reportedInfo = wireCurrency(options.reportedCurrency);
1409
1739
  if (reportedInfo === null || reportedInfo !== wireCurrency(expected.currency)) return false;
1410
1740
  }
1411
- const targets = expectedTargets(psp, expected, convention, options.acceptLegacyIsoDigits === true);
1741
+ const targets = expectedTargets(psp, expected, convention, {
1742
+ isoDigits: options.acceptLegacyIsoDigits === true,
1743
+ floatRounding: options.acceptLegacyFloatRounding === true
1744
+ });
1412
1745
  if (targets === null) return false;
1413
1746
  const { target, legacy } = targets;
1414
- const decode = (value) => tryDecodeWire(psp, value, expected.currency, { convention });
1747
+ const decode = (value) => options.textOnly && typeof value !== "string" ? void 0 : tryDecodeWireExactly(psp, value, expected.currency, convention);
1415
1748
  if (options.policy === "requested-exact-collected-covers" && options.requested !== void 0 && options.requested !== null) {
1416
1749
  const requested = decode(options.requested);
1417
1750
  const collected = decode(reported);
@@ -1420,10 +1753,14 @@ function sameWireAmount(psp, reported, expected, options = {}) {
1420
1753
  }
1421
1754
  const got = decode(reported);
1422
1755
  if (got === void 0) return false;
1423
- return got.amount === target.amount || got.amount === legacy?.amount;
1756
+ return got.amount === target.amount || legacy.some((m) => m.amount === got.amount);
1424
1757
  }
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) {
1758
+ /**
1759
+ * What `expected` decodes to once sent, and to in each accepted legacy spelling (the exact
1760
+ * ISO-digit one; the pre-0.2.0 float one at the wire digits, and at the ISO digits too when both
1761
+ * are accepted), or null when it cannot be sent.
1762
+ */
1763
+ function expectedTargets(psp, expected, convention, accept) {
1427
1764
  try {
1428
1765
  const d = pspDescriptor(psp);
1429
1766
  const wire = encodeWire(psp, expected, {
@@ -1431,21 +1768,22 @@ function expectedTargets(psp, expected, convention, acceptLegacy) {
1431
1768
  allowZero: true
1432
1769
  });
1433
1770
  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);
1771
+ const spellings = [];
1772
+ if (accept.isoDigits && d.family !== "iso-minor") spellings.push(legacyIsoMajor(expected));
1773
+ if (accept.floatRounding) spellings.push(legacyFloatMajor(psp, expected, false));
1774
+ if (accept.floatRounding && accept.isoDigits) spellings.push(legacyFloatMajor(psp, expected, true));
1775
+ const legacy = [];
1776
+ for (const spelling of spellings) if (spelling !== null && spelling !== String(wire)) legacy.push(decodeAt(psp, spelling, expected, convention));
1439
1777
  return {
1440
1778
  target,
1441
- legacy: spelling === String(wire) ? void 0 : decodeAt(psp, spelling, expected, convention)
1779
+ legacy
1442
1780
  };
1443
1781
  } catch {
1444
1782
  return null;
1445
1783
  }
1446
1784
  }
1447
1785
  function decodeAt(psp, wire, expected, convention) {
1448
- return decodeWire(psp, wire, expected.currency, convention);
1786
+ return decodeWire(psp, wire, expected.currency, { convention });
1449
1787
  }
1450
1788
  /**
1451
1789
  * One unit of the PSP's last wire digit, as Money: for ISO-minor PSPs `decodeWire(psp, 1)` (the
@@ -1459,8 +1797,7 @@ function oneWireUnit(psp, currency, convention) {
1459
1797
  function wireKeyPart(wire) {
1460
1798
  return String(wire);
1461
1799
  }
1462
- //#endregion
1463
- //#region src/data/destinations.ts
1800
+ // src/data/destinations.ts
1464
1801
  const pixel = (currencyCase, unit = "major-number") => ({
1465
1802
  unit,
1466
1803
  currencyCase,
@@ -1523,8 +1860,7 @@ const DESTINATIONS = {
1523
1860
  verified: true
1524
1861
  }
1525
1862
  };
1526
- //#endregion
1527
- //#region src/wire/destinations.ts
1863
+ // src/wire/destinations.ts
1528
1864
  function descriptorOf(dest) {
1529
1865
  if (typeof dest === "string" && require_own.hasOwnKey(DESTINATIONS, dest)) return DESTINATIONS[dest];
1530
1866
  throw new require_money.MoneyError("wire_unsupported", `Unknown destination: ${require_money.describeValue(dest)}`);
@@ -1571,8 +1907,141 @@ function eventMoney(m) {
1571
1907
  currency: require_money.medusaCurrencyCode(info.code)
1572
1908
  };
1573
1909
  }
1574
- //#endregion
1575
- //#region src/wire/provider-amount.ts
1910
+ // src/wire/legacy-refund.ts
1911
+ const minus = (a, b) => require_money.addDecimal(a, {
1912
+ units: -b.units,
1913
+ scale: b.scale
1914
+ });
1915
+ /**
1916
+ * The PSP's remaining amount, to refund instead of `refund`, when the PSP holds the capture only
1917
+ * in Medusa's pre-0.2.0 float spelling (`legacyFloatToFixed` PSPs: USD 1072.5 captured as
1918
+ * `'10.72'`, where today's spelling is `'10.73'`) and `refund` is Medusa's whole remaining amount
1919
+ * (`captured - refunded`, exactly). Medusa's remaining then encodes one wire unit above what the
1920
+ * PSP still holds (after a `'0.72'` refund, Medusa's 1000.5 is `'10.01'` against the PSP's
1921
+ * `'10.00'`), and the PSP refuses it (NOT_ALLOWED), so the refund could never complete.
1922
+ * Returns `{ wire, money }` for the PSP's remaining (`'10.00'`, USD 1000), never more than it and
1923
+ * never more than one legacy rounding below what `refund` encodes to. Null (send `refund` as
1924
+ * is) for every other case: a PSP that never encoded in floating point, a capture that matches
1925
+ * today's spelling or no spelling, a partial refund, a refund that already fits, mixed
1926
+ * currencies, and any amount or PSP fact that cannot be read. Never throws.
1927
+ */
1928
+ function legacyFloatRefundClamp(psp, input) {
1929
+ try {
1930
+ if (!pspDescriptor(psp).legacyFloatToFixed) return null;
1931
+ const { captured, refunded, refund } = input;
1932
+ const info = wireCurrency(captured.currency);
1933
+ if (info === null || wireCurrency(refunded.currency) !== info || wireCurrency(refund.currency) !== info) return null;
1934
+ const pspCaptured = tryDecodeWireExactly(psp, input.capturedWire, info.code, void 0);
1935
+ const pspRefunded = tryDecodeWireExactly(psp, input.refundedWire, info.code, void 0);
1936
+ if (pspCaptured === void 0 || pspRefunded === void 0) return null;
1937
+ if (!(!sameWireAmount(psp, input.capturedWire, captured) && sameWireAmount(psp, input.capturedWire, captured, { acceptLegacyFloatRounding: true }))) return null;
1938
+ const medusaRemaining = minus(wireStoredDecimal(captured).stored, wireStoredDecimal(refunded).stored);
1939
+ if (require_money.compareDecimal(wireStoredDecimal(refund).stored, medusaRemaining) !== 0) return null;
1940
+ const encode = encodeWire;
1941
+ const decode = decodeWire;
1942
+ const sent = decode(psp, encode(psp, refund, { label: "refund amount" }), info.code).amount;
1943
+ const pspRemaining = pspCaptured.amount - pspRefunded.amount;
1944
+ const legacyGap = decode(psp, encode(psp, captured), info.code).amount - pspCaptured.amount;
1945
+ if (pspRemaining <= 0 || sent <= pspRemaining || sent - pspRemaining > legacyGap) return null;
1946
+ const money = require_money.boundedMoney(BigInt(pspRemaining), info);
1947
+ return {
1948
+ wire: encode(psp, money, { label: "refund amount" }),
1949
+ money
1950
+ };
1951
+ } catch {
1952
+ return null;
1953
+ }
1954
+ }
1955
+ // src/wire/presentment.ts
1956
+ /** Registry codes only, sorted: a provider-only code (Adyen CNH) or one the registry does not know is left out. */
1957
+ function knownCodes(codes) {
1958
+ const known = [];
1959
+ for (const code of codes) {
1960
+ const info = wireCurrency(code);
1961
+ if (info) known.push(info.code);
1962
+ }
1963
+ return known.sort();
1964
+ }
1965
+ /**
1966
+ * The PSP's presentment rules, as data: the union of currencies some account can charge, the
1967
+ * currencies that need an account in particular countries, and the account countries limited to
1968
+ * a list. For capability declarations that hold the rules themselves; `pspPresentmentCurrencies`
1969
+ * applies them to one account. Fresh collections on every call. An unknown PSP throws
1970
+ * `wire_unsupported`.
1971
+ */
1972
+ function pspPresentment(psp) {
1973
+ const d = pspDescriptor(psp);
1974
+ const requires = PRESENTMENT_REQUIRES_ACCOUNT_COUNTRY[d.id] ?? {};
1975
+ const allowsOnly = PRESENTMENT_ACCOUNT_COUNTRY_ALLOWS_ONLY[d.id] ?? {};
1976
+ const countries = new Map([...d.accountCountries].map(([country, entry]) => {
1977
+ const lists = {
1978
+ currencies: new Set(knownCodes(entry.currencies)),
1979
+ optIn: new Set(knownCodes(entry.optIn))
1980
+ };
1981
+ return [country, Object.freeze(lists)];
1982
+ }));
1983
+ return Object.freeze({
1984
+ currencies: new Set(knownCodes(d.presentmentCurrencies)),
1985
+ currencyRequiresAccountCountry: new Map(knownCodes(Object.keys(requires)).map((code) => [code, new Set(requires[code])])),
1986
+ accountCountryAllowsOnly: new Map(Object.keys(allowsOnly).map((country) => [country, new Set(knownCodes(allowsOnly[country]))])),
1987
+ accountCountries: countries,
1988
+ optInCompanions: optInCompanionsOf(countries)
1989
+ });
1990
+ }
1991
+ /**
1992
+ * Internal: per currency that names one account country, that country's opt-in currencies. A
1993
+ * currency two countries share names neither, so it gets no companion (no Paystack currency is
1994
+ * shared today; the tests hold the rule with a fixture).
1995
+ */
1996
+ function optInCompanionsOf(countries) {
1997
+ const companions = /* @__PURE__ */ new Map();
1998
+ for (const entry of countries.values()) {
1999
+ if (entry.optIn.size === 0) continue;
2000
+ for (const code of entry.currencies) if (impliedCountry(countries, code) !== null) companions.set(code, new Set(entry.optIn));
2001
+ }
2002
+ return companions;
2003
+ }
2004
+ /** The one account country whose list (always or opt-in) holds `code`, else null. */
2005
+ function impliedCountry(countries, code) {
2006
+ const holders = [...countries].filter(([, entry]) => entry.currencies.has(code) || entry.optIn.has(code));
2007
+ return holders.length === 1 ? holders[0][0] : null;
2008
+ }
2009
+ /**
2010
+ * The account country a currency names for `psp`: the one country whose accounts present it
2011
+ * (Paystack NGN: `'NG'`, XOF: `'CI'`, the only XOF country Paystack onboards). Null when several
2012
+ * countries' accounts can present it (Paystack USD, on NG and KE accounts), when the PSP has no
2013
+ * per-country lists, or for an unknown code. Never throws for a bad code; an unknown PSP throws
2014
+ * `wire_unsupported`.
2015
+ */
2016
+ function pspAccountCountryForCurrency(psp, currency) {
2017
+ const d = pspDescriptor(psp);
2018
+ const info = wireCurrency(currency);
2019
+ return info === null ? null : impliedCountry(d.accountCountries, info.code);
2020
+ }
2021
+ /**
2022
+ * The currencies `psp` lets an account present (charge buyers in). Without `accountCountry`, the
2023
+ * union over every account country, opt-in currencies included; with it (ISO 3166 alpha-2, any
2024
+ * case), only what an account registered there can charge: Stripe's three-decimal five need a UAE
2025
+ * account, PayPal's BRL, CNY and MYR an in-country one, Airwallex cards on a US account take its
2026
+ * documented 76 and on an MX account MXN only, a Paystack account its country's currency (plus USD
2027
+ * on NG and KE with `includeOptIn`, and nothing for a country Paystack does not onboard). Not
2028
+ * `pspCurrencies`, which is what the codec scales and the facade lets through (Stripe's adds every
2029
+ * two-decimal ISO code it accepts on the wire).
2030
+ */
2031
+ function pspPresentmentCurrencies(psp, options = {}) {
2032
+ const rules = pspPresentment(psp);
2033
+ if (options.accountCountry === void 0) return rules.currencies;
2034
+ const country = options.accountCountry.trim().toUpperCase();
2035
+ const only = rules.accountCountryAllowsOnly.get(country);
2036
+ const listed = rules.accountCountries.get(country);
2037
+ const codes = [...rules.currencies].filter((code) => {
2038
+ const required = rules.currencyRequiresAccountCountry.get(code);
2039
+ const inCountryList = rules.accountCountries.size === 0 || listed !== void 0 && (listed.currencies.has(code) || options.includeOptIn === true && listed.optIn.has(code));
2040
+ return (required === void 0 || required.has(country)) && (only === void 0 || only.has(code)) && inCountryList;
2041
+ });
2042
+ return new Set(codes);
2043
+ }
2044
+ // src/wire/provider-amount.ts
1576
2045
  /**
1577
2046
  * A provider's major-unit amount (finite number, or text read by the separator engine, which
1578
2047
  * strips the currency's symbol and code) as Money, rounded once to the storage digits. Null on
@@ -1607,24 +2076,27 @@ function parseProviderAmount(value, currency, options = {}) {
1607
2076
  return null;
1608
2077
  }
1609
2078
  }
1610
- //#endregion
1611
- //#region src/wire/providers.ts
2079
+ // src/wire/providers.ts
1612
2080
  /** RBI e-mandate cap Razorpay token registrations carry as `max_amount`: INR 15,000 (1,500,000 paise = stored). */
1613
2081
  const RAZORPAY_AFA_FREE_CAP = Object.freeze(require_money.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
- */
2082
+ const VERIFICATION_AMOUNTS = {
2083
+ paystack: "one-major-unit",
2084
+ razorpay: "one-major-unit",
2085
+ mollie: "one-wire-unit",
2086
+ adyen: "zero"
2087
+ };
2088
+ function verificationMoney(psp, code) {
2089
+ const amount = VERIFICATION_AMOUNTS[psp];
2090
+ if (amount === "one-major-unit") return require_money.fromMajor("1", code);
2091
+ if (amount === "one-wire-unit") return wireUnit(psp, code);
2092
+ return require_money.boundedMoney(0n, resolveWireCurrency(code));
2093
+ }
1619
2094
  function verificationCharge(psp, currency) {
1620
- const code = assertPspCurrency(psp, currency);
1621
- let charge;
1622
- if (psp === "paystack") charge = require_money.fromMajor("1", code);
1623
- else if (psp === "razorpay") {
1624
- if (code !== "INR") throw new require_money.MoneyError("wire_unsupported", `[Razorpay] Verification charges are made in INR only, not ${code}.`);
1625
- charge = require_money.fromMajor("1", code);
1626
- } else if (psp === "mollie") charge = wireUnit("mollie", code);
1627
- else charge = require_money.boundedMoney(0n, resolveWireCurrency(code));
2095
+ const d = pspDescriptor(psp);
2096
+ const documented = d.verificationCurrency;
2097
+ const code = assertPspCurrency(psp, currency === void 0 ? documented?.code : currency);
2098
+ if (documented?.only && code !== documented.code) throw new require_money.MoneyError("wire_unsupported", `[${d.tag}] Verification charges are made in ${documented.code} only, not ${code}.`);
2099
+ const charge = verificationMoney(psp, code);
1628
2100
  return {
1629
2101
  wire: encodeWire(psp, charge, {
1630
2102
  convention: ISO_MINOR_V2,
@@ -1644,8 +2116,7 @@ function iyzicoPrice(wire) {
1644
2116
  const text = require_money.toDecimalText(parsed);
1645
2117
  return text.includes(".") ? text : `${text}.0`;
1646
2118
  }
1647
- //#endregion
1648
- //#region src/wire/slot.ts
2119
+ // src/wire/slot.ts
1649
2120
  /** `m` for a slot context's `money` key. */
1650
2121
  function slotMoney(m) {
1651
2122
  const { info } = require_money.storedDecimal(m);
@@ -1671,13 +2142,18 @@ function readSlotMoney(context) {
1671
2142
  const money = context.money;
1672
2143
  return (isRecord(money) ? read(money.amount, money.currency_code) : void 0) ?? read(context.total ?? context.price, context.currency_code, context.currency);
1673
2144
  }
1674
- //#endregion
1675
2145
  Object.defineProperty(exports, "ISO_MINOR_V2", {
1676
2146
  enumerable: true,
1677
2147
  get: function() {
1678
2148
  return ISO_MINOR_V2;
1679
2149
  }
1680
2150
  });
2151
+ Object.defineProperty(exports, "PAYSTACK_CURRENCIES", {
2152
+ enumerable: true,
2153
+ get: function() {
2154
+ return PAYSTACK_CURRENCIES;
2155
+ }
2156
+ });
1681
2157
  Object.defineProperty(exports, "PRE_CUTOVER", {
1682
2158
  enumerable: true,
1683
2159
  get: function() {
@@ -1696,6 +2172,24 @@ Object.defineProperty(exports, "RAZORPAY_AFA_FREE_CAP", {
1696
2172
  return RAZORPAY_AFA_FREE_CAP;
1697
2173
  }
1698
2174
  });
2175
+ Object.defineProperty(exports, "WIRE_ROUNDING_EXACT", {
2176
+ enumerable: true,
2177
+ get: function() {
2178
+ return WIRE_ROUNDING_EXACT;
2179
+ }
2180
+ });
2181
+ Object.defineProperty(exports, "WIRE_ROUNDING_KEY", {
2182
+ enumerable: true,
2183
+ get: function() {
2184
+ return WIRE_ROUNDING_KEY;
2185
+ }
2186
+ });
2187
+ Object.defineProperty(exports, "WIRE_ROUNDING_MARKER", {
2188
+ enumerable: true,
2189
+ get: function() {
2190
+ return WIRE_ROUNDING_MARKER;
2191
+ }
2192
+ });
1699
2193
  Object.defineProperty(exports, "WIRE_SCALE_ISO_MINOR_V2", {
1700
2194
  enumerable: true,
1701
2195
  get: function() {
@@ -1780,6 +2274,18 @@ Object.defineProperty(exports, "jsonDecimalLiteral", {
1780
2274
  return jsonDecimalLiteral;
1781
2275
  }
1782
2276
  });
2277
+ Object.defineProperty(exports, "legacyFloatRefundClamp", {
2278
+ enumerable: true,
2279
+ get: function() {
2280
+ return legacyFloatRefundClamp;
2281
+ }
2282
+ });
2283
+ Object.defineProperty(exports, "mayHoldLegacyFloatRounding", {
2284
+ enumerable: true,
2285
+ get: function() {
2286
+ return mayHoldLegacyFloatRounding;
2287
+ }
2288
+ });
1783
2289
  Object.defineProperty(exports, "oneWireUnit", {
1784
2290
  enumerable: true,
1785
2291
  get: function() {
@@ -1792,6 +2298,12 @@ Object.defineProperty(exports, "parseProviderAmount", {
1792
2298
  return parseProviderAmount;
1793
2299
  }
1794
2300
  });
2301
+ Object.defineProperty(exports, "pspAccountCountryForCurrency", {
2302
+ enumerable: true,
2303
+ get: function() {
2304
+ return pspAccountCountryForCurrency;
2305
+ }
2306
+ });
1795
2307
  Object.defineProperty(exports, "pspCurrencies", {
1796
2308
  enumerable: true,
1797
2309
  get: function() {
@@ -1804,12 +2316,36 @@ Object.defineProperty(exports, "pspDefaultCurrency", {
1804
2316
  return pspDefaultCurrency;
1805
2317
  }
1806
2318
  });
2319
+ Object.defineProperty(exports, "pspHomeCountry", {
2320
+ enumerable: true,
2321
+ get: function() {
2322
+ return pspHomeCountry;
2323
+ }
2324
+ });
2325
+ Object.defineProperty(exports, "pspHomeCurrency", {
2326
+ enumerable: true,
2327
+ get: function() {
2328
+ return pspHomeCurrency;
2329
+ }
2330
+ });
1807
2331
  Object.defineProperty(exports, "pspMinimumAmount", {
1808
2332
  enumerable: true,
1809
2333
  get: function() {
1810
2334
  return pspMinimumAmount;
1811
2335
  }
1812
2336
  });
2337
+ Object.defineProperty(exports, "pspPresentment", {
2338
+ enumerable: true,
2339
+ get: function() {
2340
+ return pspPresentment;
2341
+ }
2342
+ });
2343
+ Object.defineProperty(exports, "pspPresentmentCurrencies", {
2344
+ enumerable: true,
2345
+ get: function() {
2346
+ return pspPresentmentCurrencies;
2347
+ }
2348
+ });
1813
2349
  Object.defineProperty(exports, "pspSupportsCurrency", {
1814
2350
  enumerable: true,
1815
2351
  get: function() {