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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,351 @@
1
- # Temporary Holding Version
1
+ # multi-payment-system
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ npm package + optional Express server for **Xendit, Midtrans, Razorpay, Stripe, Omise** (SG / MY / SEA), plus Yokke EDC attempt helpers.
4
+
5
+ ---
6
+
7
+ ## Install (as a library)
8
+
9
+ ```bash
10
+ npm install multi-payment-system
11
+ ```
12
+
13
+ ### Use gateways as classes
14
+
15
+ ```js
16
+ import { XenditGateway, StripeGateway, charge, getStatus, listGateways } from "multi-payment-system";
17
+
18
+ const xendit = new XenditGateway();
19
+ const pay = await xendit.createPayment({
20
+ paymentId: "pay_1",
21
+ amountMinor: 3000, // SGD 30.00
22
+ currency: "SGD",
23
+ paymentMethod: "card",
24
+ keys: { secretKey: process.env.XENDIT_SECRET_KEY },
25
+ returnUrl: "https://example.com/success",
26
+ cancelUrl: "https://example.com/cancel",
27
+ });
28
+
29
+ console.log(pay.checkoutUrl);
30
+ ```
31
+
32
+ ### Or one-liner helpers
33
+
34
+ ```js
35
+ import { charge, getStatus } from "multi-payment-system";
36
+
37
+ const result = await charge({
38
+ gateway: "xendit",
39
+ amount: 30,
40
+ currency: "SGD",
41
+ paymentMethod: "card",
42
+ keys: { secretKey: process.env.XENDIT_SECRET_KEY },
43
+ returnUrl: "https://example.com/success",
44
+ });
45
+
46
+ const status = await getStatus({
47
+ gateway: "xendit",
48
+ gatewayRef: result.gatewayRef,
49
+ keys: { secretKey: process.env.XENDIT_SECRET_KEY },
50
+ });
51
+ ```
52
+
53
+ ### Exports
54
+
55
+ | Export | What |
56
+ |--------|------|
57
+ | `XenditGateway`, `MidtransGateway`, `RazorpayGateway`, `StripeGateway`, `OmiseGateway` | Gateway classes |
58
+ | `charge`, `getStatus`, `listGateways` | Helper functions |
59
+ | `getGateway("xendit")` | Resolve by name |
60
+ | `createApp()` | Express app (HTTP API) |
61
+ | `PaymentService` | In-memory payment tracker used by the server |
62
+
63
+ ### Publish to npm
64
+
65
+ ```bash
66
+ npm login
67
+ npm publish --access public
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Run as a local server
73
+
74
+ ```bash
75
+ npm install
76
+ npm start
77
+ ```
78
+
79
+ Server: `http://localhost:3000`
80
+
81
+ ```bash
82
+ npm run test:smoke # local EDC + validation checks (no real gateway keys needed)
83
+ ```
84
+
85
+ ### Postman
86
+
87
+ Import these files into Postman:
88
+
89
+ 1. `postman/YOKKE_EDC_Payment_Server.postman_collection.json`
90
+ 2. `postman/YOKKE_Local.postman_environment.json`
91
+
92
+ Select environment **YOKKE Local**, put your test keys in the environment variables, then run:
93
+
94
+ 1. **Health** (no keys needed)
95
+ 2. **Create EDC attempt** → **Report terminal result** (no gateway keys)
96
+ 3. Or **Charge - Stripe / Xendit / …** after filling keys
97
+
98
+ ---
99
+
100
+ ## How it fits the kiosk diagram
101
+
102
+ ```
103
+ WebView → Kotlin Bridge → (A) this server OR (B) Yokke EDC via USB
104
+ │
105
+ ├─ online: POST /api/payments/charge (Xendit/Stripe/…)
106
+ └─ EDC: POST /api/payments/attempts
107
+ then POST .../terminal-result after EDC responds
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Amount rules
113
+
114
+ | Field | Meaning |
115
+ |--------|---------|
116
+ | `amount` | Major units by default (`10.50` SGD = S$10.50) |
117
+ | `amountAlreadyMinor: true` | Treat `amount` as cents/sen/paise already (`1050`) |
118
+ | `currency` | ISO code: `SGD`, `MYR`, `IDR`, `THB`, `PHP`, `VND`, `USD`, `INR`, … |
119
+
120
+ Zero-decimal currencies (e.g. `IDR`, `VND`, `JPY`) use whole major units as the charge amount.
121
+
122
+ ---
123
+
124
+ ## Create an online payment
125
+
126
+ `POST /api/payments/charge`
127
+
128
+ ### Payment method (all gateways)
129
+
130
+ Send one or both:
131
+
132
+ ```json
133
+ "paymentMethod": "qr",
134
+ "paymentMethods": ["qr", "card"]
135
+ ```
136
+
137
+ Common values: `card`, `qr`, `ewallet`, `bank`, `paynow`, `promptpay`, `fpx`, `upi`, `gopay`, `grabpay`, `ovo`, `dana`, `shopeepay`.
138
+
139
+ | Value | Typical meaning |
140
+ |-------|-----------------|
141
+ | `card` | Credit/debit card |
142
+ | `qr` | QRIS / PayNow / PromptPay / UPI QR (mapped per gateway + currency) |
143
+ | `ewallet` | OVO, DANA, GrabPay, etc. |
144
+ | `bank` | VA / FPX / bank transfer |
145
+
146
+ ### Stripe (SG / MY / global)
147
+
148
+ ```json
149
+ {
150
+ "gateway": "stripe",
151
+ "amount": 12.5,
152
+ "currency": "SGD",
153
+ "paymentMethod": "qr",
154
+ "keys": { "secretKey": "sk_test_..." },
155
+ "orderId": "order-1001",
156
+ "description": "Kiosk order 1001",
157
+ "returnUrl": "https://your-app/success?session_id={CHECKOUT_SESSION_ID}",
158
+ "cancelUrl": "https://your-app/cancel",
159
+ "customer": { "email": "buyer@example.com" }
160
+ }
161
+ ```
162
+
163
+ ### Xendit (SG / MY / ID / PH / TH / VN)
164
+
165
+ ```json
166
+ {
167
+ "gateway": "xendit",
168
+ "amount": 25,
169
+ "currency": "SGD",
170
+ "paymentMethod": "qr",
171
+ "paymentMethods": ["qr", "card"],
172
+ "keys": { "secretKey": "xnd_development_..." },
173
+ "orderId": "order-1002",
174
+ "returnUrl": "https://your-app/success",
175
+ "customer": { "email": "buyer@example.com" }
176
+ }
177
+ ```
178
+
179
+ ### Midtrans (Indonesia)
180
+
181
+ ```json
182
+ {
183
+ "gateway": "midtrans",
184
+ "amount": 50000,
185
+ "currency": "IDR",
186
+ "keys": {
187
+ "serverKey": "SB-Mid-server-...",
188
+ "isProduction": false
189
+ },
190
+ "orderId": "order-1003",
191
+ "returnUrl": "https://your-app/finish"
192
+ }
193
+ ```
194
+
195
+ ### Razorpay
196
+
197
+ ```json
198
+ {
199
+ "gateway": "razorpay",
200
+ "amount": 100,
201
+ "currency": "INR",
202
+ "amountAlreadyMinor": true,
203
+ "keys": {
204
+ "keyId": "rzp_test_...",
205
+ "keySecret": "..."
206
+ },
207
+ "orderId": "order-1004"
208
+ }
209
+ ```
210
+
211
+ Response includes `payment.checkout` for opening Razorpay Checkout in the WebView.
212
+
213
+ ### Omise / Opn (SG PayNow, MY FPX, TH PromptPay)
214
+
215
+ ```json
216
+ {
217
+ "gateway": "omise",
218
+ "amount": 15,
219
+ "currency": "SGD",
220
+ "paymentMethod": "paynow",
221
+ "keys": { "secretKey": "skey_test_..." },
222
+ "returnUrl": "https://your-app/return",
223
+ "orderId": "order-1005"
224
+ }
225
+ ```
226
+
227
+ For Malaysia FPX:
228
+
229
+ ```json
230
+ {
231
+ "gateway": "omise",
232
+ "amount": 20,
233
+ "currency": "MYR",
234
+ "paymentMethod": "fpx",
235
+ "keys": { "secretKey": "skey_test_...", "bank": "uob" },
236
+ "returnUrl": "https://your-app/return"
237
+ }
238
+ ```
239
+
240
+ ### Success response (shape)
241
+
242
+ ```json
243
+ {
244
+ "ok": true,
245
+ "payment": {
246
+ "id": "uuid",
247
+ "gateway": "stripe",
248
+ "status": "pending",
249
+ "checkoutUrl": "https://checkout.stripe.com/...",
250
+ "amount": 12.5,
251
+ "amountMinor": 1250,
252
+ "currency": "SGD"
253
+ }
254
+ }
255
+ ```
256
+
257
+ Open `checkoutUrl` in the kiosk WebView (or system browser). Then poll:
258
+
259
+ - `GET /api/payments/:id`
260
+ - or `POST /api/payments/:id/refresh` with `{ "keys": { ... } }` if you did not keep keys server-side
261
+
262
+ ---
263
+
264
+ ## Yokke EDC local terminal flow
265
+
266
+ ### 1. Create attempt (before talking to the EDC)
267
+
268
+ ```http
269
+ POST /api/payments/attempts
270
+ ```
271
+
272
+ ```json
273
+ {
274
+ "amount": 75000,
275
+ "currency": "IDR",
276
+ "orderId": "order-2001",
277
+ "deviceId": "kiosk-storeA-01"
278
+ }
279
+ ```
280
+
281
+ ### 2. Kotlin sends sale to EDC over USB/serial
282
+
283
+ ### 3. Report verified terminal result
284
+
285
+ ```http
286
+ POST /api/payments/attempts/{id}/terminal-result
287
+ ```
288
+
289
+ ```json
290
+ {
291
+ "status": "approved",
292
+ "approvalCode": "00",
293
+ "referenceNo": "123456",
294
+ "rrn": "999888",
295
+ "amountMinor": 75000
296
+ }
297
+ ```
298
+
299
+ Only mark the order paid when this returns `status: "succeeded"`.
300
+
301
+ ---
302
+
303
+ ## Webhooks (optional)
304
+
305
+ Point each gateway dashboard to:
306
+
307
+ | Gateway | URL |
308
+ |-----------|-----|
309
+ | Xendit | `POST /webhooks/xendit` |
310
+ | Midtrans | `POST /webhooks/midtrans` |
311
+ | Razorpay | `POST /webhooks/razorpay` |
312
+ | Stripe | `POST /webhooks/stripe` |
313
+ | Omise | `POST /webhooks/omise` |
314
+
315
+ Use a public tunnel (`PUBLIC_BASE_URL`) in development.
316
+
317
+ ---
318
+
319
+ ## Optional API key
320
+
321
+ Set in `.env`:
322
+
323
+ ```
324
+ API_KEY=change-me-kiosk-api-key
325
+ ```
326
+
327
+ Then send header: `x-api-key: change-me-kiosk-api-key`
328
+
329
+ ---
330
+
331
+ ## Project layout
332
+
333
+ ```
334
+ src/
335
+ index.js # starts server
336
+ app.js # Express app
337
+ gateways/ # Xendit, Midtrans, Razorpay, Stripe, Omise
338
+ routes/payments.js # charge + EDC endpoints
339
+ routes/webhooks.js
340
+ services/PaymentService.js
341
+ store/MemoryStore.js # swap for DB in production
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Production notes
347
+
348
+ - Replace `MemoryStore` with Postgres/Redis.
349
+ - Prefer storing merchant keys encrypted server-side (vault) instead of accepting raw secrets on every request long-term.
350
+ - Never log full card data from EDC responses.
351
+ - Confirm Yokke EDC model / protocol before production USB framing (see the integration guide).
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "multi-payment-system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Multi-gateway payment SDK + optional Express server (Xendit, Midtrans, Razorpay, Stripe, Omise)",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./server": "./src/server.js",
10
+ "./app": "./src/app.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "start": "node src/server.js",
20
+ "dev": "node --watch src/server.js",
21
+ "test:smoke": "node scripts/smoke-test.js",
22
+ "prepublishOnly": "node scripts/smoke-test.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "keywords": [
28
+ "payment",
29
+ "xendit",
30
+ "midtrans",
31
+ "razorpay",
32
+ "stripe",
33
+ "omise",
34
+ "sgd",
35
+ "myr",
36
+ "kiosk",
37
+ "gateway"
38
+ ],
39
+ "author": "",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": ""
44
+ },
45
+ "dependencies": {
46
+ "axios": "^1.7.9",
47
+ "cors": "^2.8.5",
48
+ "dotenv": "^16.4.7",
49
+ "express": "^4.21.2",
50
+ "uuid": "^11.0.5",
51
+ "zod": "^3.24.1"
52
+ }
53
+ }
package/src/api.js ADDED
@@ -0,0 +1,117 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getGateway, GATEWAY_NAMES, gatewayRegionsHint } from "./gateways/index.js";
3
+ import { toMinorUnits, normalizeCurrency, SUPPORTED_CURRENCIES } from "./utils/money.js";
4
+ import { PAYMENT_METHOD_HINTS, normalizeMethods } from "./utils/paymentMethods.js";
5
+ import { AppError } from "./utils/errors.js";
6
+
7
+ /**
8
+ * Create a payment on any supported gateway.
9
+ *
10
+ * @example
11
+ * import { charge } from "multi-payment-system";
12
+ * const result = await charge({
13
+ * gateway: "xendit",
14
+ * amount: 30,
15
+ * currency: "SGD",
16
+ * paymentMethod: "card",
17
+ * keys: { secretKey: "xnd_development_..." },
18
+ * returnUrl: "https://example.com/success",
19
+ * });
20
+ */
21
+ export async function charge(input = {}) {
22
+ const gatewayName = String(input.gateway || "").toLowerCase();
23
+ if (!gatewayName) throw new AppError("gateway is required");
24
+
25
+ const currency = normalizeCurrency(input.currency);
26
+ if (!currency) throw new AppError("currency is required");
27
+
28
+ if (!input.keys || typeof input.keys !== "object") {
29
+ throw new AppError("keys object is required (gateway API credentials)");
30
+ }
31
+
32
+ const amountMinor = toMinorUnits(
33
+ input.amount,
34
+ currency,
35
+ Boolean(input.amountAlreadyMinor)
36
+ );
37
+ if (!amountMinor || amountMinor <= 0) {
38
+ throw new AppError("amount must be greater than 0");
39
+ }
40
+
41
+ const paymentId = input.paymentId || uuidv4();
42
+ const gateway = getGateway(gatewayName);
43
+
44
+ const result = await gateway.createPayment({
45
+ paymentId,
46
+ amountMinor,
47
+ currency,
48
+ keys: input.keys,
49
+ orderId: input.orderId,
50
+ description: input.description,
51
+ customer: input.customer,
52
+ returnUrl: input.returnUrl,
53
+ cancelUrl: input.cancelUrl,
54
+ metadata: input.metadata,
55
+ paymentMethod: input.paymentMethod,
56
+ paymentMethods: input.paymentMethods,
57
+ cardToken: input.cardToken,
58
+ bank: input.bank,
59
+ });
60
+
61
+ return {
62
+ ok: true,
63
+ paymentId,
64
+ gateway: gateway.name,
65
+ amount: Number(input.amount),
66
+ amountMinor,
67
+ currency,
68
+ paymentMethod: input.paymentMethod || null,
69
+ paymentMethods: normalizeMethods(input.paymentMethod, input.paymentMethods),
70
+ ...result,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Refresh / query payment status from the gateway.
76
+ *
77
+ * @example
78
+ * import { getStatus } from "multi-payment-system";
79
+ * const status = await getStatus({
80
+ * gateway: "xendit",
81
+ * gatewayRef: "inv_...",
82
+ * keys: { secretKey: "xnd_development_..." },
83
+ * });
84
+ */
85
+ export async function getStatus(input = {}) {
86
+ const gatewayName = String(input.gateway || "").toLowerCase();
87
+ if (!gatewayName) throw new AppError("gateway is required");
88
+ if (!input.gatewayRef && !input.orderId) {
89
+ throw new AppError("gatewayRef (or orderId for Midtrans) is required");
90
+ }
91
+ if (!input.keys || typeof input.keys !== "object") {
92
+ throw new AppError("keys object is required");
93
+ }
94
+
95
+ const gateway = getGateway(gatewayName);
96
+ const result = await gateway.getStatus({
97
+ keys: input.keys,
98
+ gatewayRef: input.gatewayRef,
99
+ orderId: input.orderId,
100
+ });
101
+
102
+ return {
103
+ ok: true,
104
+ gateway: gateway.name,
105
+ ...result,
106
+ };
107
+ }
108
+
109
+ /** List supported gateways, regions, currencies, payment method hints */
110
+ export function listGateways() {
111
+ return {
112
+ gateways: GATEWAY_NAMES,
113
+ regions: gatewayRegionsHint(),
114
+ currencies: SUPPORTED_CURRENCIES,
115
+ paymentMethods: PAYMENT_METHOD_HINTS,
116
+ };
117
+ }
package/src/app.js ADDED
@@ -0,0 +1,43 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import paymentsRouter from "./routes/payments.js";
4
+ import webhooksRouter from "./routes/webhooks.js";
5
+ import { optionalApiKey } from "./middleware/auth.js";
6
+ import { errorHandler } from "./utils/errors.js";
7
+ import { GATEWAY_NAMES, gatewayRegionsHint } from "./gateways/index.js";
8
+
9
+ export function createApp() {
10
+ const app = express();
11
+
12
+ app.use(cors());
13
+ app.use(express.json({ limit: "1mb" }));
14
+ app.use(express.urlencoded({ extended: true }));
15
+
16
+ app.get("/health", (_req, res) => {
17
+ res.json({ ok: true, service: "yokke-edc-payment-server", time: new Date().toISOString() });
18
+ });
19
+
20
+ app.get("/", (_req, res) => {
21
+ res.json({
22
+ ok: true,
23
+ name: "YOKKE EDC + Multi-Gateway Payment Server",
24
+ gateways: GATEWAY_NAMES,
25
+ regions: gatewayRegionsHint(),
26
+ endpoints: {
27
+ meta: "GET /api/payments/meta",
28
+ charge: "POST /api/payments/charge",
29
+ get: "GET /api/payments/:id",
30
+ refresh: "POST /api/payments/:id/refresh",
31
+ edcAttempt: "POST /api/payments/attempts",
32
+ terminalResult: "POST /api/payments/attempts/:id/terminal-result",
33
+ webhooks: "POST /webhooks/{xendit|midtrans|razorpay|stripe|omise}",
34
+ },
35
+ });
36
+ });
37
+
38
+ app.use("/api/payments", optionalApiKey, paymentsRouter);
39
+ app.use("/webhooks", webhooksRouter);
40
+
41
+ app.use(errorHandler);
42
+ return app;
43
+ }
package/src/config.js ADDED
@@ -0,0 +1,10 @@
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ export const config = {
6
+ port: Number(process.env.PORT || 3000),
7
+ nodeEnv: process.env.NODE_ENV || "development",
8
+ apiKey: process.env.API_KEY || "",
9
+ publicBaseUrl: process.env.PUBLIC_BASE_URL || "",
10
+ };
@@ -0,0 +1,41 @@
1
+ export class BaseGateway {
2
+ constructor(name) {
3
+ this.name = name;
4
+ }
5
+
6
+ /**
7
+ * @param {object} ctx
8
+ * @param {string} ctx.paymentId
9
+ * @param {number} ctx.amountMinor
10
+ * @param {string} ctx.currency
11
+ * @param {object} ctx.keys
12
+ * @param {string} [ctx.orderId]
13
+ * @param {string} [ctx.description]
14
+ * @param {object} [ctx.customer]
15
+ * @param {string} [ctx.returnUrl]
16
+ * @param {string} [ctx.cancelUrl]
17
+ * @param {object} [ctx.metadata]
18
+ * @param {string} [ctx.paymentMethod]
19
+ */
20
+ async createPayment(_ctx) {
21
+ throw new Error(`${this.name}: createPayment not implemented`);
22
+ }
23
+
24
+ async getStatus(_ctx) {
25
+ throw new Error(`${this.name}: getStatus not implemented`);
26
+ }
27
+
28
+ normalizeStatus(raw) {
29
+ const s = String(raw || "").toLowerCase();
30
+ if (["succeeded", "success", "paid", "capture", "captured", "settlement", "completed"].includes(s)) {
31
+ return "succeeded";
32
+ }
33
+ if (["pending", "requires_action", "requires_payment_method", "awaiting_payment", "created", "authorized"].includes(s)) {
34
+ return "pending";
35
+ }
36
+ if (["failed", "failure", "cancel", "cancelled", "canceled", "expired", "deny", "denied", "void"].includes(s)) {
37
+ return "failed";
38
+ }
39
+ return "pending";
40
+ }
41
+ }