paybridge 0.3.1 → 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.
@@ -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;
@@ -0,0 +1,16 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export type RouterEventType = 'attempt.start' | 'attempt.success' | 'attempt.failure' | 'attempt.rate_limited' | 'attempt.timeout' | 'circuit.opened' | 'circuit.half_opened' | 'circuit.closed' | 'webhook.duplicate' | 'request.success' | 'request.failure';
3
+ export interface RouterEvent {
4
+ type: RouterEventType;
5
+ provider?: string;
6
+ operation?: 'createPayment' | 'createSubscription' | 'getPayment' | 'refund' | 'parseWebhook' | 'createOnRamp' | 'createOffRamp' | 'getQuote' | 'getRamp';
7
+ reference?: string;
8
+ durationMs?: number;
9
+ errorCode?: string;
10
+ errorMessage?: string;
11
+ attempt?: number;
12
+ timestamp: string;
13
+ }
14
+ export declare class RouterEventEmitter extends EventEmitter {
15
+ emitEvent(event: RouterEvent): void;
16
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RouterEventEmitter = void 0;
4
+ const node_events_1 = require("node:events");
5
+ class RouterEventEmitter extends node_events_1.EventEmitter {
6
+ emitEvent(event) {
7
+ this.emit(event.type, event);
8
+ this.emit('*', event);
9
+ }
10
+ }
11
+ exports.RouterEventEmitter = RouterEventEmitter;
package/dist/router.d.ts CHANGED
@@ -5,6 +5,9 @@ import { PayBridge } from './index';
5
5
  import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent, Provider } from './types';
6
6
  import { RoutingStrategy, FallbackConfig } from './routing-types';
7
7
  import type { IdempotencyStore } from './webhook-idempotency-store';
8
+ import { RouterEventEmitter } from './router-events';
9
+ import type { LedgerStore } from './ledger';
10
+ import type { TracerLike } from './tracer';
8
11
  export declare class WebhookDuplicateError extends Error {
9
12
  readonly name = "WebhookDuplicateError";
10
13
  readonly eventId: string;
@@ -21,13 +24,18 @@ export interface PayBridgeRouterConfig {
21
24
  fallback?: FallbackConfig;
22
25
  circuitBreakerStore?: import('./circuit-breaker-store').CircuitBreakerStore;
23
26
  idempotencyStore?: IdempotencyStore;
27
+ ledger?: LedgerStore;
28
+ tracer?: TracerLike;
24
29
  }
25
30
  export declare class PayBridgeRouter {
31
+ readonly events: RouterEventEmitter;
26
32
  private providers;
27
33
  private strategy;
28
34
  private fallback;
29
35
  private circuitBreakers;
30
36
  private idempotencyStore?;
37
+ private ledger?;
38
+ private tracer;
31
39
  private rrIndex;
32
40
  private config;
33
41
  constructor(config: PayBridgeRouterConfig);
@@ -39,4 +47,5 @@ export declare class PayBridgeRouter {
39
47
  verifyWebhook(body: any, headers: any, providerName: Provider): boolean;
40
48
  private filterProviders;
41
49
  private sleep;
50
+ private recordLedgerEntry;
42
51
  }