@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 +139 -82
- package/dist/index.cjs +182 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +20 -148
- package/dist/index.d.ts +20 -148
- package/dist/index.mjs +182 -24
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +173 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.mts +29 -0
- package/dist/react.d.ts +29 -0
- package/dist/react.mjs +167 -0
- package/dist/react.mjs.map +1 -0
- package/dist/types-D3bPaYhQ.d.mts +194 -0
- package/dist/types-D3bPaYhQ.d.ts +194 -0
- package/package.json +20 -3
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
|
|
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
|
-
## ⚡
|
|
7
|
+
## ⚡ Key Highlights
|
|
8
8
|
|
|
9
|
-
- 🔌 **
|
|
10
|
-
- 🛡️ **
|
|
11
|
-
- 💱 **
|
|
12
|
-
-
|
|
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
|
-
## 🚀
|
|
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 &
|
|
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
|
-
|
|
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
|
-
//
|
|
83
|
-
|
|
84
|
-
amount
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
+
## 🧰 Explicit TypeScript Type Exports
|
|
136
222
|
|
|
137
223
|
```typescript
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
|
222
|
+
if (!signature) {
|
|
220
223
|
return {
|
|
221
224
|
isValid: false,
|
|
225
|
+
normalizedEvent: "UNKNOWN",
|
|
222
226
|
gateway: "razorpay",
|
|
223
|
-
error: "Missing
|
|
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
|
|
233
|
+
let parsed;
|
|
230
234
|
try {
|
|
231
|
-
|
|
235
|
+
parsed = JSON.parse(rawString);
|
|
232
236
|
} catch {
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
400
|
+
let parsed;
|
|
373
401
|
try {
|
|
374
|
-
|
|
402
|
+
parsed = JSON.parse(rawString);
|
|
375
403
|
} catch {
|
|
376
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
|
514
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
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);
|