multi-payment-system 0.0.0-stage → 1.0.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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Zero-decimal currencies (amount already in major units for Stripe-style APIs).
3
+ * IDR/VND/etc. are treated as zero-decimal for Stripe; Midtrans/Xendit have their own rules.
4
+ */
5
+ const ZERO_DECIMAL = new Set([
6
+ "BIF",
7
+ "CLP",
8
+ "DJF",
9
+ "GNF",
10
+ "IDR", // Stripe + SEA gateways treat rupiah as whole units
11
+ "JPY",
12
+ "KMF",
13
+ "KRW",
14
+ "MGA",
15
+ "PYG",
16
+ "RWF",
17
+ "UGX",
18
+ "VND",
19
+ "VUV",
20
+ "XAF",
21
+ "XOF",
22
+ "XPF",
23
+ ]);
24
+
25
+ /** Currencies commonly used with SEA / SG / MY focus */
26
+ export const SUPPORTED_CURRENCIES = [
27
+ "SGD",
28
+ "MYR",
29
+ "IDR",
30
+ "THB",
31
+ "PHP",
32
+ "VND",
33
+ "USD",
34
+ "INR",
35
+ "JPY",
36
+ "AUD",
37
+ "HKD",
38
+ ];
39
+
40
+ export function normalizeCurrency(currency) {
41
+ return String(currency || "").trim().toUpperCase();
42
+ }
43
+
44
+ export function isZeroDecimal(currency) {
45
+ return ZERO_DECIMAL.has(normalizeCurrency(currency));
46
+ }
47
+
48
+ /**
49
+ * Convert major-unit amount (e..g. 10.50 SGD) to minor units (1050).
50
+ * Pass amountAlreadyMinor=true if the client already sent cents/sen.
51
+ */
52
+ export function toMinorUnits(amount, currency, amountAlreadyMinor = false) {
53
+ const cur = normalizeCurrency(currency);
54
+ const n = Number(amount);
55
+ if (!Number.isFinite(n) || n < 0) {
56
+ throw new Error("Invalid amount");
57
+ }
58
+ if (amountAlreadyMinor || isZeroDecimal(cur)) {
59
+ return Math.round(n);
60
+ }
61
+ return Math.round(n * 100);
62
+ }
63
+
64
+ /** Convert minor units back to major for gateways that expect major units */
65
+ export function toMajorUnits(minorAmount, currency) {
66
+ const cur = normalizeCurrency(currency);
67
+ if (isZeroDecimal(cur)) return Math.round(Number(minorAmount));
68
+ return Number(minorAmount) / 100;
69
+ }
70
+
71
+ export function formatMajor(minorAmount, currency) {
72
+ const major = toMajorUnits(minorAmount, currency);
73
+ if (isZeroDecimal(currency)) return String(Math.round(major));
74
+ return major.toFixed(2);
75
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Normalize client paymentMethod / paymentMethods into gateway-specific values.
3
+ *
4
+ * Accepted aliases (any gateway):
5
+ * card, qr, qris, ewallet, wallet, bank, bank_transfer, va,
6
+ * paynow, promptpay, fpx, upi, gopay, grabpay, shopeepay, ovo, dana
7
+ */
8
+
9
+ const ALIASES = {
10
+ card: "card",
11
+ credit_card: "card",
12
+ debit_card: "card",
13
+ qr: "qr",
14
+ qris: "qr",
15
+ qr_code: "qr",
16
+ qrcode: "qr",
17
+ ewallet: "ewallet",
18
+ wallet: "ewallet",
19
+ e_wallet: "ewallet",
20
+ bank: "bank",
21
+ bank_transfer: "bank",
22
+ va: "bank",
23
+ virtual_account: "bank",
24
+ paynow: "paynow",
25
+ sgqr: "sgqr",
26
+ promptpay: "promptpay",
27
+ apple_pay: "apple_pay",
28
+ google_pay: "google_pay",
29
+ fpx: "fpx",
30
+ upi: "upi",
31
+ gopay: "gopay",
32
+ grabpay: "grabpay",
33
+ shopeepay: "shopeepay",
34
+ ovo: "ovo",
35
+ dana: "dana",
36
+ linkaja: "linkaja",
37
+ };
38
+
39
+ export const PAYMENT_METHOD_HINTS = {
40
+ common: ["card", "qr", "ewallet", "bank", "paynow", "promptpay", "fpx", "upi"],
41
+ xendit: {
42
+ card: ["CREDIT_CARD"],
43
+ // "qr" is remapped by currency below (SGD→PAYNOW, IDR→QRIS, THB→PROMPTPAY)
44
+ qr: ["QRIS"],
45
+ ewallet: ["GRABPAY", "OVO", "DANA", "LINKAJA", "SHOPEEPAY", "GCASH", "PAYMAYA"],
46
+ bank: ["VIRTUAL_ACCOUNT", "DIRECT_DEBIT"],
47
+ // Xendit SG PayNow QR channel code is SGQR (not PAYNOW)
48
+ paynow: ["SGQR"],
49
+ sgqr: ["SGQR"],
50
+ promptpay: ["PROMPTPAY"],
51
+ grabpay: ["GRABPAY"],
52
+ ovo: ["OVO"],
53
+ dana: ["DANA"],
54
+ shopeepay: ["SHOPEEPAY"],
55
+ apple_pay: ["CREDIT_CARD"],
56
+ google_pay: ["CREDIT_CARD"],
57
+ },
58
+ midtrans: {
59
+ card: ["credit_card"],
60
+ qr: ["qris", "gopay", "shopeepay"],
61
+ ewallet: ["gopay", "shopeepay", "other_qris"],
62
+ bank: ["bank_transfer", "echannel", "permata_va", "bca_va", "bni_va", "bri_va"],
63
+ gopay: ["gopay"],
64
+ shopeepay: ["shopeepay"],
65
+ },
66
+ stripe: {
67
+ card: ["card"],
68
+ qr: ["paynow", "promptpay", "alipay"],
69
+ ewallet: ["grabpay", "alipay", "wechat_pay"],
70
+ bank: ["fpx", "paynow"],
71
+ paynow: ["paynow"],
72
+ promptpay: ["promptpay"],
73
+ fpx: ["fpx"],
74
+ grabpay: ["grabpay"],
75
+ },
76
+ razorpay: {
77
+ card: ["card"],
78
+ qr: ["upi"],
79
+ ewallet: ["wallet"],
80
+ bank: ["netbanking"],
81
+ upi: ["upi"],
82
+ },
83
+ omise: {
84
+ card: ["card"],
85
+ qr: ["promptpay", "paynow"],
86
+ ewallet: ["truemoney", "alipay"],
87
+ bank: ["fpx", "internet_banking_bay", "internet_banking_bbl"],
88
+ paynow: ["paynow"],
89
+ promptpay: ["promptpay"],
90
+ fpx: ["fpx"],
91
+ },
92
+ };
93
+
94
+ export function normalizeMethods(paymentMethod, paymentMethods) {
95
+ const list = [];
96
+ if (paymentMethod) list.push(paymentMethod);
97
+ if (Array.isArray(paymentMethods)) list.push(...paymentMethods);
98
+ if (typeof paymentMethods === "string") list.push(paymentMethods);
99
+
100
+ const out = [];
101
+ for (const raw of list) {
102
+ const key = String(raw || "")
103
+ .trim()
104
+ .toLowerCase()
105
+ .replace(/\s+/g, "_");
106
+ if (!key) continue;
107
+ const canon = ALIASES[key] || key;
108
+ if (!out.includes(canon)) out.push(canon);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ export function mapMethodsForGateway(gateway, methods, currency) {
114
+ const gw = String(gateway || "").toLowerCase();
115
+ const table = PAYMENT_METHOD_HINTS[gw] || {};
116
+ const mapped = [];
117
+
118
+ for (const m of methods) {
119
+ const values = table[m];
120
+ if (values) {
121
+ for (const v of values) {
122
+ if (!mapped.includes(v)) mapped.push(v);
123
+ }
124
+ continue;
125
+ }
126
+ // pass-through vendor-specific codes (e.g. CREDIT_CARD, qris, paynow)
127
+ if (!mapped.includes(m)) mapped.push(m);
128
+ }
129
+
130
+ const cur = String(currency || "").toUpperCase();
131
+
132
+ // Currency-aware QR / ewallet mapping (SG account has PAYNOW + cards, not QRIS/OVO)
133
+ if (gw === "xendit") {
134
+ const out = [];
135
+ for (const m of methods) {
136
+ if (m === "qr" || m === "paynow" || m === "sgqr") {
137
+ if (cur === "SGD") out.push("SGQR");
138
+ else if (cur === "THB") out.push("PROMPTPAY");
139
+ else if (cur === "PHP") out.push("QRPH");
140
+ else out.push("QRIS");
141
+ continue;
142
+ }
143
+ if (m === "ewallet") {
144
+ if (cur === "SGD") out.push("GRABPAY");
145
+ else out.push("OVO", "DANA", "LINKAJA", "SHOPEEPAY", "GRABPAY");
146
+ continue;
147
+ }
148
+ if (m === "card") {
149
+ out.push("CREDIT_CARD");
150
+ continue;
151
+ }
152
+ const values = table[m];
153
+ if (values) out.push(...values);
154
+ else out.push(m);
155
+ }
156
+ return [...new Set(out)];
157
+ }
158
+
159
+ if (methods.includes("qr") && mapped.length) {
160
+ if (gw === "omise" || gw === "stripe") {
161
+ if (cur === "SGD") return ["paynow"];
162
+ if (cur === "THB") return ["promptpay"];
163
+ if (cur === "MYR" && gw === "stripe") return ["fpx"];
164
+ }
165
+ }
166
+
167
+ return mapped;
168
+ }