@boostengine/payments 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # @boostengine/payments
2
2
 
3
- > **Unified Multi-Gateway Payment Orchestration Layer** for Indian & Global eCommerce. Seamlessly plug in **Razorpay, Cashfree, PhonePe, Paytm, Stripe, and COD** through a single unified API with smart routing, automatic fallbacks, and signature verification.
3
+ > **Unified Multi-Gateway Payment Orchestration Layer** for Indian & Global eCommerce. Seamlessly integrate **Razorpay, Cashfree, PhonePe, Paytm, Stripe, and Cash On Delivery (COD)** through a single unified API with smart routing, automatic fallbacks, Next.js App Router webhook helpers, and React checkout hooks.
4
4
 
5
5
  ---
6
6
 
7
- ## ⚡ Why @boostengine/payments?
7
+ ## ⚡ Key Highlights
8
8
 
9
- - 🔌 **Unified API**: Write your payment flow once. Switch or combine Razorpay, Cashfree, PhonePe, or Stripe without rewriting checkout logic.
10
- - 🛡️ **Smart Fallback**: If your primary payment gateway has a downtime or failure, transactions automatically failover to your secondary gateway!
11
- - 💱 **Multi-Currency Auto-Routing**: Automatically route USD/EUR to Stripe and INR to Razorpay/Cashfree/PhonePe.
12
- - 🔐 **Built-in Signature Verification**: Native verification for Razorpay HMAC, Cashfree webhook, PhonePe SHA-256 + Salt `X-VERIFY`, and Stripe webhooks.
9
+ - 🔌 **Universal Checkout API**: Pass standard amounts (e.g. `1499.00`). Subunits (paise/cents) are normalized internally!
10
+ - 🛡️ **High-Availability Fallback**: If your primary gateway is down or bank servers timeout, automatically failover to your secondary gateway!
11
+ - 💱 **Smart Currency Routing**: Automatically route USD/EUR to Stripe and INR to Cashfree/Razorpay/PhonePe.
12
+ - **Next.js App Router Native**: 2-line webhook verification with `payments.verifyNextJsWebhook(req, { gateway: 'razorpay' })`.
13
+ - 🏷️ **Normalized Webhook Events**: Standardized events like `'PAYMENT_SUCCESS'`, `'PAYMENT_FAILED'`, `'REFUND_PROCESSED'`.
14
+ - ⚛️ **Client Checkout Hook**: `useBoostPayment()` hook to open Razorpay modals or Cashfree dropin with automatic SDK loading!
13
15
  - 🪶 **Zero Dependency Bloat**: Uses native Node.js `fetch` and `crypto`. No 10 heavy third-party vendor SDKs.
14
16
 
15
17
  ---
@@ -22,18 +24,14 @@ npm install @boostengine/payments
22
24
 
23
25
  ---
24
26
 
25
- ## 🚀 Quickstart
26
-
27
- ### 1. Initialize PaymentManager
27
+ ## 🚀 1. Server-Side Setup (`lib/payments.ts`)
28
28
 
29
29
  ```typescript
30
30
  import { createPaymentManager } from '@boostengine/payments';
31
31
 
32
32
  export const payments = createPaymentManager({
33
- // Default gateway when none is specified
34
33
  defaultGateway: 'cashfree',
35
34
 
36
- // Configure any or all gateways
37
35
  gateways: {
38
36
  cashfree: {
39
37
  appId: process.env.CASHFREE_APP_ID!,
@@ -62,7 +60,7 @@ export const payments = createPaymentManager({
62
60
  },
63
61
  },
64
62
 
65
- // Smart Routing & Fallbacks
63
+ // Smart Routing & Resilience
66
64
  smartRouting: {
67
65
  currencyMap: {
68
66
  USD: 'stripe',
@@ -76,43 +74,131 @@ export const payments = createPaymentManager({
76
74
 
77
75
  ---
78
76
 
79
- ### 2. Create an Order (Single or Multi-Gateway)
77
+ ## 💳 2. Unified Order Creation (Server Route)
78
+
79
+ Always pass standard human currency units (e.g. `1499.00`). Package handles paise/cents conversions automatically:
80
80
 
81
81
  ```typescript
82
- // Works identically across Cashfree, Razorpay, PhonePe, Stripe, and COD!
83
- const order = await payments.createOrder({
84
- amount: 1499.00,
85
- currency: 'INR',
86
- receipt: `order_${Date.now()}`,
87
- customer: {
88
- name: 'Aman Sharma',
89
- email: 'aman@example.com',
90
- phone: '9876543210',
91
- },
92
- // Optionally override gateway per checkout button:
93
- // gateway: 'razorpay' | 'phonepe' | 'cashfree' | 'stripe' | 'cod'
94
- });
82
+ // Next.js Route Handler / Express
83
+ export async function POST(req: Request) {
84
+ const { amount, customer, chosenGateway } = await req.json();
85
+
86
+ const order = await payments.createOrder({
87
+ amount: 1499.00, // Always in standard currency (e.g. Rs. 1499.00)
88
+ currency: 'INR',
89
+ receipt: `order_${Date.now()}`,
90
+ customer: {
91
+ name: customer.name,
92
+ email: customer.email,
93
+ phone: customer.phone,
94
+ },
95
+ gateway: chosenGateway, // or let smartRouting decide
96
+ });
95
97
 
96
- console.log(order);
97
- /*
98
- Output:
99
- {
100
- gateway: 'cashfree',
101
- orderId: 'order_1709923812',
102
- gatewayOrderId: 'order_1709923812',
103
- amount: 1499,
104
- currency: 'INR',
105
- status: 'CREATED',
106
- paymentSessionId: 'session_cf_109283719283', // Ready for Cashfree Dropin
107
- redirectUrl: '...', // Available for PhonePe / Stripe
108
- rawResponse: { ... }
98
+ return Response.json(order);
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 🔥 3. Next.js App Router Webhook (`app/api/webhooks/[gateway]/route.ts`)
105
+
106
+ No manual stream parsing or signature math required:
107
+
108
+ ```typescript
109
+ import { payments } from '@/lib/payments';
110
+
111
+ export async function POST(
112
+ req: Request,
113
+ { params }: { params: { gateway: string } }
114
+ ) {
115
+ const result = await payments.verifyNextJsWebhook(req, {
116
+ gateway: params.gateway as any,
117
+ });
118
+
119
+ if (!result.isValid) {
120
+ return new Response('Invalid Signature', { status: 400 });
121
+ }
122
+
123
+ // Use standardized normalized events across all gateways!
124
+ switch (result.normalizedEvent) {
125
+ case 'PAYMENT_SUCCESS':
126
+ console.log(`✅ Order ${result.orderId} paid successfully! Amount: ${result.amount}`);
127
+ // Update database: Order status -> PAID
128
+ break;
129
+
130
+ case 'PAYMENT_FAILED':
131
+ console.log(`❌ Order ${result.orderId} failed.`);
132
+ break;
133
+
134
+ case 'REFUND_PROCESSED':
135
+ console.log(`🔄 Refund processed for payment ${result.paymentId}`);
136
+ break;
137
+ }
138
+
139
+ return new Response('OK');
109
140
  }
110
- */
111
141
  ```
112
142
 
113
143
  ---
114
144
 
115
- ### 3. Smart Fallback (High-Availability Checkout)
145
+ ## ⚛️ 4. Frontend Checkout Hook (`useBoostPayment`)
146
+
147
+ Automatically detects gateway, dynamically loads required vendor script, and triggers modals or redirects:
148
+
149
+ ```tsx
150
+ 'use client';
151
+
152
+ import { useBoostPayment } from '@boostengine/payments/react';
153
+ import { useRouter } from 'next/navigation';
154
+
155
+ export default function CheckoutButton({ cartTotal, customer }) {
156
+ const router = useRouter();
157
+ const { openPaymentModal, isProcessing } = useBoostPayment();
158
+
159
+ const handleCheckout = async (gateway: 'razorpay' | 'cashfree' | 'phonepe' | 'cod') => {
160
+ // 1. Call your server API to create order
161
+ const res = await fetch('/api/orders/create', {
162
+ method: 'POST',
163
+ body: JSON.stringify({ amount: cartTotal, customer, chosenGateway: gateway }),
164
+ });
165
+ const order = await res.json();
166
+
167
+ // 2. Open checkout modal / redirect automatically
168
+ openPaymentModal({
169
+ order,
170
+ name: 'Boost Engine Store',
171
+ description: 'Order Checkout',
172
+ themeColor: '#4f46e5',
173
+ onSuccess: async (response) => {
174
+ // Automatically called when Razorpay modal succeeds or COD is chosen
175
+ router.push(`/order-confirmed?id=${response.orderId}`);
176
+ },
177
+ onFailure: (err) => {
178
+ alert(err.message);
179
+ },
180
+ });
181
+ };
182
+
183
+ return (
184
+ <div className="flex gap-2">
185
+ <button disabled={isProcessing} onClick={() => handleCheckout('razorpay')}>
186
+ Pay with Razorpay
187
+ </button>
188
+ <button disabled={isProcessing} onClick={() => handleCheckout('cashfree')}>
189
+ Pay with Cashfree
190
+ </button>
191
+ <button disabled={isProcessing} onClick={() => handleCheckout('cod')}>
192
+ Cash On Delivery
193
+ </button>
194
+ </div>
195
+ );
196
+ }
197
+ ```
198
+
199
+ ---
200
+
201
+ ## 🛡️ 5. High-Availability Fallback Checkout
116
202
 
117
203
  If your primary payment gateway experiences bank server outages or 500 errors, automatic fallback routes the order through the next available gateway:
118
204
 
@@ -132,53 +218,24 @@ const order = await payments.createOrderWithFallback({
132
218
 
133
219
  ---
134
220
 
135
- ### 4. Verify Payment & Webhooks
221
+ ## 🧰 Explicit TypeScript Type Exports
136
222
 
137
223
  ```typescript
138
- // 1. Verify frontend checkout completion:
139
- const verification = await payments.verifyPayment({
140
- gateway: 'razorpay',
141
- orderId: 'order_123',
142
- paymentId: 'pay_456',
143
- signature: req.body.razorpay_signature,
144
- });
145
-
146
- if (verification.isSuccessful) {
147
- console.log(`Payment confirmed! ID: ${verification.paymentId}`);
148
- }
149
-
150
- // 2. Universal Webhook Authenticator:
151
- const webhook = await payments.verifyWebhook({
152
- gateway: 'cashfree', // or 'razorpay' | 'phonepe' | 'stripe'
153
- rawBody: req.rawBody,
154
- headers: req.headers,
155
- });
156
-
157
- if (webhook.isValid) {
158
- console.log(`Verified webhook event: ${webhook.event}`, webhook.data);
159
- }
160
- ```
161
-
162
- ---
163
-
164
- ### 5. Instant Refunds
165
-
166
- ```typescript
167
- const refund = await payments.refund({
168
- gateway: 'razorpay',
169
- paymentId: 'pay_456',
170
- amount: 1499.00, // full or partial
171
- reason: 'Customer cancelled before dispatch',
172
- });
173
-
174
- console.log('Refund ID:', refund.refundId, 'Status:', refund.status);
224
+ import type {
225
+ PaymentOrderResult,
226
+ VerificationResult,
227
+ RefundResult,
228
+ WebhookResult,
229
+ SupportedGateway,
230
+ NormalizedWebhookEvent,
231
+ CreateOrderOptions,
232
+ BoostPaymentOpenOptions,
233
+ } from '@boostengine/payments';
175
234
  ```
176
235
 
177
236
  ---
178
237
 
179
- ## 🛠️ CLI Quickstart
180
-
181
- Generate an environment template or check supported gateways:
238
+ ## 🛠️ CLI Tool
182
239
 
183
240
  ```bash
184
241
  # List all 6 supported gateways
package/dist/index.cjs CHANGED
@@ -178,6 +178,7 @@ var RazorpayAdapter = class extends BasePaymentAdapter {
178
178
  paymentId: payment.id,
179
179
  orderId: options.orderId,
180
180
  amount: payment.amount / 100,
181
+ // Normalized back to standard currency units
181
182
  currency: payment.currency,
182
183
  paymentMethod: payment.method,
183
184
  rawResponse: payment
@@ -202,6 +203,7 @@ var RazorpayAdapter = class extends BasePaymentAdapter {
202
203
  refundId: res.id,
203
204
  paymentId: options.paymentId,
204
205
  amount: res.amount / 100,
206
+ // Normalized to standard currency units
205
207
  status: res.status === "processed" ? "SUCCESS" : "PENDING",
206
208
  rawResponse: res
207
209
  };
@@ -211,32 +213,58 @@ var RazorpayAdapter = class extends BasePaymentAdapter {
211
213
  if (!secret) {
212
214
  return {
213
215
  isValid: false,
216
+ normalizedEvent: "UNKNOWN",
214
217
  gateway: "razorpay",
215
218
  error: "Razorpay webhookSecret is not configured."
216
219
  };
217
220
  }
218
221
  const signature = this.getHeader(options.headers, "x-razorpay-signature");
219
- if (!signature || typeof signature !== "string") {
222
+ if (!signature) {
220
223
  return {
221
224
  isValid: false,
225
+ normalizedEvent: "UNKNOWN",
222
226
  gateway: "razorpay",
223
- error: "Missing X-Razorpay-Signature header."
227
+ error: "Missing x-razorpay-signature header."
224
228
  };
225
229
  }
226
230
  const rawString = typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8");
227
231
  const expectedSignature = hmacSha256(rawString, secret);
228
232
  const isValid = safeCompare(expectedSignature, signature);
229
- let parsedData;
233
+ let parsed;
230
234
  try {
231
- parsedData = JSON.parse(rawString);
235
+ parsed = JSON.parse(rawString);
232
236
  } catch {
233
- parsedData = null;
237
+ parsed = null;
234
238
  }
239
+ const rawEvent = parsed?.event || "";
240
+ let normalizedEvent = "UNKNOWN";
241
+ if (rawEvent === "order.paid" || rawEvent === "payment.captured" || rawEvent === "payment.authorized") {
242
+ normalizedEvent = "PAYMENT_SUCCESS";
243
+ } else if (rawEvent === "payment.failed") {
244
+ normalizedEvent = "PAYMENT_FAILED";
245
+ } else if (rawEvent === "refund.processed" || rawEvent === "refund.created") {
246
+ normalizedEvent = "REFUND_PROCESSED";
247
+ } else if (rawEvent === "refund.failed") {
248
+ normalizedEvent = "REFUND_FAILED";
249
+ } else if (rawEvent?.includes("dispute")) {
250
+ normalizedEvent = "DISPUTE_CREATED";
251
+ }
252
+ const paymentEntity = parsed?.payload?.payment?.entity;
253
+ const orderEntity = parsed?.payload?.order?.entity;
254
+ const orderId = orderEntity?.receipt || paymentEntity?.order_id || orderEntity?.id;
255
+ const paymentId = paymentEntity?.id;
256
+ const amount = paymentEntity?.amount ? paymentEntity.amount / 100 : orderEntity?.amount ? orderEntity.amount / 100 : void 0;
257
+ const currency = paymentEntity?.currency || orderEntity?.currency;
235
258
  return {
236
259
  isValid,
260
+ normalizedEvent,
261
+ rawEvent,
237
262
  gateway: "razorpay",
238
- event: parsedData?.event,
239
- data: parsedData?.payload
263
+ orderId,
264
+ paymentId,
265
+ amount,
266
+ currency,
267
+ data: parsed?.payload
240
268
  };
241
269
  }
242
270
  };
@@ -369,17 +397,41 @@ var CashfreeAdapter = class extends BasePaymentAdapter {
369
397
  ).toString("base64");
370
398
  isValid = safeCompare(expectedSignature, signature);
371
399
  }
372
- let parsedData;
400
+ let parsed;
373
401
  try {
374
- parsedData = JSON.parse(rawString);
402
+ parsed = JSON.parse(rawString);
375
403
  } catch {
376
- parsedData = null;
404
+ parsed = null;
405
+ }
406
+ const rawEvent = parsed?.type || "";
407
+ let normalizedEvent = "UNKNOWN";
408
+ if (rawEvent === "PAYMENT_SUCCESS_WEBHOOK") {
409
+ normalizedEvent = "PAYMENT_SUCCESS";
410
+ } else if (rawEvent === "PAYMENT_FAILED_WEBHOOK" || rawEvent === "USER_DROPPED_WEBHOOK") {
411
+ normalizedEvent = "PAYMENT_FAILED";
412
+ } else if (rawEvent === "REFUND_STATUS_WEBHOOK") {
413
+ const refundStatus = parsed?.data?.refund?.refund_status;
414
+ normalizedEvent = refundStatus === "SUCCESS" ? "REFUND_PROCESSED" : "REFUND_FAILED";
415
+ } else if (rawEvent?.includes("DISPUTE")) {
416
+ normalizedEvent = "DISPUTE_CREATED";
377
417
  }
418
+ const orderData = parsed?.data?.order;
419
+ const paymentData = parsed?.data?.payment;
420
+ const refundData = parsed?.data?.refund;
421
+ const orderId = orderData?.order_id || refundData?.order_id;
422
+ const paymentId = paymentData ? String(paymentData.cf_payment_id) : refundData ? String(refundData.cf_payment_id) : void 0;
423
+ const amount = paymentData?.payment_amount ?? orderData?.order_amount ?? refundData?.refund_amount;
424
+ const currency = paymentData?.payment_currency || orderData?.order_currency;
378
425
  return {
379
426
  isValid,
427
+ normalizedEvent,
428
+ rawEvent,
380
429
  gateway: "cashfree",
381
- event: parsedData?.type,
382
- data: parsedData?.data
430
+ orderId,
431
+ paymentId,
432
+ amount,
433
+ currency,
434
+ data: parsed?.data
383
435
  };
384
436
  }
385
437
  };
@@ -505,17 +557,36 @@ var PhonePeAdapter = class extends BasePaymentAdapter {
505
557
  try {
506
558
  parsed = JSON.parse(rawString);
507
559
  } catch {
508
- return { isValid: false, gateway: "phonepe", error: "Invalid JSON payload" };
560
+ return { isValid: false, normalizedEvent: "UNKNOWN", gateway: "phonepe", error: "Invalid JSON payload" };
509
561
  }
510
562
  if (!parsed.response) {
511
- return { isValid: false, gateway: "phonepe", error: "Missing response field in PhonePe callback" };
563
+ return { isValid: false, normalizedEvent: "UNKNOWN", gateway: "phonepe", error: "Missing response field in PhonePe callback" };
564
+ }
565
+ let decoded = {};
566
+ try {
567
+ decoded = JSON.parse(base64Decode(parsed.response));
568
+ } catch {
569
+ return { isValid: false, normalizedEvent: "UNKNOWN", gateway: "phonepe", error: "Failed to decode base64 PhonePe response" };
512
570
  }
513
- const decoded = JSON.parse(base64Decode(parsed.response));
514
- decoded.code === "PAYMENT_SUCCESS";
571
+ const rawEvent = decoded.code || "";
572
+ let normalizedEvent = "UNKNOWN";
573
+ if (rawEvent === "PAYMENT_SUCCESS") {
574
+ normalizedEvent = "PAYMENT_SUCCESS";
575
+ } else if (rawEvent === "PAYMENT_ERROR" || rawEvent === "PAYMENT_DECLINED") {
576
+ normalizedEvent = "PAYMENT_FAILED";
577
+ }
578
+ const orderId = decoded.data?.merchantTransactionId;
579
+ const paymentId = decoded.data?.transactionId;
580
+ const amount = decoded.data?.amount ? decoded.data.amount / 100 : void 0;
515
581
  return {
516
582
  isValid: true,
583
+ normalizedEvent,
584
+ rawEvent,
517
585
  gateway: "phonepe",
518
- event: decoded.code,
586
+ orderId,
587
+ paymentId,
588
+ amount,
589
+ currency: "INR",
519
590
  data: decoded.data
520
591
  };
521
592
  }
@@ -630,11 +701,21 @@ var PaytmAdapter = class extends BasePaymentAdapter {
630
701
  const params = new URLSearchParams(rawString);
631
702
  parsed = Object.fromEntries(params.entries());
632
703
  }
633
- const isSuccess = parsed?.STATUS === "TXN_SUCCESS" || parsed?.resultInfo?.resultStatus === "TXN_SUCCESS";
704
+ const rawStatus = parsed?.STATUS || parsed?.resultInfo?.resultStatus;
705
+ const isSuccess = rawStatus === "TXN_SUCCESS";
706
+ const normalizedEvent = isSuccess ? "PAYMENT_SUCCESS" : "PAYMENT_FAILED";
707
+ const orderId = parsed?.ORDERID || parsed?.orderId;
708
+ const paymentId = parsed?.TXNID || parsed?.txnId;
709
+ const amount = parsed?.TXNAMOUNT ? parseFloat(parsed.TXNAMOUNT) : void 0;
634
710
  return {
635
711
  isValid: true,
712
+ normalizedEvent,
713
+ rawEvent: rawStatus,
636
714
  gateway: "paytm",
637
- event: isSuccess ? "TXN_SUCCESS" : "TXN_FAILURE",
715
+ orderId,
716
+ paymentId,
717
+ amount,
718
+ currency: "INR",
638
719
  data: parsed
639
720
  };
640
721
  }
@@ -726,6 +807,7 @@ var StripeAdapter = class extends BasePaymentAdapter {
726
807
  refundId: res.id,
727
808
  paymentId: options.paymentId,
728
809
  amount: res.amount / 100,
810
+ // Normalized to standard currency units
729
811
  status: res.status === "succeeded" ? "SUCCESS" : "PENDING",
730
812
  rawResponse: res
731
813
  };
@@ -733,11 +815,11 @@ var StripeAdapter = class extends BasePaymentAdapter {
733
815
  async verifyWebhook(options) {
734
816
  const secret = options.webhookSecret || this.config.webhookSecret;
735
817
  if (!secret) {
736
- return { isValid: false, gateway: "stripe", error: "Stripe webhookSecret is not configured." };
818
+ return { isValid: false, normalizedEvent: "UNKNOWN", gateway: "stripe", error: "Stripe webhookSecret is not configured." };
737
819
  }
738
820
  const sigHeader = this.getHeader(options.headers, "stripe-signature");
739
821
  if (!sigHeader || typeof sigHeader !== "string") {
740
- return { isValid: false, gateway: "stripe", error: "Missing stripe-signature header." };
822
+ return { isValid: false, normalizedEvent: "UNKNOWN", gateway: "stripe", error: "Missing stripe-signature header." };
741
823
  }
742
824
  const parts = sigHeader.split(",");
743
825
  let timestamp = "";
@@ -757,11 +839,32 @@ var StripeAdapter = class extends BasePaymentAdapter {
757
839
  } catch {
758
840
  parsed = null;
759
841
  }
842
+ const rawEvent = parsed?.type || "";
843
+ let normalizedEvent = "UNKNOWN";
844
+ if (rawEvent === "checkout.session.completed" || rawEvent === "payment_intent.succeeded") {
845
+ normalizedEvent = "PAYMENT_SUCCESS";
846
+ } else if (rawEvent === "payment_intent.payment_failed") {
847
+ normalizedEvent = "PAYMENT_FAILED";
848
+ } else if (rawEvent === "charge.refunded") {
849
+ normalizedEvent = "REFUND_PROCESSED";
850
+ } else if (rawEvent?.includes("dispute")) {
851
+ normalizedEvent = "DISPUTE_CREATED";
852
+ }
853
+ const obj = parsed?.data?.object;
854
+ const orderId = obj?.client_reference_id || obj?.metadata?.order_id || obj?.id;
855
+ const paymentId = obj?.payment_intent || obj?.id;
856
+ const amount = obj?.amount_total ? obj.amount_total / 100 : obj?.amount ? obj.amount / 100 : void 0;
857
+ const currency = obj?.currency ? obj.currency.toUpperCase() : void 0;
760
858
  return {
761
859
  isValid,
860
+ normalizedEvent,
861
+ rawEvent,
762
862
  gateway: "stripe",
763
- event: parsed?.type,
764
- data: parsed?.data?.object
863
+ orderId,
864
+ paymentId,
865
+ amount,
866
+ currency,
867
+ data: obj
765
868
  };
766
869
  }
767
870
  };
@@ -830,8 +933,9 @@ var CODAdapter = class extends BasePaymentAdapter {
830
933
  async verifyWebhook(_options) {
831
934
  return {
832
935
  isValid: true,
936
+ normalizedEvent: "PAYMENT_SUCCESS",
937
+ rawEvent: "COD_ORDER",
833
938
  gateway: "cod",
834
- event: "COD_ORDER",
835
939
  data: {}
836
940
  };
837
941
  }
@@ -962,6 +1066,60 @@ var PaymentManager = class {
962
1066
  const adapter = this.getAdapter(options.gateway);
963
1067
  return adapter.verifyWebhook(options);
964
1068
  }
1069
+ /**
1070
+ * Ready-made Next.js 13/14/15 App Router Route Handler Webhook Authenticator.
1071
+ * Directly consumes the standard web Request object with raw stream body handling:
1072
+ *
1073
+ * ```typescript
1074
+ * export async function POST(req: Request) {
1075
+ * const result = await payments.verifyNextJsWebhook(req, { gateway: 'razorpay' });
1076
+ * if (!result.isValid) return new Response('Invalid Signature', { status: 400 });
1077
+ * console.log('Event:', result.normalizedEvent, result.orderId);
1078
+ * return new Response('OK');
1079
+ * }
1080
+ * ```
1081
+ */
1082
+ async verifyNextJsWebhook(request, options) {
1083
+ try {
1084
+ let rawBody = "";
1085
+ if (typeof request.text === "function") {
1086
+ rawBody = await request.text();
1087
+ } else if (typeof request.body === "string") {
1088
+ rawBody = request.body;
1089
+ } else if (Buffer.isBuffer(request.body)) {
1090
+ rawBody = request.body.toString("utf8");
1091
+ }
1092
+ const headers = {};
1093
+ if (request.headers) {
1094
+ if (typeof request.headers.forEach === "function") {
1095
+ request.headers.forEach((val, key) => {
1096
+ headers[key.toLowerCase()] = val;
1097
+ });
1098
+ } else if (typeof request.headers.entries === "function") {
1099
+ for (const [key, val] of request.headers.entries()) {
1100
+ headers[key.toLowerCase()] = val;
1101
+ }
1102
+ } else if (typeof request.headers === "object") {
1103
+ Object.entries(request.headers).forEach(([k, v]) => {
1104
+ headers[k.toLowerCase()] = Array.isArray(v) ? v[0] : v;
1105
+ });
1106
+ }
1107
+ }
1108
+ return this.verifyWebhook({
1109
+ gateway: options.gateway,
1110
+ rawBody,
1111
+ headers,
1112
+ webhookSecret: options.webhookSecret
1113
+ });
1114
+ } catch (err) {
1115
+ return {
1116
+ isValid: false,
1117
+ normalizedEvent: "UNKNOWN",
1118
+ gateway: options.gateway,
1119
+ error: `Failed to process Next.js webhook: ${err.message}`
1120
+ };
1121
+ }
1122
+ }
965
1123
  };
966
1124
  function createPaymentManager(options) {
967
1125
  return new PaymentManager(options);