@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/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