paybridge 0.4.0 → 0.5.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 +56 -1
- package/dist/index.js +33 -0
- package/dist/providers/adyen.d.ts +46 -0
- package/dist/providers/adyen.js +289 -0
- package/dist/providers/mercadopago.d.ts +36 -0
- package/dist/providers/mercadopago.js +297 -0
- package/dist/providers/razorpay.d.ts +37 -0
- package/dist/providers/razorpay.js +328 -0
- package/dist/types.d.ts +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -194,6 +194,9 @@ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
|
194
194
|
| **Stripe** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
195
195
|
| **Peach Payments** | ✅ | ⛔ | ✅ | ✅ | **Production** |
|
|
196
196
|
| **Flutterwave** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
197
|
+
| **Adyen** | ✅ | ⛔ | ✅ | ✅ | **Production** |
|
|
198
|
+
| **Mercado Pago** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
199
|
+
| **Razorpay** | ✅ | ✅ | ✅ | ✅ | **Production** |
|
|
197
200
|
|
|
198
201
|
### Crypto on/off-ramp providers
|
|
199
202
|
|
|
@@ -207,7 +210,7 @@ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
|
207
210
|
**Notes:**
|
|
208
211
|
- `⛔` marks features the underlying provider's API doesn't support — those methods throw a clear error explaining the limitation. Use a different provider for that capability or use `PayBridgeRouter` to route accordingly.
|
|
209
212
|
- **Yellow Card** is gated behind `@experimental` until partner API documentation is verified — it logs a warning on instantiation. Do not use in production without partner-confirmed spec.
|
|
210
|
-
- **Sandbox testing.** PayFast / PayStack / Stripe / Peach / Flutterwave are wired and unit-tested, but have not yet been validated against live sandbox credentials. To validate against real sandboxes, set the relevant `*_API_KEY` env vars and run `npm run test:e2e:sandbox`.
|
|
213
|
+
- **Sandbox testing.** PayFast / PayStack / Stripe / Peach / Flutterwave / Adyen / Mercado Pago / Razorpay are wired and unit-tested, but have not yet been validated against live sandbox credentials. To validate against real sandboxes, set the relevant `*_API_KEY` env vars and run `npm run test:e2e:sandbox`.
|
|
211
214
|
|
|
212
215
|
## Provider Configuration
|
|
213
216
|
|
|
@@ -258,6 +261,58 @@ const pay = new PayBridge({
|
|
|
258
261
|
|
|
259
262
|
**Docs:** [Ozow Hub](https://hub.ozow.com)
|
|
260
263
|
|
|
264
|
+
### Adyen
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const pay = new PayBridge({
|
|
268
|
+
provider: 'adyen',
|
|
269
|
+
credentials: {
|
|
270
|
+
apiKey: 'your_api_key',
|
|
271
|
+
merchantAccount: 'YourMerchantAccount',
|
|
272
|
+
liveUrlPrefix: 'abc123' // Only for live mode
|
|
273
|
+
},
|
|
274
|
+
sandbox: true,
|
|
275
|
+
webhookSecret: 'your_hmac_key_hex'
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Docs:** [Adyen API Explorer](https://docs.adyen.com/api-explorer/)
|
|
280
|
+
|
|
281
|
+
**Note:** Adyen subscriptions require recurring tokenization flow (not yet supported). Use Stripe or PayFast for subscriptions.
|
|
282
|
+
|
|
283
|
+
### Mercado Pago
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const pay = new PayBridge({
|
|
287
|
+
provider: 'mercadopago',
|
|
288
|
+
credentials: {
|
|
289
|
+
apiKey: 'TEST-...' // Or APP_USR-... for live
|
|
290
|
+
},
|
|
291
|
+
sandbox: true,
|
|
292
|
+
webhookSecret: 'your_webhook_secret'
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Docs:** [Mercado Pago Developers](https://www.mercadopago.com/developers/en/reference)
|
|
297
|
+
|
|
298
|
+
### Razorpay
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const pay = new PayBridge({
|
|
302
|
+
provider: 'razorpay',
|
|
303
|
+
credentials: {
|
|
304
|
+
apiKey: 'rzp_test_...', // key_id
|
|
305
|
+
secretKey: 'your_key_secret'
|
|
306
|
+
},
|
|
307
|
+
sandbox: true,
|
|
308
|
+
webhookSecret: 'your_webhook_secret'
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Docs:** [Razorpay API Reference](https://razorpay.com/docs/api)
|
|
313
|
+
|
|
314
|
+
**Note:** Razorpay webhooks do not include timestamp-based replay protection.
|
|
315
|
+
|
|
261
316
|
## Switch Providers in 1 Line
|
|
262
317
|
|
|
263
318
|
```typescript
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,9 @@ const stripe_1 = require("./providers/stripe");
|
|
|
29
29
|
const payfast_1 = require("./providers/payfast");
|
|
30
30
|
const paystack_1 = require("./providers/paystack");
|
|
31
31
|
const flutterwave_1 = require("./providers/flutterwave");
|
|
32
|
+
const adyen_1 = require("./providers/adyen");
|
|
33
|
+
const mercadopago_1 = require("./providers/mercadopago");
|
|
34
|
+
const razorpay_1 = require("./providers/razorpay");
|
|
32
35
|
__exportStar(require("./types"), exports);
|
|
33
36
|
__exportStar(require("./utils/currency"), exports);
|
|
34
37
|
__exportStar(require("./utils/fetch"), exports);
|
|
@@ -135,6 +138,36 @@ class PayBridge {
|
|
|
135
138
|
sandbox,
|
|
136
139
|
webhookSecret,
|
|
137
140
|
});
|
|
141
|
+
case 'adyen':
|
|
142
|
+
if (!credentials.apiKey || !credentials.merchantAccount) {
|
|
143
|
+
throw new Error('Adyen requires apiKey and merchantAccount');
|
|
144
|
+
}
|
|
145
|
+
return new adyen_1.AdyenProvider({
|
|
146
|
+
apiKey: credentials.apiKey,
|
|
147
|
+
merchantAccount: credentials.merchantAccount,
|
|
148
|
+
liveUrlPrefix: credentials.liveUrlPrefix,
|
|
149
|
+
sandbox,
|
|
150
|
+
webhookSecret,
|
|
151
|
+
});
|
|
152
|
+
case 'mercadopago':
|
|
153
|
+
if (!credentials.apiKey) {
|
|
154
|
+
throw new Error('Mercado Pago requires apiKey (access token TEST-* or APP_USR-*)');
|
|
155
|
+
}
|
|
156
|
+
return new mercadopago_1.MercadoPagoProvider({
|
|
157
|
+
accessToken: credentials.apiKey,
|
|
158
|
+
sandbox,
|
|
159
|
+
webhookSecret,
|
|
160
|
+
});
|
|
161
|
+
case 'razorpay':
|
|
162
|
+
if (!credentials.apiKey || !credentials.secretKey) {
|
|
163
|
+
throw new Error('Razorpay requires apiKey (key_id rzp_test_* or rzp_live_*) and secretKey (key_secret)');
|
|
164
|
+
}
|
|
165
|
+
return new razorpay_1.RazorpayProvider({
|
|
166
|
+
keyId: credentials.apiKey,
|
|
167
|
+
keySecret: credentials.secretKey,
|
|
168
|
+
sandbox,
|
|
169
|
+
webhookSecret,
|
|
170
|
+
});
|
|
138
171
|
default:
|
|
139
172
|
throw new Error(`Unknown provider: ${provider}`);
|
|
140
173
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adyen payment provider
|
|
3
|
+
* Global payment platform supporting 150+ countries
|
|
4
|
+
* @see https://docs.adyen.com/api-explorer/Checkout/71/overview
|
|
5
|
+
*/
|
|
6
|
+
import { PaymentProvider } from './base';
|
|
7
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
import { ProviderCapabilities } from '../routing-types';
|
|
9
|
+
interface AdyenConfig {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
merchantAccount: string;
|
|
12
|
+
liveUrlPrefix?: string;
|
|
13
|
+
webhookSecret?: string;
|
|
14
|
+
sandbox?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class AdyenProvider extends PaymentProvider {
|
|
17
|
+
readonly name = "adyen";
|
|
18
|
+
readonly supportedCurrencies: string[];
|
|
19
|
+
private apiKey;
|
|
20
|
+
private merchantAccount;
|
|
21
|
+
private liveUrlPrefix?;
|
|
22
|
+
private webhookSecret?;
|
|
23
|
+
private sandbox;
|
|
24
|
+
private baseUrl;
|
|
25
|
+
constructor(config: AdyenConfig);
|
|
26
|
+
private apiRequest;
|
|
27
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Adyen subscriptions require recurring tokenization flow (shopperReference + recurring contract).
|
|
30
|
+
* This is not yet supported by paybridge's simple checkout URL model.
|
|
31
|
+
* Use Stripe or PayFast for subscriptions.
|
|
32
|
+
*/
|
|
33
|
+
createSubscription(_params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
34
|
+
getPayment(id: string): Promise<PaymentResult>;
|
|
35
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Parse Adyen webhook notification.
|
|
38
|
+
* Note: Adyen webhooks contain multiple notificationItems in a batch.
|
|
39
|
+
* This method returns only the FIRST item (paybridge webhook interface is single-event).
|
|
40
|
+
* Multi-event batches require custom handling outside paybridge.
|
|
41
|
+
*/
|
|
42
|
+
parseWebhook(body: any, _headers?: any): WebhookEvent;
|
|
43
|
+
verifyWebhook(body: string | Buffer, _headers?: any): boolean;
|
|
44
|
+
getCapabilities(): ProviderCapabilities;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Adyen payment provider
|
|
4
|
+
* Global payment platform supporting 150+ countries
|
|
5
|
+
* @see https://docs.adyen.com/api-explorer/Checkout/71/overview
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.AdyenProvider = void 0;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const base_1 = require("./base");
|
|
44
|
+
const currency_1 = require("../utils/currency");
|
|
45
|
+
const fetch_1 = require("../utils/fetch");
|
|
46
|
+
class AdyenProvider extends base_1.PaymentProvider {
|
|
47
|
+
constructor(config) {
|
|
48
|
+
super();
|
|
49
|
+
this.name = 'adyen';
|
|
50
|
+
this.supportedCurrencies = ['ZAR', 'EUR', 'USD', 'GBP', 'AUD', 'BRL', 'INR', 'NGN'];
|
|
51
|
+
this.apiKey = config.apiKey;
|
|
52
|
+
this.merchantAccount = config.merchantAccount;
|
|
53
|
+
this.liveUrlPrefix = config.liveUrlPrefix;
|
|
54
|
+
this.webhookSecret = config.webhookSecret;
|
|
55
|
+
this.sandbox = config.sandbox ?? true;
|
|
56
|
+
if (this.sandbox) {
|
|
57
|
+
this.baseUrl = 'https://checkout-test.adyen.com/v71';
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (!this.liveUrlPrefix) {
|
|
61
|
+
throw new Error('Adyen live mode requires liveUrlPrefix');
|
|
62
|
+
}
|
|
63
|
+
this.baseUrl = `https://checkout-${this.liveUrlPrefix}.adyenpayments.com/v71`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async apiRequest(method, path, data) {
|
|
67
|
+
const url = `${this.baseUrl}${path}`;
|
|
68
|
+
const response = await (0, fetch_1.timedFetchOrThrow)(url, {
|
|
69
|
+
method,
|
|
70
|
+
headers: {
|
|
71
|
+
'x-API-key': this.apiKey,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
75
|
+
});
|
|
76
|
+
return (await response.json());
|
|
77
|
+
}
|
|
78
|
+
async createPayment(params) {
|
|
79
|
+
this.validateCurrency(params.currency);
|
|
80
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
81
|
+
const [firstName, ...lastNameParts] = params.customer.name.split(' ');
|
|
82
|
+
const lastName = lastNameParts.join(' ') || firstName;
|
|
83
|
+
const sessionData = {
|
|
84
|
+
amount: {
|
|
85
|
+
value: amountInMinorUnits,
|
|
86
|
+
currency: params.currency,
|
|
87
|
+
},
|
|
88
|
+
merchantAccount: this.merchantAccount,
|
|
89
|
+
reference: params.reference,
|
|
90
|
+
returnUrl: params.urls.success,
|
|
91
|
+
shopperEmail: params.customer.email,
|
|
92
|
+
shopperName: {
|
|
93
|
+
firstName,
|
|
94
|
+
lastName,
|
|
95
|
+
},
|
|
96
|
+
countryCode: 'ZA',
|
|
97
|
+
metadata: {
|
|
98
|
+
reference: params.reference,
|
|
99
|
+
...(params.metadata || {}),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const response = await this.apiRequest('POST', '/sessions', sessionData);
|
|
103
|
+
return {
|
|
104
|
+
id: response.id,
|
|
105
|
+
checkoutUrl: response.url,
|
|
106
|
+
status: 'pending',
|
|
107
|
+
amount: (0, currency_1.toMajorUnit)(response.amount.value, params.currency),
|
|
108
|
+
currency: response.amount.currency,
|
|
109
|
+
reference: params.reference,
|
|
110
|
+
provider: 'adyen',
|
|
111
|
+
createdAt: new Date().toISOString(),
|
|
112
|
+
raw: response,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Adyen subscriptions require recurring tokenization flow (shopperReference + recurring contract).
|
|
117
|
+
* This is not yet supported by paybridge's simple checkout URL model.
|
|
118
|
+
* Use Stripe or PayFast for subscriptions.
|
|
119
|
+
*/
|
|
120
|
+
async createSubscription(_params) {
|
|
121
|
+
throw new Error('Adyen subscriptions require recurring tokenization flow; not yet supported by paybridge. Use Stripe or PayFast for subscriptions.');
|
|
122
|
+
}
|
|
123
|
+
async getPayment(id) {
|
|
124
|
+
const session = await this.apiRequest('GET', `/sessions/${id}`);
|
|
125
|
+
let status = 'pending';
|
|
126
|
+
if (session.status === 'completed') {
|
|
127
|
+
status = 'completed';
|
|
128
|
+
}
|
|
129
|
+
else if (session.status === 'paymentPending' || session.status === 'pending') {
|
|
130
|
+
status = 'pending';
|
|
131
|
+
}
|
|
132
|
+
else if (session.status === 'expired') {
|
|
133
|
+
status = 'cancelled';
|
|
134
|
+
}
|
|
135
|
+
else if (session.status === 'refused' || session.status === 'error') {
|
|
136
|
+
status = 'failed';
|
|
137
|
+
}
|
|
138
|
+
const currency = session.amount?.currency || 'EUR';
|
|
139
|
+
return {
|
|
140
|
+
id: session.id,
|
|
141
|
+
checkoutUrl: session.url || '',
|
|
142
|
+
status,
|
|
143
|
+
amount: (0, currency_1.toMajorUnit)(session.amount?.value || 0, currency),
|
|
144
|
+
currency,
|
|
145
|
+
reference: session.reference || session.id,
|
|
146
|
+
provider: 'adyen',
|
|
147
|
+
createdAt: new Date().toISOString(),
|
|
148
|
+
raw: session,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async refund(params) {
|
|
152
|
+
const refundData = {
|
|
153
|
+
merchantAccount: this.merchantAccount,
|
|
154
|
+
reference: `refund-${params.paymentId}-${Date.now()}`,
|
|
155
|
+
};
|
|
156
|
+
if (params.amount !== undefined) {
|
|
157
|
+
refundData.amount = {
|
|
158
|
+
value: (0, currency_1.toMinorUnit)(params.amount, 'EUR'),
|
|
159
|
+
currency: 'EUR',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const response = await this.apiRequest('POST', `/payments/${params.paymentId}/refunds`, refundData);
|
|
163
|
+
const currency = response.amount?.currency || 'EUR';
|
|
164
|
+
return {
|
|
165
|
+
id: response.pspReference,
|
|
166
|
+
status: response.status === 'received' ? 'pending' : 'completed',
|
|
167
|
+
amount: (0, currency_1.toMajorUnit)(response.amount?.value || 0, currency),
|
|
168
|
+
currency,
|
|
169
|
+
paymentId: params.paymentId,
|
|
170
|
+
createdAt: new Date().toISOString(),
|
|
171
|
+
raw: response,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse Adyen webhook notification.
|
|
176
|
+
* Note: Adyen webhooks contain multiple notificationItems in a batch.
|
|
177
|
+
* This method returns only the FIRST item (paybridge webhook interface is single-event).
|
|
178
|
+
* Multi-event batches require custom handling outside paybridge.
|
|
179
|
+
*/
|
|
180
|
+
parseWebhook(body, _headers) {
|
|
181
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
182
|
+
const notificationItems = event.notificationItems || [];
|
|
183
|
+
if (notificationItems.length === 0) {
|
|
184
|
+
throw new Error('Adyen webhook contains no notificationItems');
|
|
185
|
+
}
|
|
186
|
+
const item = notificationItems[0].NotificationRequestItem;
|
|
187
|
+
const typeMap = {
|
|
188
|
+
AUTHORISATION: item.success === 'true' ? 'payment.completed' : 'payment.failed',
|
|
189
|
+
CANCELLATION: 'payment.cancelled',
|
|
190
|
+
REFUND: item.success === 'true' ? 'refund.completed' : 'payment.failed',
|
|
191
|
+
};
|
|
192
|
+
const eventType = typeMap[item.eventCode] || 'payment.pending';
|
|
193
|
+
const currency = item.amount?.currency || 'EUR';
|
|
194
|
+
let payment;
|
|
195
|
+
let refund;
|
|
196
|
+
if (item.eventCode === 'AUTHORISATION') {
|
|
197
|
+
payment = {
|
|
198
|
+
id: item.pspReference,
|
|
199
|
+
checkoutUrl: '',
|
|
200
|
+
status: item.success === 'true' ? 'completed' : 'failed',
|
|
201
|
+
amount: (0, currency_1.toMajorUnit)(item.amount?.value || 0, currency),
|
|
202
|
+
currency,
|
|
203
|
+
reference: item.merchantReference,
|
|
204
|
+
provider: 'adyen',
|
|
205
|
+
createdAt: new Date().toISOString(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
else if (item.eventCode === 'REFUND') {
|
|
209
|
+
refund = {
|
|
210
|
+
id: item.pspReference,
|
|
211
|
+
status: item.success === 'true' ? 'completed' : 'failed',
|
|
212
|
+
amount: (0, currency_1.toMajorUnit)(item.amount?.value || 0, currency),
|
|
213
|
+
currency,
|
|
214
|
+
paymentId: item.originalReference || item.pspReference,
|
|
215
|
+
createdAt: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
else if (item.eventCode === 'CANCELLATION') {
|
|
219
|
+
payment = {
|
|
220
|
+
id: item.pspReference,
|
|
221
|
+
checkoutUrl: '',
|
|
222
|
+
status: 'cancelled',
|
|
223
|
+
amount: (0, currency_1.toMajorUnit)(item.amount?.value || 0, currency),
|
|
224
|
+
currency,
|
|
225
|
+
reference: item.merchantReference,
|
|
226
|
+
provider: 'adyen',
|
|
227
|
+
createdAt: new Date().toISOString(),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
type: eventType,
|
|
232
|
+
payment,
|
|
233
|
+
refund,
|
|
234
|
+
raw: event,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
verifyWebhook(body, _headers) {
|
|
238
|
+
if (!this.webhookSecret) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const event = typeof body === 'string' ? JSON.parse(body) : JSON.parse(body.toString('utf8'));
|
|
242
|
+
const notificationItems = event.notificationItems || [];
|
|
243
|
+
if (notificationItems.length === 0) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const item = notificationItems[0].NotificationRequestItem;
|
|
247
|
+
const hmacSignature = item.additionalData?.hmacSignature;
|
|
248
|
+
if (!hmacSignature) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const signedFields = [
|
|
252
|
+
item.pspReference || '',
|
|
253
|
+
item.originalReference || '',
|
|
254
|
+
item.merchantAccountCode || this.merchantAccount,
|
|
255
|
+
item.merchantReference || '',
|
|
256
|
+
String(item.amount?.value || ''),
|
|
257
|
+
item.amount?.currency || '',
|
|
258
|
+
item.eventCode || '',
|
|
259
|
+
item.success || '',
|
|
260
|
+
];
|
|
261
|
+
const signedString = signedFields.join('|');
|
|
262
|
+
const hmacKey = Buffer.from(this.webhookSecret, 'hex');
|
|
263
|
+
const computedSig = crypto.createHmac('sha256', hmacKey).update(signedString, 'utf8').digest('base64');
|
|
264
|
+
try {
|
|
265
|
+
const computedBuffer = Buffer.from(computedSig, 'utf8');
|
|
266
|
+
const expectedBuffer = Buffer.from(hmacSignature, 'utf8');
|
|
267
|
+
if (computedBuffer.length !== expectedBuffer.length) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
return crypto.timingSafeEqual(computedBuffer, expectedBuffer);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
getCapabilities() {
|
|
277
|
+
return {
|
|
278
|
+
fees: {
|
|
279
|
+
fixed: 0.11,
|
|
280
|
+
percent: 0.6,
|
|
281
|
+
currency: 'EUR',
|
|
282
|
+
},
|
|
283
|
+
currencies: this.supportedCurrencies,
|
|
284
|
+
country: 'GLOBAL',
|
|
285
|
+
avgLatencyMs: 250,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
exports.AdyenProvider = AdyenProvider;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mercado Pago payment provider
|
|
3
|
+
* Leading payment platform for Latin America
|
|
4
|
+
* @see https://www.mercadopago.com/developers/en/reference
|
|
5
|
+
*/
|
|
6
|
+
import { PaymentProvider } from './base';
|
|
7
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
import { ProviderCapabilities } from '../routing-types';
|
|
9
|
+
interface MercadoPagoConfig {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
webhookSecret?: string;
|
|
12
|
+
sandbox?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class MercadoPagoProvider extends PaymentProvider {
|
|
15
|
+
readonly name = "mercadopago";
|
|
16
|
+
readonly supportedCurrencies: string[];
|
|
17
|
+
private accessToken;
|
|
18
|
+
private webhookSecret?;
|
|
19
|
+
private sandbox;
|
|
20
|
+
private baseUrl;
|
|
21
|
+
constructor(config: MercadoPagoConfig);
|
|
22
|
+
private apiRequest;
|
|
23
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
24
|
+
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
25
|
+
getPayment(id: string): Promise<PaymentResult>;
|
|
26
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Parse Mercado Pago webhook notification.
|
|
29
|
+
* Note: MP webhooks are notification events that require a follow-up API call to get full payment details.
|
|
30
|
+
* This method returns a pending event; caller should use getPayment(data.id) to fetch real status.
|
|
31
|
+
*/
|
|
32
|
+
parseWebhook(body: any, _headers?: any): WebhookEvent;
|
|
33
|
+
verifyWebhook(body: string | Buffer, headers?: any): boolean;
|
|
34
|
+
getCapabilities(): ProviderCapabilities;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Mercado Pago payment provider
|
|
4
|
+
* Leading payment platform for Latin America
|
|
5
|
+
* @see https://www.mercadopago.com/developers/en/reference
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.MercadoPagoProvider = void 0;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const base_1 = require("./base");
|
|
44
|
+
const fetch_1 = require("../utils/fetch");
|
|
45
|
+
class MercadoPagoProvider extends base_1.PaymentProvider {
|
|
46
|
+
constructor(config) {
|
|
47
|
+
super();
|
|
48
|
+
this.name = 'mercadopago';
|
|
49
|
+
this.supportedCurrencies = ['BRL', 'ARS', 'USD', 'MXN', 'COP', 'CLP', 'ZAR'];
|
|
50
|
+
this.baseUrl = 'https://api.mercadopago.com';
|
|
51
|
+
this.accessToken = config.accessToken;
|
|
52
|
+
this.webhookSecret = config.webhookSecret;
|
|
53
|
+
this.sandbox = config.sandbox ?? this.accessToken.startsWith('TEST-');
|
|
54
|
+
}
|
|
55
|
+
async apiRequest(method, path, data) {
|
|
56
|
+
const url = `${this.baseUrl}${path}`;
|
|
57
|
+
const response = await (0, fetch_1.timedFetch)(url, {
|
|
58
|
+
method,
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
},
|
|
63
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
64
|
+
});
|
|
65
|
+
const json = await response.json();
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const message = json.message || json.error || `Mercado Pago API error (${method} ${path}): ${response.status}`;
|
|
68
|
+
throw new Error(message);
|
|
69
|
+
}
|
|
70
|
+
return json;
|
|
71
|
+
}
|
|
72
|
+
async createPayment(params) {
|
|
73
|
+
this.validateCurrency(params.currency);
|
|
74
|
+
const [firstName, ...lastNameParts] = params.customer.name.split(' ');
|
|
75
|
+
const lastName = lastNameParts.join(' ') || firstName;
|
|
76
|
+
const metadata = {
|
|
77
|
+
reference: params.reference,
|
|
78
|
+
...(params.metadata || {}),
|
|
79
|
+
};
|
|
80
|
+
const requestBody = {
|
|
81
|
+
items: [
|
|
82
|
+
{
|
|
83
|
+
title: params.description || params.reference,
|
|
84
|
+
quantity: 1,
|
|
85
|
+
unit_price: params.amount,
|
|
86
|
+
currency_id: params.currency,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
payer: {
|
|
90
|
+
email: params.customer.email,
|
|
91
|
+
name: firstName,
|
|
92
|
+
surname: lastName,
|
|
93
|
+
},
|
|
94
|
+
external_reference: params.reference,
|
|
95
|
+
back_urls: {
|
|
96
|
+
success: params.urls.success,
|
|
97
|
+
failure: params.urls.cancel,
|
|
98
|
+
pending: params.urls.success,
|
|
99
|
+
},
|
|
100
|
+
notification_url: params.urls.webhook,
|
|
101
|
+
auto_return: 'approved',
|
|
102
|
+
metadata,
|
|
103
|
+
};
|
|
104
|
+
const response = await this.apiRequest('POST', '/checkout/preferences', requestBody);
|
|
105
|
+
const checkoutUrl = this.sandbox ? response.sandbox_init_point : response.init_point;
|
|
106
|
+
return {
|
|
107
|
+
id: response.id,
|
|
108
|
+
checkoutUrl,
|
|
109
|
+
status: 'pending',
|
|
110
|
+
amount: params.amount,
|
|
111
|
+
currency: params.currency.toUpperCase(),
|
|
112
|
+
reference: params.reference,
|
|
113
|
+
provider: 'mercadopago',
|
|
114
|
+
createdAt: new Date().toISOString(),
|
|
115
|
+
raw: response,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async createSubscription(params) {
|
|
119
|
+
this.validateCurrency(params.currency);
|
|
120
|
+
let frequency;
|
|
121
|
+
let frequencyType;
|
|
122
|
+
switch (params.interval) {
|
|
123
|
+
case 'weekly':
|
|
124
|
+
frequency = 7;
|
|
125
|
+
frequencyType = 'days';
|
|
126
|
+
break;
|
|
127
|
+
case 'monthly':
|
|
128
|
+
frequency = 1;
|
|
129
|
+
frequencyType = 'months';
|
|
130
|
+
break;
|
|
131
|
+
case 'yearly':
|
|
132
|
+
frequency = 12;
|
|
133
|
+
frequencyType = 'months';
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const requestBody = {
|
|
137
|
+
reason: params.description || params.reference,
|
|
138
|
+
auto_recurring: {
|
|
139
|
+
frequency,
|
|
140
|
+
frequency_type: frequencyType,
|
|
141
|
+
transaction_amount: params.amount,
|
|
142
|
+
currency_id: params.currency,
|
|
143
|
+
},
|
|
144
|
+
payer_email: params.customer.email,
|
|
145
|
+
back_url: params.urls.success,
|
|
146
|
+
external_reference: params.reference,
|
|
147
|
+
};
|
|
148
|
+
const response = await this.apiRequest('POST', '/preapproval', requestBody);
|
|
149
|
+
return {
|
|
150
|
+
id: response.id,
|
|
151
|
+
checkoutUrl: response.init_point,
|
|
152
|
+
status: 'pending',
|
|
153
|
+
amount: params.amount,
|
|
154
|
+
currency: params.currency.toUpperCase(),
|
|
155
|
+
interval: params.interval,
|
|
156
|
+
reference: params.reference,
|
|
157
|
+
provider: 'mercadopago',
|
|
158
|
+
startsAt: params.startDate,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
raw: response,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async getPayment(id) {
|
|
164
|
+
const response = await this.apiRequest('GET', `/v1/payments/search?external_reference=${encodeURIComponent(id)}`);
|
|
165
|
+
if (!response.results || response.results.length === 0) {
|
|
166
|
+
return {
|
|
167
|
+
id,
|
|
168
|
+
checkoutUrl: '',
|
|
169
|
+
status: 'pending',
|
|
170
|
+
amount: 0,
|
|
171
|
+
currency: 'BRL',
|
|
172
|
+
reference: id,
|
|
173
|
+
provider: 'mercadopago',
|
|
174
|
+
createdAt: new Date().toISOString(),
|
|
175
|
+
raw: response,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const payment = response.results[0];
|
|
179
|
+
const currency = (payment.currency_id || 'BRL').toUpperCase();
|
|
180
|
+
let status = 'pending';
|
|
181
|
+
if (payment.status === 'approved') {
|
|
182
|
+
status = 'completed';
|
|
183
|
+
}
|
|
184
|
+
else if (payment.status === 'rejected') {
|
|
185
|
+
status = 'failed';
|
|
186
|
+
}
|
|
187
|
+
else if (payment.status === 'cancelled') {
|
|
188
|
+
status = 'cancelled';
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
id: payment.id?.toString() || id,
|
|
192
|
+
checkoutUrl: '',
|
|
193
|
+
status,
|
|
194
|
+
amount: payment.transaction_amount || 0,
|
|
195
|
+
currency,
|
|
196
|
+
reference: payment.external_reference || id,
|
|
197
|
+
provider: 'mercadopago',
|
|
198
|
+
createdAt: new Date(payment.date_created || Date.now()).toISOString(),
|
|
199
|
+
raw: response,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async refund(params) {
|
|
203
|
+
const refundData = {};
|
|
204
|
+
if (params.amount !== undefined) {
|
|
205
|
+
refundData.amount = params.amount;
|
|
206
|
+
}
|
|
207
|
+
const response = await this.apiRequest('POST', `/v1/payments/${params.paymentId}/refunds`, refundData);
|
|
208
|
+
const currency = (response.payment?.currency_id || 'BRL').toUpperCase();
|
|
209
|
+
return {
|
|
210
|
+
id: response.id?.toString() || response.refund_id?.toString(),
|
|
211
|
+
status: response.status === 'approved' ? 'completed' : 'pending',
|
|
212
|
+
amount: response.amount || 0,
|
|
213
|
+
currency,
|
|
214
|
+
paymentId: params.paymentId,
|
|
215
|
+
createdAt: new Date(response.date_created || Date.now()).toISOString(),
|
|
216
|
+
raw: response,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Parse Mercado Pago webhook notification.
|
|
221
|
+
* Note: MP webhooks are notification events that require a follow-up API call to get full payment details.
|
|
222
|
+
* This method returns a pending event; caller should use getPayment(data.id) to fetch real status.
|
|
223
|
+
*/
|
|
224
|
+
parseWebhook(body, _headers) {
|
|
225
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
226
|
+
const eventType = 'payment.pending';
|
|
227
|
+
return {
|
|
228
|
+
type: eventType,
|
|
229
|
+
payment: {
|
|
230
|
+
id: event.data?.id?.toString() || '',
|
|
231
|
+
checkoutUrl: '',
|
|
232
|
+
status: 'pending',
|
|
233
|
+
amount: 0,
|
|
234
|
+
currency: 'BRL',
|
|
235
|
+
reference: '',
|
|
236
|
+
provider: 'mercadopago',
|
|
237
|
+
createdAt: new Date().toISOString(),
|
|
238
|
+
},
|
|
239
|
+
raw: event,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
verifyWebhook(body, headers) {
|
|
243
|
+
if (!this.webhookSecret) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const signature = headers?.['x-signature'] || headers?.['X-Signature'];
|
|
247
|
+
if (!signature) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const parts = signature.split(',').reduce((acc, part) => {
|
|
251
|
+
const [key, value] = part.split('=');
|
|
252
|
+
if (key && value) {
|
|
253
|
+
acc[key.trim()] = value.trim();
|
|
254
|
+
}
|
|
255
|
+
return acc;
|
|
256
|
+
}, {});
|
|
257
|
+
const ts = parts.ts;
|
|
258
|
+
const v1 = parts.v1;
|
|
259
|
+
if (!ts || !v1) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const timestampNum = parseInt(ts, 10);
|
|
263
|
+
const now = Math.floor(Date.now() / 1000);
|
|
264
|
+
if (now - timestampNum > 300) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
const requestId = headers?.['x-request-id'] || headers?.['X-Request-Id'];
|
|
268
|
+
const eventData = typeof body === 'string' ? JSON.parse(body) : body;
|
|
269
|
+
const dataId = eventData.data?.id || '';
|
|
270
|
+
const template = `id:${dataId};request-id:${requestId};ts:${ts};`;
|
|
271
|
+
const computedSig = crypto.createHmac('sha256', this.webhookSecret).update(template, 'utf8').digest('hex');
|
|
272
|
+
try {
|
|
273
|
+
const computedBuffer = Buffer.from(computedSig, 'hex');
|
|
274
|
+
const expectedBuffer = Buffer.from(v1, 'hex');
|
|
275
|
+
if (computedBuffer.length !== expectedBuffer.length) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return crypto.timingSafeEqual(computedBuffer, expectedBuffer);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
getCapabilities() {
|
|
285
|
+
return {
|
|
286
|
+
fees: {
|
|
287
|
+
fixed: 0,
|
|
288
|
+
percent: 4.99,
|
|
289
|
+
currency: 'BRL',
|
|
290
|
+
},
|
|
291
|
+
currencies: this.supportedCurrencies,
|
|
292
|
+
country: 'BR',
|
|
293
|
+
avgLatencyMs: 700,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
exports.MercadoPagoProvider = MercadoPagoProvider;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Razorpay payment provider
|
|
3
|
+
* Leading payment gateway for India
|
|
4
|
+
* @see https://razorpay.com/docs/api
|
|
5
|
+
*/
|
|
6
|
+
import { PaymentProvider } from './base';
|
|
7
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
import { ProviderCapabilities } from '../routing-types';
|
|
9
|
+
interface RazorpayConfig {
|
|
10
|
+
keyId: string;
|
|
11
|
+
keySecret: string;
|
|
12
|
+
webhookSecret?: string;
|
|
13
|
+
sandbox?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare class RazorpayProvider extends PaymentProvider {
|
|
16
|
+
readonly name = "razorpay";
|
|
17
|
+
readonly supportedCurrencies: string[];
|
|
18
|
+
private keyId;
|
|
19
|
+
private keySecret;
|
|
20
|
+
private webhookSecret?;
|
|
21
|
+
private sandbox;
|
|
22
|
+
private baseUrl;
|
|
23
|
+
constructor(config: RazorpayConfig);
|
|
24
|
+
private apiRequest;
|
|
25
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
26
|
+
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
27
|
+
getPayment(id: string): Promise<PaymentResult>;
|
|
28
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
29
|
+
parseWebhook(body: any, _headers?: any): WebhookEvent;
|
|
30
|
+
/**
|
|
31
|
+
* Verify Razorpay webhook signature using HMAC-SHA256.
|
|
32
|
+
* Note: Razorpay does not include timestamp in webhook signature (no replay protection).
|
|
33
|
+
*/
|
|
34
|
+
verifyWebhook(body: string | Buffer, headers?: any): boolean;
|
|
35
|
+
getCapabilities(): ProviderCapabilities;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Razorpay payment provider
|
|
4
|
+
* Leading payment gateway for India
|
|
5
|
+
* @see https://razorpay.com/docs/api
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.RazorpayProvider = void 0;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const base_1 = require("./base");
|
|
44
|
+
const currency_1 = require("../utils/currency");
|
|
45
|
+
const fetch_1 = require("../utils/fetch");
|
|
46
|
+
class RazorpayProvider extends base_1.PaymentProvider {
|
|
47
|
+
constructor(config) {
|
|
48
|
+
super();
|
|
49
|
+
this.name = 'razorpay';
|
|
50
|
+
this.supportedCurrencies = ['INR', 'USD', 'EUR', 'GBP', 'SGD', 'AED', 'AUD'];
|
|
51
|
+
this.baseUrl = 'https://api.razorpay.com/v1';
|
|
52
|
+
this.keyId = config.keyId;
|
|
53
|
+
this.keySecret = config.keySecret;
|
|
54
|
+
this.webhookSecret = config.webhookSecret;
|
|
55
|
+
this.sandbox = config.sandbox ?? this.keyId.startsWith('rzp_test_');
|
|
56
|
+
}
|
|
57
|
+
async apiRequest(method, path, data) {
|
|
58
|
+
const url = `${this.baseUrl}${path}`;
|
|
59
|
+
const authString = Buffer.from(`${this.keyId}:${this.keySecret}`).toString('base64');
|
|
60
|
+
const response = await (0, fetch_1.timedFetch)(url, {
|
|
61
|
+
method,
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Basic ${authString}`,
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
},
|
|
66
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
67
|
+
});
|
|
68
|
+
const json = await response.json();
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const message = json.error?.description || `Razorpay API error (${method} ${path}): ${response.status}`;
|
|
71
|
+
throw new Error(message);
|
|
72
|
+
}
|
|
73
|
+
return json;
|
|
74
|
+
}
|
|
75
|
+
async createPayment(params) {
|
|
76
|
+
this.validateCurrency(params.currency);
|
|
77
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
78
|
+
const orderData = {
|
|
79
|
+
amount: amountInMinorUnits,
|
|
80
|
+
currency: params.currency,
|
|
81
|
+
receipt: params.reference,
|
|
82
|
+
notes: {
|
|
83
|
+
reference: params.reference,
|
|
84
|
+
customerEmail: params.customer.email,
|
|
85
|
+
...(params.metadata || {}),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const order = await this.apiRequest('POST', '/orders', orderData);
|
|
89
|
+
const checkoutUrl = `https://api.razorpay.com/v1/checkout/embedded?key_id=${this.keyId}&order_id=${order.id}`;
|
|
90
|
+
return {
|
|
91
|
+
id: order.id,
|
|
92
|
+
checkoutUrl,
|
|
93
|
+
status: 'pending',
|
|
94
|
+
amount: (0, currency_1.toMajorUnit)(order.amount, params.currency),
|
|
95
|
+
currency: order.currency,
|
|
96
|
+
reference: params.reference,
|
|
97
|
+
provider: 'razorpay',
|
|
98
|
+
createdAt: new Date(order.created_at * 1000).toISOString(),
|
|
99
|
+
raw: order,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async createSubscription(params) {
|
|
103
|
+
this.validateCurrency(params.currency);
|
|
104
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
105
|
+
const intervalMap = {
|
|
106
|
+
weekly: 'weekly',
|
|
107
|
+
monthly: 'monthly',
|
|
108
|
+
yearly: 'yearly',
|
|
109
|
+
};
|
|
110
|
+
const planData = {
|
|
111
|
+
period: intervalMap[params.interval],
|
|
112
|
+
interval: 1,
|
|
113
|
+
item: {
|
|
114
|
+
name: params.description || params.reference,
|
|
115
|
+
amount: amountInMinorUnits,
|
|
116
|
+
currency: params.currency,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const plan = await this.apiRequest('POST', '/plans', planData);
|
|
120
|
+
const subscriptionData = {
|
|
121
|
+
plan_id: plan.id,
|
|
122
|
+
total_count: 12,
|
|
123
|
+
customer_notify: 1,
|
|
124
|
+
notes: {
|
|
125
|
+
reference: params.reference,
|
|
126
|
+
...(params.metadata || {}),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const subscription = await this.apiRequest('POST', '/subscriptions', subscriptionData);
|
|
130
|
+
const checkoutUrl = `https://api.razorpay.com/v1/checkout/embedded?key_id=${this.keyId}&subscription_id=${subscription.id}`;
|
|
131
|
+
return {
|
|
132
|
+
id: subscription.id,
|
|
133
|
+
checkoutUrl,
|
|
134
|
+
status: 'pending',
|
|
135
|
+
amount: (0, currency_1.toMajorUnit)(subscription.plan_id ? amountInMinorUnits : 0, params.currency),
|
|
136
|
+
currency: params.currency,
|
|
137
|
+
interval: params.interval,
|
|
138
|
+
reference: params.reference,
|
|
139
|
+
provider: 'razorpay',
|
|
140
|
+
startsAt: params.startDate,
|
|
141
|
+
createdAt: new Date(subscription.created_at * 1000).toISOString(),
|
|
142
|
+
raw: subscription,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async getPayment(id) {
|
|
146
|
+
const paymentsResponse = await this.apiRequest('GET', `/orders/${id}/payments`);
|
|
147
|
+
if (!paymentsResponse.items || paymentsResponse.items.length === 0) {
|
|
148
|
+
const order = await this.apiRequest('GET', `/orders/${id}`);
|
|
149
|
+
const currency = order.currency || 'INR';
|
|
150
|
+
return {
|
|
151
|
+
id: order.id,
|
|
152
|
+
checkoutUrl: '',
|
|
153
|
+
status: 'pending',
|
|
154
|
+
amount: (0, currency_1.toMajorUnit)(order.amount || 0, currency),
|
|
155
|
+
currency,
|
|
156
|
+
reference: order.receipt || id,
|
|
157
|
+
provider: 'razorpay',
|
|
158
|
+
createdAt: new Date(order.created_at * 1000).toISOString(),
|
|
159
|
+
raw: order,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const payment = paymentsResponse.items[0];
|
|
163
|
+
const currency = payment.currency || 'INR';
|
|
164
|
+
let status = 'pending';
|
|
165
|
+
if (payment.status === 'captured') {
|
|
166
|
+
status = 'completed';
|
|
167
|
+
}
|
|
168
|
+
else if (payment.status === 'authorized') {
|
|
169
|
+
status = 'pending';
|
|
170
|
+
}
|
|
171
|
+
else if (payment.status === 'failed') {
|
|
172
|
+
status = 'failed';
|
|
173
|
+
}
|
|
174
|
+
else if (payment.status === 'refunded') {
|
|
175
|
+
status = 'refunded';
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
id: payment.id,
|
|
179
|
+
checkoutUrl: '',
|
|
180
|
+
status,
|
|
181
|
+
amount: (0, currency_1.toMajorUnit)(payment.amount || 0, currency),
|
|
182
|
+
currency,
|
|
183
|
+
reference: payment.notes?.reference || payment.order_id || id,
|
|
184
|
+
provider: 'razorpay',
|
|
185
|
+
createdAt: new Date(payment.created_at * 1000).toISOString(),
|
|
186
|
+
raw: paymentsResponse,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async refund(params) {
|
|
190
|
+
let paymentId = params.paymentId;
|
|
191
|
+
if (paymentId.startsWith('order_')) {
|
|
192
|
+
const paymentsResponse = await this.apiRequest('GET', `/orders/${paymentId}/payments`);
|
|
193
|
+
if (paymentsResponse.items && paymentsResponse.items.length > 0) {
|
|
194
|
+
paymentId = paymentsResponse.items[0].id;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
throw new Error('Order has no payments to refund');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const refundData = {
|
|
201
|
+
speed: 'normal',
|
|
202
|
+
};
|
|
203
|
+
if (params.amount !== undefined) {
|
|
204
|
+
refundData.amount = (0, currency_1.toMinorUnit)(params.amount, 'INR');
|
|
205
|
+
}
|
|
206
|
+
if (params.reason) {
|
|
207
|
+
refundData.notes = { reason: params.reason };
|
|
208
|
+
}
|
|
209
|
+
const response = await this.apiRequest('POST', `/payments/${paymentId}/refund`, refundData);
|
|
210
|
+
const currency = response.currency || 'INR';
|
|
211
|
+
return {
|
|
212
|
+
id: response.id,
|
|
213
|
+
status: response.status === 'processed' ? 'completed' : 'pending',
|
|
214
|
+
amount: (0, currency_1.toMajorUnit)(response.amount || 0, currency),
|
|
215
|
+
currency,
|
|
216
|
+
paymentId: params.paymentId,
|
|
217
|
+
createdAt: new Date(response.created_at * 1000).toISOString(),
|
|
218
|
+
raw: response,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
parseWebhook(body, _headers) {
|
|
222
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
223
|
+
const typeMap = {
|
|
224
|
+
'payment.captured': 'payment.completed',
|
|
225
|
+
'payment.failed': 'payment.failed',
|
|
226
|
+
'subscription.activated': 'subscription.created',
|
|
227
|
+
'subscription.cancelled': 'subscription.cancelled',
|
|
228
|
+
'refund.created': 'refund.completed',
|
|
229
|
+
'refund.processed': 'refund.completed',
|
|
230
|
+
};
|
|
231
|
+
const eventType = typeMap[event.event] || 'payment.pending';
|
|
232
|
+
const data = event.payload?.payment?.entity || event.payload?.subscription?.entity || event.payload?.refund?.entity || {};
|
|
233
|
+
let payment;
|
|
234
|
+
let subscription;
|
|
235
|
+
let refund;
|
|
236
|
+
if (event.event.startsWith('payment.')) {
|
|
237
|
+
const currency = (data.currency || 'INR').toUpperCase();
|
|
238
|
+
let status = 'pending';
|
|
239
|
+
if (event.event === 'payment.captured') {
|
|
240
|
+
status = 'completed';
|
|
241
|
+
}
|
|
242
|
+
else if (event.event === 'payment.failed') {
|
|
243
|
+
status = 'failed';
|
|
244
|
+
}
|
|
245
|
+
payment = {
|
|
246
|
+
id: data.id || '',
|
|
247
|
+
checkoutUrl: '',
|
|
248
|
+
status,
|
|
249
|
+
amount: (0, currency_1.toMajorUnit)(data.amount || 0, currency),
|
|
250
|
+
currency,
|
|
251
|
+
reference: data.order_id || data.id || '',
|
|
252
|
+
provider: 'razorpay',
|
|
253
|
+
createdAt: new Date((data.created_at || Date.now() / 1000) * 1000).toISOString(),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
else if (event.event.startsWith('subscription.')) {
|
|
257
|
+
const currency = 'INR';
|
|
258
|
+
subscription = {
|
|
259
|
+
id: data.id || '',
|
|
260
|
+
checkoutUrl: '',
|
|
261
|
+
status: event.event === 'subscription.cancelled' ? 'cancelled' : 'active',
|
|
262
|
+
amount: (0, currency_1.toMajorUnit)(data.plan_id?.amount || 0, currency),
|
|
263
|
+
currency,
|
|
264
|
+
interval: 'monthly',
|
|
265
|
+
reference: data.notes?.reference || data.id || '',
|
|
266
|
+
provider: 'razorpay',
|
|
267
|
+
createdAt: new Date((data.created_at || Date.now() / 1000) * 1000).toISOString(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
else if (event.event.startsWith('refund.')) {
|
|
271
|
+
const currency = (data.currency || 'INR').toUpperCase();
|
|
272
|
+
refund = {
|
|
273
|
+
id: data.id || '',
|
|
274
|
+
status: event.event === 'refund.processed' ? 'completed' : 'pending',
|
|
275
|
+
amount: (0, currency_1.toMajorUnit)(data.amount || 0, currency),
|
|
276
|
+
currency,
|
|
277
|
+
paymentId: data.payment_id || '',
|
|
278
|
+
createdAt: new Date((data.created_at || Date.now() / 1000) * 1000).toISOString(),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
type: eventType,
|
|
283
|
+
payment,
|
|
284
|
+
subscription,
|
|
285
|
+
refund,
|
|
286
|
+
raw: event,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Verify Razorpay webhook signature using HMAC-SHA256.
|
|
291
|
+
* Note: Razorpay does not include timestamp in webhook signature (no replay protection).
|
|
292
|
+
*/
|
|
293
|
+
verifyWebhook(body, headers) {
|
|
294
|
+
if (!this.webhookSecret) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const signature = headers?.['x-razorpay-signature'] || headers?.['X-Razorpay-Signature'];
|
|
298
|
+
if (!signature) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const rawBody = typeof body === 'string' ? body : body.toString('utf8');
|
|
302
|
+
const computedSig = crypto.createHmac('sha256', this.webhookSecret).update(rawBody, 'utf8').digest('hex');
|
|
303
|
+
try {
|
|
304
|
+
const computedBuffer = Buffer.from(computedSig, 'hex');
|
|
305
|
+
const expectedBuffer = Buffer.from(signature, 'hex');
|
|
306
|
+
if (computedBuffer.length !== expectedBuffer.length) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return crypto.timingSafeEqual(computedBuffer, expectedBuffer);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
getCapabilities() {
|
|
316
|
+
return {
|
|
317
|
+
fees: {
|
|
318
|
+
fixed: 0,
|
|
319
|
+
percent: 2.0,
|
|
320
|
+
currency: 'INR',
|
|
321
|
+
},
|
|
322
|
+
currencies: this.supportedCurrencies,
|
|
323
|
+
country: 'IN',
|
|
324
|
+
avgLatencyMs: 500,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
exports.RazorpayProvider = RazorpayProvider;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PayBridge — Unified payment SDK types
|
|
3
3
|
*/
|
|
4
|
-
export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach' | 'flutterwave';
|
|
4
|
+
export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach' | 'flutterwave' | 'adyen' | 'mercadopago' | 'razorpay';
|
|
5
5
|
export type PaymentStatus = 'pending' | 'completed' | 'failed' | 'cancelled' | 'refunded';
|
|
6
6
|
export type SubscriptionInterval = 'weekly' | 'monthly' | 'yearly';
|
|
7
7
|
export type Currency = 'ZAR' | 'USD' | 'EUR' | 'GBP' | 'NGN' | 'KES' | 'UGX' | 'GHS' | string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paybridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "One API for fiat + crypto payments. Multi-provider routing, automatic failover, MoonPay on/off-ramp. SA-first, global-ready.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
"payment-integration",
|
|
29
29
|
"unified-api",
|
|
30
30
|
"zar",
|
|
31
|
-
"payment-sdk"
|
|
31
|
+
"payment-sdk",
|
|
32
|
+
"adyen",
|
|
33
|
+
"mercadopago",
|
|
34
|
+
"razorpay",
|
|
35
|
+
"india",
|
|
36
|
+
"latam"
|
|
32
37
|
],
|
|
33
38
|
"author": "Kobie Wentzel",
|
|
34
39
|
"license": "MIT",
|