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