multi-payment-system 0.0.0-stage → 1.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.
@@ -0,0 +1,91 @@
1
+ import { Router } from "express";
2
+ import { paymentService } from "../services/PaymentService.js";
3
+
4
+ const router = Router();
5
+
6
+ function mapXendit(body) {
7
+ const statusMap = {
8
+ PAID: "succeeded",
9
+ SETTLED: "succeeded",
10
+ EXPIRED: "failed",
11
+ PENDING: "pending",
12
+ };
13
+ return {
14
+ gatewayRef: body.id || body.invoice_id,
15
+ status: statusMap[String(body.status || "").toUpperCase()] || "pending",
16
+ };
17
+ }
18
+
19
+ function mapMidtrans(body) {
20
+ const s = String(body.transaction_status || "").toLowerCase();
21
+ let status = "pending";
22
+ if (["capture", "settlement"].includes(s)) status = "succeeded";
23
+ if (["deny", "cancel", "expire", "failure"].includes(s)) status = "failed";
24
+ return {
25
+ gatewayRef: body.order_id,
26
+ status,
27
+ };
28
+ }
29
+
30
+ function mapRazorpay(body) {
31
+ const event = body.event || "";
32
+ const entity = body.payload?.payment?.entity || body.payload?.order?.entity || {};
33
+ let status = "pending";
34
+ if (event.includes("captured") || entity.status === "captured" || entity.status === "paid") {
35
+ status = "succeeded";
36
+ }
37
+ if (event.includes("failed") || entity.status === "failed") status = "failed";
38
+ return {
39
+ gatewayRef: entity.order_id || entity.id,
40
+ status,
41
+ };
42
+ }
43
+
44
+ function mapStripe(body) {
45
+ const type = body.type || "";
46
+ const obj = body.data?.object || {};
47
+ let status = "pending";
48
+ if (type === "checkout.session.completed" && obj.payment_status === "paid") {
49
+ status = "succeeded";
50
+ }
51
+ if (type.includes("payment_intent.payment_failed")) status = "failed";
52
+ return {
53
+ gatewayRef: obj.id,
54
+ status,
55
+ };
56
+ }
57
+
58
+ function mapOmise(body) {
59
+ const data = body.data || body;
60
+ let status = "pending";
61
+ if (data.status === "successful") status = "succeeded";
62
+ if (data.status === "failed" || data.status === "expired") status = "failed";
63
+ return {
64
+ gatewayRef: data.id,
65
+ status,
66
+ };
67
+ }
68
+
69
+ async function handle(gateway, mapper, req, res) {
70
+ const mapped = mapper(req.body || {});
71
+ const updated = paymentService.applyWebhookUpdate({
72
+ gateway,
73
+ gatewayRef: mapped.gatewayRef,
74
+ status: mapped.status,
75
+ raw: req.body,
76
+ });
77
+ res.json({
78
+ ok: true,
79
+ matched: Boolean(updated),
80
+ paymentId: updated?.id || null,
81
+ status: updated?.status || mapped.status,
82
+ });
83
+ }
84
+
85
+ router.post("/xendit", (req, res) => handle("xendit", mapXendit, req, res));
86
+ router.post("/midtrans", (req, res) => handle("midtrans", mapMidtrans, req, res));
87
+ router.post("/razorpay", (req, res) => handle("razorpay", mapRazorpay, req, res));
88
+ router.post("/stripe", (req, res) => handle("stripe", mapStripe, req, res));
89
+ router.post("/omise", (req, res) => handle("omise", mapOmise, req, res));
90
+
91
+ export default router;
package/src/server.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createApp } from "./app.js";
2
+ import { config } from "./config.js";
3
+
4
+ const app = createApp();
5
+
6
+ app.listen(config.port, () => {
7
+ console.log(`Payment server listening on http://localhost:${config.port}`);
8
+ console.log(`Health: GET http://localhost:${config.port}/health`);
9
+ console.log(`Charge: POST http://localhost:${config.port}/api/payments/charge`);
10
+ console.log(`Meta: GET http://localhost:${config.port}/api/payments/meta`);
11
+ });
@@ -0,0 +1,249 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { store } from "../store/MemoryStore.js";
3
+ import { getGateway } from "../gateways/index.js";
4
+ import { toMinorUnits, normalizeCurrency } from "../utils/money.js";
5
+ import { AppError } from "../utils/errors.js";
6
+
7
+ function scrubKeys(keys = {}) {
8
+ // Never persist full secrets in responses; keep only for outbound calls in memory
9
+ const copy = { ...keys };
10
+ return copy;
11
+ }
12
+
13
+ function publicPayment(payment) {
14
+ const { keys, ...rest } = payment;
15
+ return {
16
+ ...rest,
17
+ keysPresent: Boolean(keys && Object.keys(keys).length),
18
+ };
19
+ }
20
+
21
+ export class PaymentService {
22
+ async createOnlinePayment(input) {
23
+ const gatewayName = String(input.gateway || "").toLowerCase();
24
+ const currency = normalizeCurrency(input.currency);
25
+ const amountMinor = toMinorUnits(
26
+ input.amount,
27
+ currency,
28
+ Boolean(input.amountAlreadyMinor)
29
+ );
30
+
31
+ if (!input.keys || typeof input.keys !== "object") {
32
+ throw new AppError("keys object is required (gateway API credentials)");
33
+ }
34
+ if (!amountMinor || amountMinor <= 0) {
35
+ throw new AppError("amount must be greater than 0");
36
+ }
37
+
38
+ const id = uuidv4();
39
+ const now = new Date().toISOString();
40
+ const gateway = getGateway(gatewayName);
41
+
42
+ const draft = {
43
+ id,
44
+ type: "online",
45
+ gateway: gateway.name,
46
+ orderId: input.orderId || null,
47
+ amount: Number(input.amount),
48
+ amountMinor,
49
+ currency,
50
+ description: input.description || null,
51
+ customer: input.customer || null,
52
+ paymentMethod: input.paymentMethod || null,
53
+ paymentMethods: input.paymentMethods || null,
54
+ status: "created",
55
+ gatewayRef: null,
56
+ checkoutUrl: null,
57
+ keys: scrubKeys(input.keys),
58
+ metadata: input.metadata || {},
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ history: [{ at: now, status: "created", note: "Payment created" }],
62
+ };
63
+
64
+ store.savePayment(draft);
65
+
66
+ const result = await gateway.createPayment({
67
+ paymentId: id,
68
+ amountMinor,
69
+ currency,
70
+ keys: input.keys,
71
+ orderId: input.orderId,
72
+ description: input.description,
73
+ customer: input.customer,
74
+ returnUrl: input.returnUrl,
75
+ cancelUrl: input.cancelUrl,
76
+ metadata: input.metadata,
77
+ paymentMethod: input.paymentMethod,
78
+ paymentMethods: input.paymentMethods,
79
+ cardToken: input.cardToken,
80
+ bank: input.bank,
81
+ });
82
+
83
+ draft.status = result.status || "pending";
84
+ draft.gatewayRef = result.gatewayRef || null;
85
+ draft.checkoutUrl = result.checkoutUrl || null;
86
+ draft.token = result.token || null;
87
+ draft.clientSecret = result.clientSecret || null;
88
+ draft.checkout = result.checkout || null;
89
+ draft.qrImageUrl = result.qrImageUrl || null;
90
+ draft.resolvedPaymentMethods = result.paymentMethods || null;
91
+ draft.amountDisplay = result.amountDisplay || null;
92
+ draft.gatewayRaw = result.raw || null;
93
+ draft.updatedAt = new Date().toISOString();
94
+ draft.history.push({
95
+ at: draft.updatedAt,
96
+ status: draft.status,
97
+ note: "Gateway charge/session created",
98
+ });
99
+
100
+ store.savePayment(draft);
101
+ return publicPayment(draft);
102
+ }
103
+
104
+ async refreshStatus(paymentId, keysOverride) {
105
+ const payment = store.getPayment(paymentId);
106
+ if (!payment) throw new AppError("Payment not found", 404);
107
+ if (payment.type === "edc") {
108
+ return publicPayment(payment);
109
+ }
110
+
111
+ const keys = keysOverride || payment.keys;
112
+ if (!keys) throw new AppError("keys required to refresh status", 400);
113
+ if (!payment.gatewayRef) throw new AppError("No gateway reference to query", 400);
114
+
115
+ const gateway = getGateway(payment.gateway);
116
+ const result = await gateway.getStatus({
117
+ keys,
118
+ gatewayRef: payment.gatewayRef,
119
+ orderId: payment.orderId,
120
+ });
121
+
122
+ payment.status = result.status;
123
+ payment.gatewayRaw = result.raw;
124
+ payment.updatedAt = new Date().toISOString();
125
+ payment.history.push({
126
+ at: payment.updatedAt,
127
+ status: payment.status,
128
+ note: "Status refreshed from gateway",
129
+ });
130
+ // refresh stored keys if override provided
131
+ if (keysOverride) payment.keys = scrubKeys(keysOverride);
132
+ store.savePayment(payment);
133
+ return publicPayment(payment);
134
+ }
135
+
136
+ getPayment(id) {
137
+ const payment = store.getPayment(id);
138
+ if (!payment) throw new AppError("Payment not found", 404);
139
+ return publicPayment(payment);
140
+ }
141
+
142
+ listPayments() {
143
+ return store.listPayments().map(publicPayment);
144
+ }
145
+
146
+ applyWebhookUpdate({ gateway, gatewayRef, status, raw }) {
147
+ const payment =
148
+ store.findPaymentByGatewayRef(String(gateway).toLowerCase(), gatewayRef) ||
149
+ null;
150
+ if (!payment) return null;
151
+
152
+ payment.status = status;
153
+ payment.gatewayRaw = raw;
154
+ payment.updatedAt = new Date().toISOString();
155
+ payment.history.push({
156
+ at: payment.updatedAt,
157
+ status,
158
+ note: "Updated via webhook",
159
+ });
160
+ store.savePayment(payment);
161
+ return publicPayment(payment);
162
+ }
163
+
164
+ // --- Yokke EDC local-terminal flow (from integration guide) ---
165
+
166
+ createEdcAttempt(input) {
167
+ const currency = normalizeCurrency(input.currency || "IDR");
168
+ const amountMinor = toMinorUnits(
169
+ input.amount,
170
+ currency,
171
+ Boolean(input.amountAlreadyMinor)
172
+ );
173
+ if (!amountMinor || amountMinor <= 0) {
174
+ throw new AppError("amount must be greater than 0");
175
+ }
176
+
177
+ const id = uuidv4();
178
+ const now = new Date().toISOString();
179
+ const attempt = {
180
+ id,
181
+ type: "edc",
182
+ gateway: "yokke_edc",
183
+ orderId: input.orderId || null,
184
+ deviceId: input.deviceId || null,
185
+ storeId: input.storeId || null,
186
+ amount: Number(input.amount),
187
+ amountMinor,
188
+ currency,
189
+ status: "awaiting_terminal",
190
+ terminalResult: null,
191
+ metadata: input.metadata || {},
192
+ createdAt: now,
193
+ updatedAt: now,
194
+ history: [{ at: now, status: "awaiting_terminal", note: "EDC attempt created" }],
195
+ };
196
+
197
+ // Also mirror into payments map for unified GET
198
+ store.saveAttempt(attempt);
199
+ store.savePayment({ ...attempt, keys: null });
200
+ return publicPayment(attempt);
201
+ }
202
+
203
+ recordTerminalResult(attemptId, result) {
204
+ const attempt = store.getAttempt(attemptId) || store.getPayment(attemptId);
205
+ if (!attempt || attempt.type !== "edc") {
206
+ throw new AppError("EDC payment attempt not found", 404);
207
+ }
208
+
209
+ const statusRaw = String(result.status || "").toLowerCase();
210
+ let status = "pending";
211
+ if (["approved", "success", "succeeded", "paid"].includes(statusRaw)) status = "succeeded";
212
+ else if (["declined", "failed", "cancelled", "canceled", "error"].includes(statusRaw)) {
213
+ status = "failed";
214
+ } else if (["unknown", "timeout", "pending"].includes(statusRaw)) {
215
+ status = "pending";
216
+ }
217
+
218
+ // Basic amount check when terminal echoes amount
219
+ if (result.amountMinor != null && Number(result.amountMinor) !== attempt.amountMinor) {
220
+ throw new AppError("Terminal amount does not match attempt amount", 409, {
221
+ expected: attempt.amountMinor,
222
+ received: result.amountMinor,
223
+ });
224
+ }
225
+
226
+ attempt.status = status;
227
+ attempt.terminalResult = {
228
+ approvalCode: result.approvalCode || null,
229
+ referenceNo: result.referenceNo || null,
230
+ rrn: result.rrn || null,
231
+ cardBrand: result.cardBrand || null,
232
+ maskedPan: result.maskedPan || null,
233
+ raw: result.raw || result,
234
+ verifiedAt: new Date().toISOString(),
235
+ };
236
+ attempt.updatedAt = new Date().toISOString();
237
+ attempt.history.push({
238
+ at: attempt.updatedAt,
239
+ status,
240
+ note: "Terminal result received from kiosk",
241
+ });
242
+
243
+ store.saveAttempt(attempt);
244
+ store.savePayment({ ...attempt, keys: null });
245
+ return publicPayment(attempt);
246
+ }
247
+ }
248
+
249
+ export const paymentService = new PaymentService();
@@ -0,0 +1,49 @@
1
+ /**
2
+ * In-memory payment store for demos / local kiosk PoC.
3
+ * Replace with Postgres/Redis for production.
4
+ */
5
+ class MemoryStore {
6
+ constructor() {
7
+ this.payments = new Map();
8
+ this.attempts = new Map();
9
+ }
10
+
11
+ savePayment(payment) {
12
+ this.payments.set(payment.id, payment);
13
+ return payment;
14
+ }
15
+
16
+ getPayment(id) {
17
+ return this.payments.get(id) || null;
18
+ }
19
+
20
+ findPaymentByGatewayRef(gateway, gatewayRef) {
21
+ for (const p of this.payments.values()) {
22
+ if (p.gateway === gateway && p.gatewayRef === gatewayRef) return p;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ listPayments(limit = 50) {
28
+ return [...this.payments.values()]
29
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
30
+ .slice(0, limit);
31
+ }
32
+
33
+ saveAttempt(attempt) {
34
+ this.attempts.set(attempt.id, attempt);
35
+ return attempt;
36
+ }
37
+
38
+ getAttempt(id) {
39
+ return this.attempts.get(id) || null;
40
+ }
41
+
42
+ listAttempts(limit = 50) {
43
+ return [...this.attempts.values()]
44
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
45
+ .slice(0, limit);
46
+ }
47
+ }
48
+
49
+ export const store = new MemoryStore();
@@ -0,0 +1,11 @@
1
+ /** Base64 encode that works in Node and browsers. */
2
+ export function toBase64(value) {
3
+ const str = String(value);
4
+ if (typeof Buffer !== "undefined") {
5
+ return Buffer.from(str, "utf8").toString("base64");
6
+ }
7
+ if (typeof btoa === "function") {
8
+ return btoa(unescape(encodeURIComponent(str)));
9
+ }
10
+ throw new Error("No Base64 encoder available in this environment");
11
+ }
@@ -0,0 +1,21 @@
1
+ export class AppError extends Error {
2
+ constructor(message, statusCode = 400, details = undefined) {
3
+ super(message);
4
+ this.name = "AppError";
5
+ this.statusCode = statusCode;
6
+ this.details = details;
7
+ }
8
+ }
9
+
10
+ export function errorHandler(err, _req, res, _next) {
11
+ const status = err.statusCode || 500;
12
+ const body = {
13
+ ok: false,
14
+ error: err.message || "Internal server error",
15
+ };
16
+ if (err.details) body.details = err.details;
17
+ if (status >= 500) {
18
+ console.error("[error]", err);
19
+ }
20
+ res.status(status).json(body);
21
+ }
@@ -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,166 @@
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
+ paynow: ["PAYNOW"],
48
+ sgqr: ["PAYNOW"],
49
+ promptpay: ["PROMPTPAY"],
50
+ grabpay: ["GRABPAY"],
51
+ ovo: ["OVO"],
52
+ dana: ["DANA"],
53
+ shopeepay: ["SHOPEEPAY"],
54
+ apple_pay: ["CREDIT_CARD"],
55
+ google_pay: ["CREDIT_CARD"],
56
+ },
57
+ midtrans: {
58
+ card: ["credit_card"],
59
+ qr: ["qris", "gopay", "shopeepay"],
60
+ ewallet: ["gopay", "shopeepay", "other_qris"],
61
+ bank: ["bank_transfer", "echannel", "permata_va", "bca_va", "bni_va", "bri_va"],
62
+ gopay: ["gopay"],
63
+ shopeepay: ["shopeepay"],
64
+ },
65
+ stripe: {
66
+ card: ["card"],
67
+ qr: ["paynow", "promptpay", "alipay"],
68
+ ewallet: ["grabpay", "alipay", "wechat_pay"],
69
+ bank: ["fpx", "paynow"],
70
+ paynow: ["paynow"],
71
+ promptpay: ["promptpay"],
72
+ fpx: ["fpx"],
73
+ grabpay: ["grabpay"],
74
+ },
75
+ razorpay: {
76
+ card: ["card"],
77
+ qr: ["upi"],
78
+ ewallet: ["wallet"],
79
+ bank: ["netbanking"],
80
+ upi: ["upi"],
81
+ },
82
+ omise: {
83
+ card: ["card"],
84
+ qr: ["promptpay", "paynow"],
85
+ ewallet: ["truemoney", "alipay"],
86
+ bank: ["fpx", "internet_banking_bay", "internet_banking_bbl"],
87
+ paynow: ["paynow"],
88
+ promptpay: ["promptpay"],
89
+ fpx: ["fpx"],
90
+ },
91
+ };
92
+
93
+ export function normalizeMethods(paymentMethod, paymentMethods) {
94
+ const list = [];
95
+ if (paymentMethod) list.push(paymentMethod);
96
+ if (Array.isArray(paymentMethods)) list.push(...paymentMethods);
97
+ if (typeof paymentMethods === "string") list.push(paymentMethods);
98
+
99
+ const out = [];
100
+ for (const raw of list) {
101
+ const key = String(raw || "")
102
+ .trim()
103
+ .toLowerCase()
104
+ .replace(/\s+/g, "_");
105
+ if (!key) continue;
106
+ const canon = ALIASES[key] || key;
107
+ if (!out.includes(canon)) out.push(canon);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ export function mapMethodsForGateway(gateway, methods, currency) {
113
+ const gw = String(gateway || "").toLowerCase();
114
+ const table = PAYMENT_METHOD_HINTS[gw] || {};
115
+ const mapped = [];
116
+
117
+ for (const m of methods) {
118
+ const values = table[m];
119
+ if (values) {
120
+ for (const v of values) {
121
+ if (!mapped.includes(v)) mapped.push(v);
122
+ }
123
+ continue;
124
+ }
125
+ // pass-through vendor-specific codes (e.g. CREDIT_CARD, qris, paynow)
126
+ if (!mapped.includes(m)) mapped.push(m);
127
+ }
128
+
129
+ const cur = String(currency || "").toUpperCase();
130
+
131
+ // Currency-aware QR / ewallet mapping (SG account has PAYNOW + cards, not QRIS/OVO)
132
+ if (gw === "xendit") {
133
+ const out = [];
134
+ for (const m of methods) {
135
+ if (m === "qr" || m === "paynow" || m === "sgqr") {
136
+ if (cur === "SGD") out.push("PAYNOW");
137
+ else if (cur === "THB") out.push("PROMPTPAY");
138
+ else out.push("QRIS");
139
+ continue;
140
+ }
141
+ if (m === "ewallet") {
142
+ if (cur === "SGD") out.push("GRABPAY");
143
+ else out.push("OVO", "DANA", "LINKAJA", "SHOPEEPAY", "GRABPAY");
144
+ continue;
145
+ }
146
+ if (m === "card") {
147
+ out.push("CREDIT_CARD");
148
+ continue;
149
+ }
150
+ const values = table[m];
151
+ if (values) out.push(...values);
152
+ else out.push(m);
153
+ }
154
+ return [...new Set(out)];
155
+ }
156
+
157
+ if (methods.includes("qr") && mapped.length) {
158
+ if (gw === "omise" || gw === "stripe") {
159
+ if (cur === "SGD") return ["paynow"];
160
+ if (cur === "THB") return ["promptpay"];
161
+ if (cur === "MYR" && gw === "stripe") return ["fpx"];
162
+ }
163
+ }
164
+
165
+ return mapped;
166
+ }