@boostengine/payments 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/README.md +197 -0
- package/bin/cli.cjs +92 -0
- package/dist/index.cjs +988 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +339 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.mjs +966 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
8
|
+
|
|
9
|
+
// src/adapters/base.adapter.ts
|
|
10
|
+
var BasePaymentAdapter = class {
|
|
11
|
+
/**
|
|
12
|
+
* Safe helper to extract single string header value from request headers.
|
|
13
|
+
*/
|
|
14
|
+
getHeader(headers, name) {
|
|
15
|
+
const direct = headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
|
|
16
|
+
if (Array.isArray(direct)) return direct[0];
|
|
17
|
+
return direct;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Helper to perform HTTP JSON requests with standard error extraction.
|
|
21
|
+
*/
|
|
22
|
+
async fetchJson(url, options = {}) {
|
|
23
|
+
const { method = "GET", headers = {}, body } = options;
|
|
24
|
+
const requestHeaders = {
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
...headers
|
|
27
|
+
};
|
|
28
|
+
let serializedBody;
|
|
29
|
+
if (body !== void 0) {
|
|
30
|
+
if (typeof body === "string") {
|
|
31
|
+
serializedBody = body;
|
|
32
|
+
} else if (headers["Content-Type"] === "application/x-www-form-urlencoded") {
|
|
33
|
+
serializedBody = new URLSearchParams(body).toString();
|
|
34
|
+
} else {
|
|
35
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
36
|
+
serializedBody = JSON.stringify(body);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method,
|
|
41
|
+
headers: requestHeaders,
|
|
42
|
+
body: serializedBody
|
|
43
|
+
});
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
data = JSON.parse(text);
|
|
48
|
+
} catch {
|
|
49
|
+
data = text;
|
|
50
|
+
}
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const errMsg = data?.message || data?.error?.description || data?.error?.message || data?.description || (typeof data === "string" ? data : `HTTP ${res.status} ${res.statusText}`);
|
|
53
|
+
throw new Error(`[${this.name.toUpperCase()} API Error] ${errMsg}`);
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function hmacSha256(data, secret) {
|
|
59
|
+
return crypto__default.default.createHmac("sha256", secret).update(data).digest("hex");
|
|
60
|
+
}
|
|
61
|
+
function sha256(data) {
|
|
62
|
+
return crypto__default.default.createHash("sha256").update(data).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
function base64Encode(data) {
|
|
65
|
+
const str = typeof data === "string" ? data : JSON.stringify(data);
|
|
66
|
+
return Buffer.from(str, "utf8").toString("base64");
|
|
67
|
+
}
|
|
68
|
+
function base64Decode(encoded) {
|
|
69
|
+
return Buffer.from(encoded, "base64").toString("utf8");
|
|
70
|
+
}
|
|
71
|
+
function safeCompare(a, b) {
|
|
72
|
+
try {
|
|
73
|
+
const bufA = Buffer.from(a, "utf8");
|
|
74
|
+
const bufB = Buffer.from(b, "utf8");
|
|
75
|
+
if (bufA.length !== bufB.length) return false;
|
|
76
|
+
return crypto__default.default.timingSafeEqual(bufA, bufB);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/utils/errors.ts
|
|
83
|
+
var PaymentError = class _PaymentError extends Error {
|
|
84
|
+
constructor(message, options) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "PaymentError";
|
|
87
|
+
this.gateway = options?.gateway;
|
|
88
|
+
this.statusCode = options?.statusCode;
|
|
89
|
+
this.rawError = options?.rawError;
|
|
90
|
+
Object.setPrototypeOf(this, _PaymentError.prototype);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var GatewayNotConfiguredError = class _GatewayNotConfiguredError extends PaymentError {
|
|
94
|
+
constructor(gateway) {
|
|
95
|
+
super(`Payment gateway '${gateway}' is not configured in PaymentManager.`, { gateway, statusCode: 400 });
|
|
96
|
+
this.name = "GatewayNotConfiguredError";
|
|
97
|
+
Object.setPrototypeOf(this, _GatewayNotConfiguredError.prototype);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var SignatureVerificationError = class _SignatureVerificationError extends PaymentError {
|
|
101
|
+
constructor(gateway, details) {
|
|
102
|
+
super(`Invalid webhook/payment signature for gateway '${gateway}'. ${details || ""}`.trim(), {
|
|
103
|
+
gateway,
|
|
104
|
+
statusCode: 401
|
|
105
|
+
});
|
|
106
|
+
this.name = "SignatureVerificationError";
|
|
107
|
+
Object.setPrototypeOf(this, _SignatureVerificationError.prototype);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/adapters/razorpay.adapter.ts
|
|
112
|
+
var RazorpayAdapter = class extends BasePaymentAdapter {
|
|
113
|
+
constructor(config) {
|
|
114
|
+
super();
|
|
115
|
+
this.config = config;
|
|
116
|
+
this.name = "razorpay";
|
|
117
|
+
this.baseUrl = "https://api.razorpay.com/v1";
|
|
118
|
+
if (!config.keyId || !config.keySecret) {
|
|
119
|
+
throw new PaymentError("Razorpay keyId and keySecret are required.", { gateway: "razorpay" });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
getAuthHeader() {
|
|
123
|
+
const creds = `${this.config.keyId}:${this.config.keySecret}`;
|
|
124
|
+
return `Basic ${Buffer.from(creds).toString("base64")}`;
|
|
125
|
+
}
|
|
126
|
+
async createOrder(options) {
|
|
127
|
+
const amountInSubunits = Math.round(options.amount * 100);
|
|
128
|
+
const payload = {
|
|
129
|
+
amount: amountInSubunits,
|
|
130
|
+
currency: options.currency.toUpperCase(),
|
|
131
|
+
receipt: options.receipt,
|
|
132
|
+
notes: {
|
|
133
|
+
customer_name: options.customer.name,
|
|
134
|
+
customer_email: options.customer.email,
|
|
135
|
+
customer_phone: options.customer.phone,
|
|
136
|
+
...options.notes
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const res = await this.fetchJson(`${this.baseUrl}/orders`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: this.getAuthHeader()
|
|
143
|
+
},
|
|
144
|
+
body: payload
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
gateway: "razorpay",
|
|
148
|
+
orderId: options.receipt,
|
|
149
|
+
gatewayOrderId: res.id,
|
|
150
|
+
amount: options.amount,
|
|
151
|
+
currency: options.currency.toUpperCase(),
|
|
152
|
+
status: "CREATED",
|
|
153
|
+
rawResponse: res
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async verifyPayment(options) {
|
|
157
|
+
if (!options.paymentId) {
|
|
158
|
+
throw new PaymentError("Razorpay payment verification requires paymentId.", { gateway: "razorpay" });
|
|
159
|
+
}
|
|
160
|
+
let isSignatureValid = false;
|
|
161
|
+
if (options.signature) {
|
|
162
|
+
const expectedSignature = hmacSha256(
|
|
163
|
+
`${options.orderId}|${options.paymentId}`,
|
|
164
|
+
this.config.keySecret
|
|
165
|
+
);
|
|
166
|
+
isSignatureValid = safeCompare(expectedSignature, options.signature);
|
|
167
|
+
}
|
|
168
|
+
const payment = await this.fetchJson(`${this.baseUrl}/payments/${options.paymentId}`, {
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: this.getAuthHeader()
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const isCaptured = payment.status === "captured" || payment.status === "authorized";
|
|
175
|
+
return {
|
|
176
|
+
gateway: "razorpay",
|
|
177
|
+
isSuccessful: (options.signature ? isSignatureValid : true) && isCaptured,
|
|
178
|
+
paymentId: payment.id,
|
|
179
|
+
orderId: options.orderId,
|
|
180
|
+
amount: payment.amount / 100,
|
|
181
|
+
currency: payment.currency,
|
|
182
|
+
paymentMethod: payment.method,
|
|
183
|
+
rawResponse: payment
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async refund(options) {
|
|
187
|
+
const payload = {
|
|
188
|
+
notes: { reason: options.reason || "Merchant requested refund" }
|
|
189
|
+
};
|
|
190
|
+
if (options.amount) {
|
|
191
|
+
payload.amount = Math.round(options.amount * 100);
|
|
192
|
+
}
|
|
193
|
+
const res = await this.fetchJson(`${this.baseUrl}/payments/${options.paymentId}/refund`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: this.getAuthHeader()
|
|
197
|
+
},
|
|
198
|
+
body: payload
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
gateway: "razorpay",
|
|
202
|
+
refundId: res.id,
|
|
203
|
+
paymentId: options.paymentId,
|
|
204
|
+
amount: res.amount / 100,
|
|
205
|
+
status: res.status === "processed" ? "SUCCESS" : "PENDING",
|
|
206
|
+
rawResponse: res
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async verifyWebhook(options) {
|
|
210
|
+
const secret = options.webhookSecret || this.config.webhookSecret;
|
|
211
|
+
if (!secret) {
|
|
212
|
+
return {
|
|
213
|
+
isValid: false,
|
|
214
|
+
gateway: "razorpay",
|
|
215
|
+
error: "Razorpay webhookSecret is not configured."
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const signature = this.getHeader(options.headers, "x-razorpay-signature");
|
|
219
|
+
if (!signature || typeof signature !== "string") {
|
|
220
|
+
return {
|
|
221
|
+
isValid: false,
|
|
222
|
+
gateway: "razorpay",
|
|
223
|
+
error: "Missing X-Razorpay-Signature header."
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const rawString = typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8");
|
|
227
|
+
const expectedSignature = hmacSha256(rawString, secret);
|
|
228
|
+
const isValid = safeCompare(expectedSignature, signature);
|
|
229
|
+
let parsedData;
|
|
230
|
+
try {
|
|
231
|
+
parsedData = JSON.parse(rawString);
|
|
232
|
+
} catch {
|
|
233
|
+
parsedData = null;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
isValid,
|
|
237
|
+
gateway: "razorpay",
|
|
238
|
+
event: parsedData?.event,
|
|
239
|
+
data: parsedData?.payload
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/adapters/cashfree.adapter.ts
|
|
245
|
+
var CashfreeAdapter = class extends BasePaymentAdapter {
|
|
246
|
+
constructor(config) {
|
|
247
|
+
super();
|
|
248
|
+
this.config = config;
|
|
249
|
+
this.name = "cashfree";
|
|
250
|
+
if (!config.appId || !config.secretKey) {
|
|
251
|
+
throw new PaymentError("Cashfree appId and secretKey are required.", { gateway: "cashfree" });
|
|
252
|
+
}
|
|
253
|
+
this.baseUrl = config.env === "PRODUCTION" ? "https://api.cashfree.com/pg" : "https://sandbox.cashfree.com/pg";
|
|
254
|
+
this.apiVersion = config.apiVersion || "2023-08-01";
|
|
255
|
+
}
|
|
256
|
+
getHeaders() {
|
|
257
|
+
return {
|
|
258
|
+
"x-client-id": this.config.appId,
|
|
259
|
+
"x-client-secret": this.config.secretKey,
|
|
260
|
+
"x-api-version": this.apiVersion,
|
|
261
|
+
"Content-Type": "application/json"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async createOrder(options) {
|
|
265
|
+
const payload = {
|
|
266
|
+
order_id: options.receipt,
|
|
267
|
+
order_amount: options.amount,
|
|
268
|
+
order_currency: options.currency.toUpperCase(),
|
|
269
|
+
customer_details: {
|
|
270
|
+
customer_id: options.customer.id || `cust_${Date.now()}`,
|
|
271
|
+
customer_name: options.customer.name,
|
|
272
|
+
customer_email: options.customer.email,
|
|
273
|
+
customer_phone: options.customer.phone
|
|
274
|
+
},
|
|
275
|
+
order_meta: {
|
|
276
|
+
return_url: options.redirectUrl,
|
|
277
|
+
notify_url: options.callbackUrl
|
|
278
|
+
},
|
|
279
|
+
order_note: options.notes ? JSON.stringify(options.notes) : "Order via Boost Payments"
|
|
280
|
+
};
|
|
281
|
+
const res = await this.fetchJson(`${this.baseUrl}/orders`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: this.getHeaders(),
|
|
284
|
+
body: payload
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
gateway: "cashfree",
|
|
288
|
+
orderId: options.receipt,
|
|
289
|
+
gatewayOrderId: res.order_id,
|
|
290
|
+
amount: options.amount,
|
|
291
|
+
currency: options.currency.toUpperCase(),
|
|
292
|
+
status: res.order_status === "ACTIVE" ? "CREATED" : "PENDING",
|
|
293
|
+
paymentSessionId: res.payment_session_id,
|
|
294
|
+
rawResponse: res
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async verifyPayment(options) {
|
|
298
|
+
const order = await this.fetchJson(`${this.baseUrl}/orders/${options.orderId}`, {
|
|
299
|
+
method: "GET",
|
|
300
|
+
headers: this.getHeaders()
|
|
301
|
+
});
|
|
302
|
+
const isSuccessful = order.order_status === "PAID";
|
|
303
|
+
let paymentMethod = "unknown";
|
|
304
|
+
let paymentId = options.paymentId || order.order_id;
|
|
305
|
+
if (isSuccessful) {
|
|
306
|
+
try {
|
|
307
|
+
const payments = await this.fetchJson(`${this.baseUrl}/orders/${options.orderId}/payments`, {
|
|
308
|
+
method: "GET",
|
|
309
|
+
headers: this.getHeaders()
|
|
310
|
+
});
|
|
311
|
+
if (Array.isArray(payments) && payments.length > 0) {
|
|
312
|
+
const latest = payments[0];
|
|
313
|
+
paymentId = String(latest.cf_payment_id || paymentId);
|
|
314
|
+
paymentMethod = latest.payment_group || (latest.payment_method ? Object.keys(latest.payment_method)[0] : "online");
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
gateway: "cashfree",
|
|
321
|
+
isSuccessful,
|
|
322
|
+
paymentId,
|
|
323
|
+
orderId: order.order_id,
|
|
324
|
+
amount: order.order_amount,
|
|
325
|
+
currency: order.order_currency,
|
|
326
|
+
paymentMethod,
|
|
327
|
+
rawResponse: order
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async refund(options) {
|
|
331
|
+
if (!options.orderId) {
|
|
332
|
+
throw new PaymentError("Cashfree refund requires orderId.", { gateway: "cashfree" });
|
|
333
|
+
}
|
|
334
|
+
const payload = {
|
|
335
|
+
refund_id: `rfnd_${Date.now()}`,
|
|
336
|
+
refund_amount: options.amount,
|
|
337
|
+
refund_note: options.reason || "Merchant initiated refund"
|
|
338
|
+
};
|
|
339
|
+
const res = await this.fetchJson(`${this.baseUrl}/orders/${options.orderId}/refunds`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: this.getHeaders(),
|
|
342
|
+
body: payload
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
gateway: "cashfree",
|
|
346
|
+
refundId: res.refund_id,
|
|
347
|
+
paymentId: options.paymentId,
|
|
348
|
+
amount: res.refund_amount,
|
|
349
|
+
status: res.refund_status === "SUCCESS" ? "SUCCESS" : "PENDING",
|
|
350
|
+
rawResponse: res
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async verifyWebhook(options) {
|
|
354
|
+
const signature = this.getHeader(options.headers, "x-webhook-signature");
|
|
355
|
+
const timestamp = this.getHeader(options.headers, "x-webhook-timestamp");
|
|
356
|
+
const rawString = typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8");
|
|
357
|
+
let isValid = false;
|
|
358
|
+
if (signature && timestamp) {
|
|
359
|
+
const signedData = `${timestamp}${rawString}`;
|
|
360
|
+
const expectedSignature = Buffer.from(
|
|
361
|
+
hmacSha256(signedData, this.config.secretKey),
|
|
362
|
+
"hex"
|
|
363
|
+
).toString("base64");
|
|
364
|
+
isValid = safeCompare(expectedSignature, signature);
|
|
365
|
+
} else if (signature) {
|
|
366
|
+
const expectedSignature = Buffer.from(
|
|
367
|
+
hmacSha256(rawString, this.config.secretKey),
|
|
368
|
+
"hex"
|
|
369
|
+
).toString("base64");
|
|
370
|
+
isValid = safeCompare(expectedSignature, signature);
|
|
371
|
+
}
|
|
372
|
+
let parsedData;
|
|
373
|
+
try {
|
|
374
|
+
parsedData = JSON.parse(rawString);
|
|
375
|
+
} catch {
|
|
376
|
+
parsedData = null;
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
isValid,
|
|
380
|
+
gateway: "cashfree",
|
|
381
|
+
event: parsedData?.type,
|
|
382
|
+
data: parsedData?.data
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/adapters/phonepe.adapter.ts
|
|
388
|
+
var PhonePeAdapter = class extends BasePaymentAdapter {
|
|
389
|
+
constructor(config) {
|
|
390
|
+
super();
|
|
391
|
+
this.config = config;
|
|
392
|
+
this.name = "phonepe";
|
|
393
|
+
if (!config.merchantId || !config.saltKey) {
|
|
394
|
+
throw new PaymentError("PhonePe merchantId and saltKey are required.", { gateway: "phonepe" });
|
|
395
|
+
}
|
|
396
|
+
this.baseUrl = config.env === "PRODUCTION" ? "https://api.phonepe.com/apis/hermes" : "https://api-preprod.phonepe.com/apis/pg-sandbox";
|
|
397
|
+
this.saltIndex = config.saltIndex || "1";
|
|
398
|
+
}
|
|
399
|
+
calculateXVerify(base64Payload, endpoint) {
|
|
400
|
+
const stringToHash = `${base64Payload}${endpoint}${this.config.saltKey}`;
|
|
401
|
+
const hash = sha256(stringToHash);
|
|
402
|
+
return `${hash}###${this.saltIndex}`;
|
|
403
|
+
}
|
|
404
|
+
async createOrder(options) {
|
|
405
|
+
const amountInPaise = Math.round(options.amount * 100);
|
|
406
|
+
const payload = {
|
|
407
|
+
merchantId: this.config.merchantId,
|
|
408
|
+
merchantTransactionId: options.receipt,
|
|
409
|
+
merchantUserId: options.customer.id || `MUID_${Date.now()}`,
|
|
410
|
+
amount: amountInPaise,
|
|
411
|
+
redirectUrl: options.redirectUrl || "https://yourstore.com/order-success",
|
|
412
|
+
redirectMode: "POST",
|
|
413
|
+
callbackUrl: options.callbackUrl || "https://api.yourstore.com/webhooks/phonepe",
|
|
414
|
+
mobileNumber: options.customer.phone.replace(/[^0-9]/g, "").slice(-10),
|
|
415
|
+
paymentInstrument: {
|
|
416
|
+
type: "PAY_PAGE"
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const base64Payload = base64Encode(payload);
|
|
420
|
+
const endpoint = "/pg/v1/pay";
|
|
421
|
+
const xVerify = this.calculateXVerify(base64Payload, endpoint);
|
|
422
|
+
const res = await this.fetchJson(`${this.baseUrl}${endpoint}`, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
"Content-Type": "application/json",
|
|
426
|
+
"X-VERIFY": xVerify
|
|
427
|
+
},
|
|
428
|
+
body: {
|
|
429
|
+
request: base64Payload
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
const redirectUrl = res?.data?.instrumentResponse?.redirectInfo?.url;
|
|
433
|
+
return {
|
|
434
|
+
gateway: "phonepe",
|
|
435
|
+
orderId: options.receipt,
|
|
436
|
+
gatewayOrderId: options.receipt,
|
|
437
|
+
amount: options.amount,
|
|
438
|
+
currency: options.currency.toUpperCase(),
|
|
439
|
+
status: res.success ? "CREATED" : "FAILED",
|
|
440
|
+
redirectUrl,
|
|
441
|
+
rawResponse: res
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async verifyPayment(options) {
|
|
445
|
+
const endpoint = `/pg/v1/status/${this.config.merchantId}/${options.orderId}`;
|
|
446
|
+
const stringToHash = `${endpoint}${this.config.saltKey}`;
|
|
447
|
+
const xVerify = `${sha256(stringToHash)}###${this.saltIndex}`;
|
|
448
|
+
const res = await this.fetchJson(`${this.baseUrl}${endpoint}`, {
|
|
449
|
+
method: "GET",
|
|
450
|
+
headers: {
|
|
451
|
+
"Content-Type": "application/json",
|
|
452
|
+
"X-VERIFY": xVerify,
|
|
453
|
+
"X-MERCHANT-ID": this.config.merchantId
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
const isSuccessful = res.code === "PAYMENT_SUCCESS";
|
|
457
|
+
const amount = res.data?.amount ? res.data.amount / 100 : 0;
|
|
458
|
+
const paymentId = res.data?.transactionId || options.orderId;
|
|
459
|
+
return {
|
|
460
|
+
gateway: "phonepe",
|
|
461
|
+
isSuccessful,
|
|
462
|
+
paymentId,
|
|
463
|
+
orderId: options.orderId,
|
|
464
|
+
amount,
|
|
465
|
+
currency: "INR",
|
|
466
|
+
paymentMethod: res.data?.paymentInstrument?.type || "UPI",
|
|
467
|
+
rawResponse: res
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async refund(options) {
|
|
471
|
+
const refundTxnId = `RF_${Date.now()}`;
|
|
472
|
+
const amountInPaise = options.amount ? Math.round(options.amount * 100) : 0;
|
|
473
|
+
const payload = {
|
|
474
|
+
merchantId: this.config.merchantId,
|
|
475
|
+
merchantTransactionId: refundTxnId,
|
|
476
|
+
originalTransactionId: options.orderId || options.paymentId,
|
|
477
|
+
amount: amountInPaise,
|
|
478
|
+
callbackUrl: "https://api.yourstore.com/webhooks/phonepe"
|
|
479
|
+
};
|
|
480
|
+
const base64Payload = base64Encode(payload);
|
|
481
|
+
const endpoint = "/pg/v1/refund";
|
|
482
|
+
const xVerify = this.calculateXVerify(base64Payload, endpoint);
|
|
483
|
+
const res = await this.fetchJson(`${this.baseUrl}${endpoint}`, {
|
|
484
|
+
method: "POST",
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
"X-VERIFY": xVerify
|
|
488
|
+
},
|
|
489
|
+
body: {
|
|
490
|
+
request: base64Payload
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
gateway: "phonepe",
|
|
495
|
+
refundId: refundTxnId,
|
|
496
|
+
paymentId: options.paymentId,
|
|
497
|
+
amount: options.amount || 0,
|
|
498
|
+
status: res.success ? "SUCCESS" : "FAILED",
|
|
499
|
+
rawResponse: res
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async verifyWebhook(options) {
|
|
503
|
+
const rawString = typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8");
|
|
504
|
+
let parsed;
|
|
505
|
+
try {
|
|
506
|
+
parsed = JSON.parse(rawString);
|
|
507
|
+
} catch {
|
|
508
|
+
return { isValid: false, gateway: "phonepe", error: "Invalid JSON payload" };
|
|
509
|
+
}
|
|
510
|
+
if (!parsed.response) {
|
|
511
|
+
return { isValid: false, gateway: "phonepe", error: "Missing response field in PhonePe callback" };
|
|
512
|
+
}
|
|
513
|
+
const decoded = JSON.parse(base64Decode(parsed.response));
|
|
514
|
+
decoded.code === "PAYMENT_SUCCESS";
|
|
515
|
+
return {
|
|
516
|
+
isValid: true,
|
|
517
|
+
gateway: "phonepe",
|
|
518
|
+
event: decoded.code,
|
|
519
|
+
data: decoded.data
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/adapters/paytm.adapter.ts
|
|
525
|
+
var PaytmAdapter = class extends BasePaymentAdapter {
|
|
526
|
+
constructor(config) {
|
|
527
|
+
super();
|
|
528
|
+
this.config = config;
|
|
529
|
+
this.name = "paytm";
|
|
530
|
+
if (!config.mid || !config.merchantKey) {
|
|
531
|
+
throw new PaymentError("Paytm mid and merchantKey are required.", { gateway: "paytm" });
|
|
532
|
+
}
|
|
533
|
+
this.baseUrl = config.env === "PRODUCTION" ? "https://securegw.paytm.in" : "https://securegw-stage.paytm.in";
|
|
534
|
+
}
|
|
535
|
+
async createOrder(options) {
|
|
536
|
+
const payload = {
|
|
537
|
+
body: {
|
|
538
|
+
requestType: "Payment",
|
|
539
|
+
mid: this.config.mid,
|
|
540
|
+
websiteName: this.config.website || "DEFAULT",
|
|
541
|
+
orderId: options.receipt,
|
|
542
|
+
callbackUrl: options.callbackUrl || "https://api.yourstore.com/webhooks/paytm",
|
|
543
|
+
txnAmount: {
|
|
544
|
+
value: options.amount.toFixed(2),
|
|
545
|
+
currency: "INR"
|
|
546
|
+
},
|
|
547
|
+
userInfo: {
|
|
548
|
+
custId: options.customer.id || `CUST_${Date.now()}`,
|
|
549
|
+
mobile: options.customer.phone.replace(/[^0-9]/g, "").slice(-10),
|
|
550
|
+
email: options.customer.email
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const url = `${this.baseUrl}/theia/api/v1/initiateTransaction?mid=${this.config.mid}&orderId=${options.receipt}`;
|
|
555
|
+
const res = await this.fetchJson(url, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: payload
|
|
558
|
+
});
|
|
559
|
+
const txnToken = res?.body?.txnToken;
|
|
560
|
+
return {
|
|
561
|
+
gateway: "paytm",
|
|
562
|
+
orderId: options.receipt,
|
|
563
|
+
gatewayOrderId: options.receipt,
|
|
564
|
+
amount: options.amount,
|
|
565
|
+
currency: "INR",
|
|
566
|
+
status: txnToken ? "CREATED" : "FAILED",
|
|
567
|
+
txnToken,
|
|
568
|
+
rawResponse: res
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
async verifyPayment(options) {
|
|
572
|
+
const url = `${this.baseUrl}/v3/order/status`;
|
|
573
|
+
const payload = {
|
|
574
|
+
body: {
|
|
575
|
+
mid: this.config.mid,
|
|
576
|
+
orderId: options.orderId
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const res = await this.fetchJson(url, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
body: payload
|
|
582
|
+
});
|
|
583
|
+
const body = res?.body || {};
|
|
584
|
+
const isSuccessful = body.resultInfo?.resultStatus === "TXN_SUCCESS";
|
|
585
|
+
return {
|
|
586
|
+
gateway: "paytm",
|
|
587
|
+
isSuccessful,
|
|
588
|
+
paymentId: body.txnId || options.orderId,
|
|
589
|
+
orderId: options.orderId,
|
|
590
|
+
amount: parseFloat(body.txnAmount || "0"),
|
|
591
|
+
currency: "INR",
|
|
592
|
+
paymentMethod: body.paymentMode || "ONLINE",
|
|
593
|
+
rawResponse: res
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async refund(options) {
|
|
597
|
+
const refundRefId = `RF_${Date.now()}`;
|
|
598
|
+
const url = `${this.baseUrl}/refund/apply`;
|
|
599
|
+
const payload = {
|
|
600
|
+
body: {
|
|
601
|
+
mid: this.config.mid,
|
|
602
|
+
txnType: "REFUND",
|
|
603
|
+
orderId: options.orderId,
|
|
604
|
+
txnId: options.paymentId,
|
|
605
|
+
refId: refundRefId,
|
|
606
|
+
refundAmount: (options.amount || 0).toFixed(2)
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const res = await this.fetchJson(url, {
|
|
610
|
+
method: "POST",
|
|
611
|
+
body: payload
|
|
612
|
+
});
|
|
613
|
+
const body = res?.body || {};
|
|
614
|
+
const isSuccess = body.resultInfo?.resultStatus === "TXN_SUCCESS" || body.resultInfo?.resultStatus === "PENDING";
|
|
615
|
+
return {
|
|
616
|
+
gateway: "paytm",
|
|
617
|
+
refundId: body.refundId || refundRefId,
|
|
618
|
+
paymentId: options.paymentId,
|
|
619
|
+
amount: options.amount || 0,
|
|
620
|
+
status: isSuccess ? "SUCCESS" : "FAILED",
|
|
621
|
+
rawResponse: res
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async verifyWebhook(options) {
|
|
625
|
+
const rawString = typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8");
|
|
626
|
+
let parsed;
|
|
627
|
+
try {
|
|
628
|
+
parsed = JSON.parse(rawString);
|
|
629
|
+
} catch {
|
|
630
|
+
const params = new URLSearchParams(rawString);
|
|
631
|
+
parsed = Object.fromEntries(params.entries());
|
|
632
|
+
}
|
|
633
|
+
const isSuccess = parsed?.STATUS === "TXN_SUCCESS" || parsed?.resultInfo?.resultStatus === "TXN_SUCCESS";
|
|
634
|
+
return {
|
|
635
|
+
isValid: true,
|
|
636
|
+
gateway: "paytm",
|
|
637
|
+
event: isSuccess ? "TXN_SUCCESS" : "TXN_FAILURE",
|
|
638
|
+
data: parsed
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// src/adapters/stripe.adapter.ts
|
|
644
|
+
var StripeAdapter = class extends BasePaymentAdapter {
|
|
645
|
+
constructor(config) {
|
|
646
|
+
super();
|
|
647
|
+
this.config = config;
|
|
648
|
+
this.name = "stripe";
|
|
649
|
+
this.baseUrl = "https://api.stripe.com/v1";
|
|
650
|
+
if (!config.secretKey) {
|
|
651
|
+
throw new PaymentError("Stripe secretKey is required.", { gateway: "stripe" });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
getAuthHeader() {
|
|
655
|
+
return {
|
|
656
|
+
Authorization: `Bearer ${this.config.secretKey}`,
|
|
657
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async createOrder(options) {
|
|
661
|
+
const amountInCents = Math.round(options.amount * 100);
|
|
662
|
+
const body = {
|
|
663
|
+
"payment_method_types[0]": "card",
|
|
664
|
+
mode: "payment",
|
|
665
|
+
client_reference_id: options.receipt,
|
|
666
|
+
customer_email: options.customer.email,
|
|
667
|
+
"line_items[0][price_data][currency]": options.currency.toLowerCase(),
|
|
668
|
+
"line_items[0][price_data][unit_amount]": String(amountInCents),
|
|
669
|
+
"line_items[0][price_data][product_data][name]": `Order #${options.receipt}`,
|
|
670
|
+
"line_items[0][quantity]": "1",
|
|
671
|
+
success_url: options.redirectUrl || "https://yourstore.com/order-success?session_id={CHECKOUT_SESSION_ID}",
|
|
672
|
+
cancel_url: "https://yourstore.com/cart"
|
|
673
|
+
};
|
|
674
|
+
const res = await this.fetchJson(`${this.baseUrl}/checkout/sessions`, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: this.getAuthHeader(),
|
|
677
|
+
body
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
gateway: "stripe",
|
|
681
|
+
orderId: options.receipt,
|
|
682
|
+
gatewayOrderId: res.id,
|
|
683
|
+
amount: options.amount,
|
|
684
|
+
currency: options.currency.toUpperCase(),
|
|
685
|
+
status: "CREATED",
|
|
686
|
+
redirectUrl: res.url,
|
|
687
|
+
rawResponse: res
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async verifyPayment(options) {
|
|
691
|
+
const session = await this.fetchJson(`${this.baseUrl}/checkout/sessions/${options.orderId}`, {
|
|
692
|
+
method: "GET",
|
|
693
|
+
headers: this.getAuthHeader()
|
|
694
|
+
});
|
|
695
|
+
const isSuccessful = session.payment_status === "paid";
|
|
696
|
+
const amount = session.amount_total ? session.amount_total / 100 : 0;
|
|
697
|
+
const paymentId = session.payment_intent || session.id;
|
|
698
|
+
return {
|
|
699
|
+
gateway: "stripe",
|
|
700
|
+
isSuccessful,
|
|
701
|
+
paymentId,
|
|
702
|
+
orderId: session.client_reference_id || session.id,
|
|
703
|
+
amount,
|
|
704
|
+
currency: (session.currency || "USD").toUpperCase(),
|
|
705
|
+
paymentMethod: "card",
|
|
706
|
+
rawResponse: session
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async refund(options) {
|
|
710
|
+
const body = {
|
|
711
|
+
payment_intent: options.paymentId
|
|
712
|
+
};
|
|
713
|
+
if (options.amount) {
|
|
714
|
+
body.amount = String(Math.round(options.amount * 100));
|
|
715
|
+
}
|
|
716
|
+
if (options.reason) {
|
|
717
|
+
body.reason = "requested_by_customer";
|
|
718
|
+
}
|
|
719
|
+
const res = await this.fetchJson(`${this.baseUrl}/refunds`, {
|
|
720
|
+
method: "POST",
|
|
721
|
+
headers: this.getAuthHeader(),
|
|
722
|
+
body
|
|
723
|
+
});
|
|
724
|
+
return {
|
|
725
|
+
gateway: "stripe",
|
|
726
|
+
refundId: res.id,
|
|
727
|
+
paymentId: options.paymentId,
|
|
728
|
+
amount: res.amount / 100,
|
|
729
|
+
status: res.status === "succeeded" ? "SUCCESS" : "PENDING",
|
|
730
|
+
rawResponse: res
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async verifyWebhook(options) {
|
|
734
|
+
const secret = options.webhookSecret || this.config.webhookSecret;
|
|
735
|
+
if (!secret) {
|
|
736
|
+
return { isValid: false, gateway: "stripe", error: "Stripe webhookSecret is not configured." };
|
|
737
|
+
}
|
|
738
|
+
const sigHeader = this.getHeader(options.headers, "stripe-signature");
|
|
739
|
+
if (!sigHeader || typeof sigHeader !== "string") {
|
|
740
|
+
return { isValid: false, gateway: "stripe", error: "Missing stripe-signature header." };
|
|
741
|
+
}
|
|
742
|
+
const parts = sigHeader.split(",");
|
|
743
|
+
let timestamp = "";
|
|
744
|
+
const signatures = [];
|
|
745
|
+
parts.forEach((part) => {
|
|
746
|
+
const [key, val] = part.split("=");
|
|
747
|
+
if (key === "t") timestamp = val;
|
|
748
|
+
if (key === "v1") signatures.push(val);
|
|
749
|
+
});
|
|
750
|
+
const rawString = typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8");
|
|
751
|
+
const signedPayload = `${timestamp}.${rawString}`;
|
|
752
|
+
const expectedSignature = hmacSha256(signedPayload, secret);
|
|
753
|
+
const isValid = signatures.some((sig) => safeCompare(expectedSignature, sig));
|
|
754
|
+
let parsed;
|
|
755
|
+
try {
|
|
756
|
+
parsed = JSON.parse(rawString);
|
|
757
|
+
} catch {
|
|
758
|
+
parsed = null;
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
isValid,
|
|
762
|
+
gateway: "stripe",
|
|
763
|
+
event: parsed?.type,
|
|
764
|
+
data: parsed?.data?.object
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// src/adapters/cod.adapter.ts
|
|
770
|
+
var CODAdapter = class extends BasePaymentAdapter {
|
|
771
|
+
constructor(config = {}) {
|
|
772
|
+
super();
|
|
773
|
+
this.config = config;
|
|
774
|
+
this.name = "cod";
|
|
775
|
+
}
|
|
776
|
+
async createOrder(options) {
|
|
777
|
+
const min = this.config.minOrderValue ?? 0;
|
|
778
|
+
const max = this.config.maxOrderValue ?? 1e4;
|
|
779
|
+
if (options.amount < min) {
|
|
780
|
+
throw new PaymentError(`Order amount ${options.amount} is below minimum COD threshold of ${min}`, {
|
|
781
|
+
gateway: "cod",
|
|
782
|
+
statusCode: 400
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
if (options.amount > max) {
|
|
786
|
+
throw new PaymentError(`Order amount ${options.amount} exceeds maximum COD limit of ${max}`, {
|
|
787
|
+
gateway: "cod",
|
|
788
|
+
statusCode: 400
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const codFee = this.config.extraFee || 0;
|
|
792
|
+
const totalAmount = options.amount + codFee;
|
|
793
|
+
return {
|
|
794
|
+
gateway: "cod",
|
|
795
|
+
orderId: options.receipt,
|
|
796
|
+
gatewayOrderId: `COD_${options.receipt}`,
|
|
797
|
+
amount: totalAmount,
|
|
798
|
+
currency: options.currency.toUpperCase(),
|
|
799
|
+
status: "CREATED",
|
|
800
|
+
rawResponse: {
|
|
801
|
+
paymentMode: "Cash On Delivery",
|
|
802
|
+
baseAmount: options.amount,
|
|
803
|
+
codFee,
|
|
804
|
+
totalPayable: totalAmount
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async verifyPayment(options) {
|
|
809
|
+
return {
|
|
810
|
+
gateway: "cod",
|
|
811
|
+
isSuccessful: true,
|
|
812
|
+
paymentId: `COD_COLLECTED_${options.orderId}`,
|
|
813
|
+
orderId: options.orderId,
|
|
814
|
+
amount: 0,
|
|
815
|
+
currency: "INR",
|
|
816
|
+
paymentMethod: "cash_on_delivery",
|
|
817
|
+
rawResponse: { status: "COLLECTED" }
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
async refund(options) {
|
|
821
|
+
return {
|
|
822
|
+
gateway: "cod",
|
|
823
|
+
refundId: `COD_RF_${Date.now()}`,
|
|
824
|
+
paymentId: options.paymentId,
|
|
825
|
+
amount: options.amount || 0,
|
|
826
|
+
status: "SUCCESS",
|
|
827
|
+
rawResponse: { note: "Manual cash/store-credit refund for COD" }
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async verifyWebhook(_options) {
|
|
831
|
+
return {
|
|
832
|
+
isValid: true,
|
|
833
|
+
gateway: "cod",
|
|
834
|
+
event: "COD_ORDER",
|
|
835
|
+
data: {}
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/manager.ts
|
|
841
|
+
var PaymentManager = class {
|
|
842
|
+
constructor(options) {
|
|
843
|
+
this.adapters = /* @__PURE__ */ new Map();
|
|
844
|
+
this.defaultGateway = options.defaultGateway;
|
|
845
|
+
this.smartRouting = options.smartRouting;
|
|
846
|
+
const { gateways } = options;
|
|
847
|
+
if (gateways.razorpay) {
|
|
848
|
+
this.adapters.set("razorpay", new RazorpayAdapter(gateways.razorpay));
|
|
849
|
+
}
|
|
850
|
+
if (gateways.cashfree) {
|
|
851
|
+
this.adapters.set("cashfree", new CashfreeAdapter(gateways.cashfree));
|
|
852
|
+
}
|
|
853
|
+
if (gateways.phonepe) {
|
|
854
|
+
this.adapters.set("phonepe", new PhonePeAdapter(gateways.phonepe));
|
|
855
|
+
}
|
|
856
|
+
if (gateways.paytm) {
|
|
857
|
+
this.adapters.set("paytm", new PaytmAdapter(gateways.paytm));
|
|
858
|
+
}
|
|
859
|
+
if (gateways.stripe) {
|
|
860
|
+
this.adapters.set("stripe", new StripeAdapter(gateways.stripe));
|
|
861
|
+
}
|
|
862
|
+
if (gateways.cod) {
|
|
863
|
+
this.adapters.set("cod", new CODAdapter(gateways.cod));
|
|
864
|
+
}
|
|
865
|
+
if (this.adapters.size === 0) {
|
|
866
|
+
console.warn("\u26A0\uFE0F [PaymentManager] No payment gateways were configured in PaymentManager.");
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Returns an active adapter instance by gateway name.
|
|
871
|
+
*/
|
|
872
|
+
getAdapter(gateway) {
|
|
873
|
+
const adapter = this.adapters.get(gateway);
|
|
874
|
+
if (!adapter) {
|
|
875
|
+
throw new GatewayNotConfiguredError(gateway);
|
|
876
|
+
}
|
|
877
|
+
return adapter;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Lists all currently registered gateway names.
|
|
881
|
+
*/
|
|
882
|
+
listConfiguredGateways() {
|
|
883
|
+
return Array.from(this.adapters.keys());
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Resolves the optimal gateway based on currency rules, explicit override, or default.
|
|
887
|
+
*/
|
|
888
|
+
resolveGateway(options) {
|
|
889
|
+
if (options.gateway) {
|
|
890
|
+
return options.gateway;
|
|
891
|
+
}
|
|
892
|
+
if (options.currency && this.smartRouting?.currencyMap) {
|
|
893
|
+
const mapped = this.smartRouting.currencyMap[options.currency.toUpperCase()];
|
|
894
|
+
if (mapped && this.adapters.has(mapped)) {
|
|
895
|
+
return mapped;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (this.defaultGateway && this.adapters.has(this.defaultGateway)) {
|
|
899
|
+
return this.defaultGateway;
|
|
900
|
+
}
|
|
901
|
+
const firstAvailable = this.adapters.keys().next().value;
|
|
902
|
+
if (firstAvailable) {
|
|
903
|
+
return firstAvailable;
|
|
904
|
+
}
|
|
905
|
+
throw new PaymentError("No payment gateways configured to process order.");
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Create an order using the chosen or automatically resolved gateway.
|
|
909
|
+
*/
|
|
910
|
+
async createOrder(options) {
|
|
911
|
+
const targetGateway = this.resolveGateway({
|
|
912
|
+
gateway: options.gateway,
|
|
913
|
+
currency: options.currency
|
|
914
|
+
});
|
|
915
|
+
const adapter = this.getAdapter(targetGateway);
|
|
916
|
+
return adapter.createOrder(options);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Smart Fallback: Attempts creation on primary gateway. If it throws an error or fails,
|
|
920
|
+
* it automatically routes through fallback gateways in sequence!
|
|
921
|
+
*/
|
|
922
|
+
async createOrderWithFallback(options) {
|
|
923
|
+
const chain = options.fallbackChain || this.smartRouting?.fallbackChain || this.listConfiguredGateways();
|
|
924
|
+
if (chain.length === 0) {
|
|
925
|
+
throw new PaymentError("Fallback chain is empty. Configure at least one gateway.");
|
|
926
|
+
}
|
|
927
|
+
let lastError;
|
|
928
|
+
for (const gw of chain) {
|
|
929
|
+
if (!this.adapters.has(gw)) continue;
|
|
930
|
+
try {
|
|
931
|
+
const adapter = this.getAdapter(gw);
|
|
932
|
+
const result = await adapter.createOrder({ ...options, gateway: gw });
|
|
933
|
+
return result;
|
|
934
|
+
} catch (err) {
|
|
935
|
+
lastError = err;
|
|
936
|
+
console.warn(`[PaymentManager Fallback] Gateway '${gw}' failed (${err.message}). Trying next gateway in chain...`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
throw new PaymentError(
|
|
940
|
+
`All gateways in fallback chain [${chain.join(", ")}] failed. Last error: ${lastError?.message || "Unknown"}`,
|
|
941
|
+
{ rawError: lastError }
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Verifies payment completion signature or status query.
|
|
946
|
+
*/
|
|
947
|
+
async verifyPayment(options) {
|
|
948
|
+
const adapter = this.getAdapter(options.gateway);
|
|
949
|
+
return adapter.verifyPayment(options);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Initiates a customer refund.
|
|
953
|
+
*/
|
|
954
|
+
async refund(options) {
|
|
955
|
+
const adapter = this.getAdapter(options.gateway);
|
|
956
|
+
return adapter.refund(options);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Verifies incoming webhook authenticity and decodes payload.
|
|
960
|
+
*/
|
|
961
|
+
async verifyWebhook(options) {
|
|
962
|
+
const adapter = this.getAdapter(options.gateway);
|
|
963
|
+
return adapter.verifyWebhook(options);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
function createPaymentManager(options) {
|
|
967
|
+
return new PaymentManager(options);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
exports.BasePaymentAdapter = BasePaymentAdapter;
|
|
971
|
+
exports.CODAdapter = CODAdapter;
|
|
972
|
+
exports.CashfreeAdapter = CashfreeAdapter;
|
|
973
|
+
exports.GatewayNotConfiguredError = GatewayNotConfiguredError;
|
|
974
|
+
exports.PaymentError = PaymentError;
|
|
975
|
+
exports.PaymentManager = PaymentManager;
|
|
976
|
+
exports.PaytmAdapter = PaytmAdapter;
|
|
977
|
+
exports.PhonePeAdapter = PhonePeAdapter;
|
|
978
|
+
exports.RazorpayAdapter = RazorpayAdapter;
|
|
979
|
+
exports.SignatureVerificationError = SignatureVerificationError;
|
|
980
|
+
exports.StripeAdapter = StripeAdapter;
|
|
981
|
+
exports.base64Decode = base64Decode;
|
|
982
|
+
exports.base64Encode = base64Encode;
|
|
983
|
+
exports.createPaymentManager = createPaymentManager;
|
|
984
|
+
exports.hmacSha256 = hmacSha256;
|
|
985
|
+
exports.safeCompare = safeCompare;
|
|
986
|
+
exports.sha256 = sha256;
|
|
987
|
+
//# sourceMappingURL=index.cjs.map
|
|
988
|
+
//# sourceMappingURL=index.cjs.map
|