@boostengine/payments 1.0.0 → 1.1.1
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 +175 -103
- 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 +29 -3
package/README.md
CHANGED
|
@@ -1,39 +1,91 @@
|
|
|
1
|
-
# @boostengine/payments
|
|
1
|
+
# @boostengine/payments 💳
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@boostengine/payments)
|
|
4
|
+
[](https://www.npmjs.com/package/@boostengine/payments)
|
|
5
|
+
[](https://github.com/boostengine/boostengine/blob/main/LICENSE)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://npmjs.com/package/@boostengine/payments)
|
|
8
|
+
|
|
9
|
+
> **Unified multi-gateway payment orchestration for Indian and global eCommerce. Connect Razorpay, Cashfree, PhonePe, Paytm, Stripe, and Cash on Delivery (COD) with a single, unified API. Features automatic gateway failover, smart currency routing, Next.js webhook normalizers, and React checkout hooks.**
|
|
10
|
+
|
|
11
|
+
Zero dependency bloat — uses native Node.js `crypto` and `fetch`. No need to install 6 separate heavy vendor SDKs.
|
|
4
12
|
|
|
5
13
|
---
|
|
6
14
|
|
|
7
|
-
##
|
|
15
|
+
## 📸 Architecture & Payment Flow
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Customer Clicks "Pay Now" (₹1,499)
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
22
|
+
│ Smart Payment Orchestrator │
|
|
23
|
+
├─────────────────────────────────────────────────────────────┤
|
|
24
|
+
│ Currency Routing: │
|
|
25
|
+
│ - If INR ──► Route to Cashfree / Razorpay / PhonePe │
|
|
26
|
+
│ - If USD ──► Route to Stripe │
|
|
27
|
+
│ - If COD ──► Verify min/max limits & add handling fee │
|
|
28
|
+
│ │
|
|
29
|
+
│ High-Availability Failover: │
|
|
30
|
+
│ [Cashfree Server 500] ──► Auto-retry with [Razorpay] ──► OK│
|
|
31
|
+
└──────────────────────────────┬──────────────────────────────┘
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ Client Checkout Hook (`useBoostPayment`) │
|
|
36
|
+
├─────────────────────────────────────────────────────────────┤
|
|
37
|
+
│ Opens Razorpay Popup / Cashfree Dropin / PhonePe App / UPI │
|
|
38
|
+
└──────────────────────────────┬──────────────────────────────┘
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ Next.js Webhook Handler (2 Lines) │
|
|
43
|
+
├─────────────────────────────────────────────────────────────┤
|
|
44
|
+
│ Validates HMAC-SHA256 signature and returns normalized event:│
|
|
45
|
+
│ 'PAYMENT_SUCCESS' | 'PAYMENT_FAILED' | 'REFUND_PROCESSED' │
|
|
46
|
+
└─────────────────────────────────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
8
50
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
51
|
+
## 🌟 Gateway Comparison
|
|
52
|
+
|
|
53
|
+
| Gateway | Supported Currencies | Methods Supported | Best For |
|
|
54
|
+
| :--- | :--- | :--- | :--- |
|
|
55
|
+
| **Razorpay** | INR, USD, EUR + 90 more | UPI, Cards, NetBanking, Wallets, EMI | Reliable Indian D2C checkout |
|
|
56
|
+
| **Cashfree** | INR | Instant UPI QR, Intent, NetBanking, Cards | Low transaction fees & high UPI success |
|
|
57
|
+
| **PhonePe** | INR | PhonePe App, UPI Intent, QR Code | Fast mobile conversions in India |
|
|
58
|
+
| **Stripe** | Global (135+ currencies) | Credit/Debit Cards, Apple Pay, Google Pay | International cross-border sales |
|
|
59
|
+
| **Paytm** | INR | Paytm Wallet, UPI, Postpaid, NetBanking | Indian mobile users |
|
|
60
|
+
| **Cash on Delivery** | Any | Pay in cash upon delivery | Tier 2/3 Indian cities |
|
|
14
61
|
|
|
15
62
|
---
|
|
16
63
|
|
|
17
64
|
## 📦 Installation
|
|
18
65
|
|
|
19
66
|
```bash
|
|
67
|
+
# npm
|
|
20
68
|
npm install @boostengine/payments
|
|
69
|
+
|
|
70
|
+
# pnpm
|
|
71
|
+
pnpm add @boostengine/payments
|
|
72
|
+
|
|
73
|
+
# yarn
|
|
74
|
+
yarn add @boostengine/payments
|
|
21
75
|
```
|
|
22
76
|
|
|
23
77
|
---
|
|
24
78
|
|
|
25
|
-
## 🚀
|
|
79
|
+
## 🚀 Step-by-Step Integration Guide
|
|
26
80
|
|
|
27
|
-
### 1
|
|
81
|
+
### Step 1: Initialize Payment Manager (`lib/payments.ts`)
|
|
28
82
|
|
|
29
83
|
```typescript
|
|
30
84
|
import { createPaymentManager } from '@boostengine/payments';
|
|
31
85
|
|
|
32
86
|
export const payments = createPaymentManager({
|
|
33
|
-
// Default gateway when none is specified
|
|
34
87
|
defaultGateway: 'cashfree',
|
|
35
88
|
|
|
36
|
-
// Configure any or all gateways
|
|
37
89
|
gateways: {
|
|
38
90
|
cashfree: {
|
|
39
91
|
appId: process.env.CASHFREE_APP_ID!,
|
|
@@ -58,11 +110,11 @@ export const payments = createPaymentManager({
|
|
|
58
110
|
cod: {
|
|
59
111
|
minOrderValue: 200,
|
|
60
112
|
maxOrderValue: 10000,
|
|
61
|
-
extraFee: 49, //
|
|
113
|
+
extraFee: 49, // ₹49 COD handling charge
|
|
62
114
|
},
|
|
63
115
|
},
|
|
64
116
|
|
|
65
|
-
// Smart Routing &
|
|
117
|
+
// Smart Routing & High Availability
|
|
66
118
|
smartRouting: {
|
|
67
119
|
currencyMap: {
|
|
68
120
|
USD: 'stripe',
|
|
@@ -76,122 +128,142 @@ export const payments = createPaymentManager({
|
|
|
76
128
|
|
|
77
129
|
---
|
|
78
130
|
|
|
79
|
-
### 2
|
|
131
|
+
### Step 2: Create Order API Route (`app/api/checkout/route.ts`)
|
|
132
|
+
|
|
133
|
+
Pass standard human amounts (e.g. `1499.00`). Paise and cents conversion is handled automatically!
|
|
80
134
|
|
|
81
135
|
```typescript
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
136
|
+
import { payments } from '@/lib/payments';
|
|
137
|
+
|
|
138
|
+
export async function POST(req: Request) {
|
|
139
|
+
const { amount, customer, gateway } = await req.json();
|
|
140
|
+
|
|
141
|
+
// Create order with automatic fallback if primary gateway is down
|
|
142
|
+
const order = await payments.createOrderWithFallback({
|
|
143
|
+
amount: amount, // e.g. 1499
|
|
144
|
+
currency: 'INR',
|
|
145
|
+
receipt: `order_${Date.now()}`,
|
|
146
|
+
customer: {
|
|
147
|
+
name: customer.name,
|
|
148
|
+
email: customer.email,
|
|
149
|
+
phone: customer.phone,
|
|
150
|
+
},
|
|
151
|
+
fallbackChain: gateway ? [gateway, 'razorpay'] : ['cashfree', 'razorpay'],
|
|
152
|
+
});
|
|
95
153
|
|
|
96
|
-
|
|
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: { ... }
|
|
154
|
+
return Response.json(order);
|
|
109
155
|
}
|
|
110
|
-
*/
|
|
111
156
|
```
|
|
112
157
|
|
|
113
158
|
---
|
|
114
159
|
|
|
115
|
-
### 3
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
### Step 3: Frontend Checkout Button (`components/CheckoutButton.tsx`)
|
|
161
|
+
|
|
162
|
+
The `useBoostPayment` hook automatically injects the right vendor scripts on demand and displays payment modals:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
'use client';
|
|
166
|
+
|
|
167
|
+
import { useBoostPayment } from '@boostengine/payments/react';
|
|
168
|
+
import { useRouter } from 'next/navigation';
|
|
169
|
+
|
|
170
|
+
export default function CheckoutButton({ cartTotal, customer }: any) {
|
|
171
|
+
const router = useRouter();
|
|
172
|
+
const { openPaymentModal, isProcessing } = useBoostPayment();
|
|
173
|
+
|
|
174
|
+
const handlePay = async (gateway: 'razorpay' | 'cashfree' | 'phonepe' | 'cod') => {
|
|
175
|
+
// 1. Create order on server
|
|
176
|
+
const res = await fetch('/api/checkout', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: JSON.stringify({ amount: cartTotal, customer, gateway }),
|
|
179
|
+
});
|
|
180
|
+
const order = await res.json();
|
|
181
|
+
|
|
182
|
+
// 2. Open popup modal or redirect
|
|
183
|
+
openPaymentModal({
|
|
184
|
+
order,
|
|
185
|
+
name: 'BoostStore',
|
|
186
|
+
description: 'Order Payment',
|
|
187
|
+
themeColor: '#4f46e5',
|
|
188
|
+
onSuccess: async (result) => {
|
|
189
|
+
router.push(`/order-success?id=${result.orderId}`);
|
|
190
|
+
},
|
|
191
|
+
onFailure: (err) => {
|
|
192
|
+
alert(`Payment failed: ${err.message}`);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div style={{ display: 'flex', gap: '10px' }}>
|
|
199
|
+
<button disabled={isProcessing} onClick={() => handlePay('cashfree')}>
|
|
200
|
+
Pay Online (UPI / Card)
|
|
201
|
+
</button>
|
|
202
|
+
<button disabled={isProcessing} onClick={() => handlePay('cod')}>
|
|
203
|
+
Cash on Delivery
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
159
207
|
}
|
|
160
208
|
```
|
|
161
209
|
|
|
162
210
|
---
|
|
163
211
|
|
|
164
|
-
###
|
|
212
|
+
### Step 4: Next.js Universal Webhook Handler (`app/api/webhooks/[gateway]/route.ts`)
|
|
165
213
|
|
|
166
|
-
|
|
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
|
-
});
|
|
214
|
+
Verify webhook signatures in 2 lines with standard normalized events:
|
|
173
215
|
|
|
174
|
-
|
|
216
|
+
```typescript
|
|
217
|
+
import { payments } from '@/lib/payments';
|
|
218
|
+
|
|
219
|
+
export async function POST(
|
|
220
|
+
req: Request,
|
|
221
|
+
{ params }: { params: { gateway: string } }
|
|
222
|
+
) {
|
|
223
|
+
const result = await payments.verifyNextJsWebhook(req, {
|
|
224
|
+
gateway: params.gateway as any,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!result.isValid) {
|
|
228
|
+
return new Response('Invalid Signature', { status: 400 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle standardized events across all payment gateways!
|
|
232
|
+
switch (result.normalizedEvent) {
|
|
233
|
+
case 'PAYMENT_SUCCESS':
|
|
234
|
+
console.log(`✅ Order ${result.orderId} paid! Amount: ₹${result.amount}`);
|
|
235
|
+
// TODO: Update Order status to 'PAID' in database
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case 'PAYMENT_FAILED':
|
|
239
|
+
console.log(`❌ Order ${result.orderId} failed.`);
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case 'REFUND_PROCESSED':
|
|
243
|
+
console.log(`🔄 Refund completed for ${result.paymentId}`);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return new Response('OK');
|
|
248
|
+
}
|
|
175
249
|
```
|
|
176
250
|
|
|
177
251
|
---
|
|
178
252
|
|
|
179
|
-
## 🛠️ CLI
|
|
253
|
+
## 🛠️ CLI Utilities
|
|
180
254
|
|
|
181
|
-
|
|
255
|
+
Test and configure payment credentials quickly:
|
|
182
256
|
|
|
183
257
|
```bash
|
|
184
|
-
# List
|
|
258
|
+
# List supported gateways and their status
|
|
185
259
|
npx @boostengine/payments list
|
|
186
260
|
|
|
187
|
-
# Generate .env.payments
|
|
261
|
+
# Generate .env.payments template
|
|
188
262
|
npx @boostengine/payments init-env
|
|
189
|
-
|
|
190
|
-
# Compute quick SHA-256 hash
|
|
191
|
-
npx @boostengine/payments hash "my_test_payload"
|
|
192
263
|
```
|
|
193
264
|
|
|
194
265
|
---
|
|
195
266
|
|
|
196
267
|
## 📄 License
|
|
197
|
-
|
|
268
|
+
|
|
269
|
+
MIT © [Boost Engine](https://github.com/boostengine)
|
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);
|