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.
package/dist/router.js CHANGED
@@ -8,6 +8,8 @@ const routing_types_1 = require("./routing-types");
8
8
  const strategies_1 = require("./strategies");
9
9
  const circuit_breaker_1 = require("./circuit-breaker");
10
10
  const fetch_1 = require("./utils/fetch");
11
+ const router_events_1 = require("./router-events");
12
+ const tracer_1 = require("./tracer");
11
13
  function sanitizeErrorMessage(msg) {
12
14
  if (!msg)
13
15
  return 'unknown error';
@@ -27,6 +29,7 @@ class WebhookDuplicateError extends Error {
27
29
  exports.WebhookDuplicateError = WebhookDuplicateError;
28
30
  class PayBridgeRouter {
29
31
  constructor(config) {
32
+ this.events = new router_events_1.RouterEventEmitter();
30
33
  this.rrIndex = 0;
31
34
  this.config = config;
32
35
  this.providers = config.providers.map(p => ({
@@ -41,12 +44,36 @@ class PayBridgeRouter {
41
44
  retryDelayMs: config.fallback?.retryDelayMs ?? 250,
42
45
  };
43
46
  this.idempotencyStore = config.idempotencyStore;
47
+ this.ledger = config.ledger;
48
+ this.tracer = config.tracer ?? tracer_1.noopTracer;
44
49
  this.circuitBreakers = new Map();
45
50
  for (const p of this.providers) {
46
51
  const name = p.instance.getProviderName();
47
- this.circuitBreakers.set(name, new circuit_breaker_1.CircuitBreaker(name, {
52
+ const breaker = new circuit_breaker_1.CircuitBreaker(name, {
48
53
  store: config.circuitBreakerStore,
49
- }));
54
+ });
55
+ breaker.events.on('opened', (key) => {
56
+ this.events.emitEvent({
57
+ type: 'circuit.opened',
58
+ provider: key,
59
+ timestamp: new Date().toISOString(),
60
+ });
61
+ });
62
+ breaker.events.on('half_opened', (key) => {
63
+ this.events.emitEvent({
64
+ type: 'circuit.half_opened',
65
+ provider: key,
66
+ timestamp: new Date().toISOString(),
67
+ });
68
+ });
69
+ breaker.events.on('closed', (key) => {
70
+ this.events.emitEvent({
71
+ type: 'circuit.closed',
72
+ provider: key,
73
+ timestamp: new Date().toISOString(),
74
+ });
75
+ });
76
+ this.circuitBreakers.set(name, breaker);
50
77
  }
51
78
  }
52
79
  async createPayment(params) {
@@ -82,9 +109,24 @@ class PayBridgeRouter {
82
109
  continue;
83
110
  }
84
111
  const startTime = Date.now();
112
+ const span = this.tracer.startSpan('paybridge.router.createPayment', {
113
+ 'paybridge.provider': providerName,
114
+ 'paybridge.strategy': this.strategy,
115
+ 'paybridge.attempt': attempts.length + 1,
116
+ });
117
+ this.events.emitEvent({
118
+ type: 'attempt.start',
119
+ provider: providerName,
120
+ operation: 'createPayment',
121
+ reference: params.reference,
122
+ attempt: attempts.length + 1,
123
+ timestamp: new Date().toISOString(),
124
+ });
85
125
  try {
86
126
  const result = await providerMeta.instance.createPayment(params);
87
127
  const latencyMs = Date.now() - startTime;
128
+ span.setAttribute('paybridge.payment.id', result.id);
129
+ span.setAttribute('paybridge.payment.status', result.status);
88
130
  if (breaker)
89
131
  await breaker.recordSuccess();
90
132
  attempts.push({
@@ -92,21 +134,59 @@ class PayBridgeRouter {
92
134
  status: 'success',
93
135
  latencyMs,
94
136
  });
137
+ this.events.emitEvent({
138
+ type: 'attempt.success',
139
+ provider: providerName,
140
+ operation: 'createPayment',
141
+ reference: params.reference,
142
+ durationMs: latencyMs,
143
+ attempt: attempts.length,
144
+ timestamp: new Date().toISOString(),
145
+ });
146
+ this.events.emitEvent({
147
+ type: 'request.success',
148
+ provider: providerName,
149
+ operation: 'createPayment',
150
+ reference: params.reference,
151
+ durationMs: latencyMs,
152
+ timestamp: new Date().toISOString(),
153
+ });
154
+ await this.recordLedgerEntry({
155
+ id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
156
+ timestamp: new Date().toISOString(),
157
+ operation: 'createPayment',
158
+ provider: providerName,
159
+ reference: params.reference,
160
+ providerId: result.id,
161
+ status: 'success',
162
+ amount: params.amount,
163
+ currency: params.currency,
164
+ durationMs: latencyMs,
165
+ });
95
166
  const routingMeta = {
96
167
  attempts,
97
168
  chosenProvider: providerName,
98
169
  strategy: this.strategy,
99
170
  };
171
+ span.end();
100
172
  return {
101
173
  ...result,
102
174
  routingMeta,
103
175
  };
104
176
  }
105
177
  catch (error) {
178
+ span.recordException?.(error instanceof Error ? error : new Error(String(error)));
106
179
  const latencyMs = Date.now() - startTime;
107
180
  lastError = error;
108
181
  const isRateLimited = error instanceof fetch_1.HttpError &&
109
182
  (error.status === 429 || (error.status === 503 && error.retryAfterMs !== undefined));
183
+ let errorCode = error.code;
184
+ if (error instanceof fetch_1.FetchTimeoutError) {
185
+ errorCode = 'TIMEOUT';
186
+ }
187
+ else if (isRateLimited) {
188
+ errorCode = 'RATE_LIMITED';
189
+ }
110
190
  if (isRateLimited) {
111
191
  attempts.push({
112
192
  provider: providerName,
@@ -115,14 +195,34 @@ class PayBridgeRouter {
115
195
  errorMessage: sanitizeErrorMessage(error.message),
116
196
  latencyMs,
117
197
  });
198
+ this.events.emitEvent({
199
+ type: 'attempt.rate_limited',
200
+ provider: providerName,
201
+ operation: 'createPayment',
202
+ reference: params.reference,
203
+ durationMs: latencyMs,
204
+ errorCode: 'RATE_LIMITED',
205
+ errorMessage: sanitizeErrorMessage(error.message),
206
+ attempt: attempts.length,
207
+ timestamp: new Date().toISOString(),
208
+ });
209
+ await this.recordLedgerEntry({
210
+ id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
211
+ timestamp: new Date().toISOString(),
212
+ operation: 'createPayment',
213
+ provider: providerName,
214
+ reference: params.reference,
215
+ status: 'rate_limited',
216
+ amount: params.amount,
217
+ currency: params.currency,
218
+ durationMs: latencyMs,
219
+ errorCode: 'RATE_LIMITED',
220
+ errorMessage: sanitizeErrorMessage(error.message),
221
+ });
118
222
  }
119
223
  else {
120
224
  if (breaker)
121
225
  await breaker.recordFailure();
122
- let errorCode = error.code;
123
- if (error instanceof fetch_1.FetchTimeoutError) {
124
- errorCode = 'TIMEOUT';
125
- }
126
226
  attempts.push({
127
227
  provider: providerName,
128
228
  status: 'failed',
@@ -130,13 +230,48 @@ class PayBridgeRouter {
130
230
  errorMessage: sanitizeErrorMessage(error.message),
131
231
  latencyMs,
132
232
  });
233
+ const eventType = error instanceof fetch_1.FetchTimeoutError ? 'attempt.timeout' : 'attempt.failure';
234
+ this.events.emitEvent({
235
+ type: eventType,
236
+ provider: providerName,
237
+ operation: 'createPayment',
238
+ reference: params.reference,
239
+ durationMs: latencyMs,
240
+ errorCode,
241
+ errorMessage: sanitizeErrorMessage(error.message),
242
+ attempt: attempts.length,
243
+ timestamp: new Date().toISOString(),
244
+ });
245
+ const ledgerStatus = error instanceof fetch_1.FetchTimeoutError ? 'timeout' : 'failed';
246
+ await this.recordLedgerEntry({
247
+ id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
248
+ timestamp: new Date().toISOString(),
249
+ operation: 'createPayment',
250
+ provider: providerName,
251
+ reference: params.reference,
252
+ status: ledgerStatus,
253
+ amount: params.amount,
254
+ currency: params.currency,
255
+ durationMs: latencyMs,
256
+ errorCode,
257
+ errorMessage: sanitizeErrorMessage(error.message),
258
+ });
133
259
  }
260
+ span.setAttribute('paybridge.error.code', errorCode ?? 'unknown');
261
+ span.end();
134
262
  if (!this.fallback.enabled || attempts.length >= (this.fallback.maxAttempts ?? 3)) {
135
263
  break;
136
264
  }
137
265
  await this.sleep(this.fallback.retryDelayMs ?? 250);
138
266
  }
139
267
  }
268
+ this.events.emitEvent({
269
+ type: 'request.failure',
270
+ operation: 'createPayment',
271
+ reference: params.reference,
272
+ errorMessage: lastError?.message || 'All providers failed',
273
+ timestamp: new Date().toISOString(),
274
+ });
140
275
  throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
141
276
  }
142
277
  async createSubscription(params) {
@@ -169,6 +304,14 @@ class PayBridgeRouter {
169
304
  continue;
170
305
  }
171
306
  const startTime = Date.now();
307
+ this.events.emitEvent({
308
+ type: 'attempt.start',
309
+ provider: providerName,
310
+ operation: 'createSubscription',
311
+ reference: params.reference,
312
+ attempt: attempts.length + 1,
313
+ timestamp: new Date().toISOString(),
314
+ });
172
315
  try {
173
316
  const result = await providerMeta.instance.createSubscription(params);
174
317
  const latencyMs = Date.now() - startTime;
@@ -179,6 +322,35 @@ class PayBridgeRouter {
179
322
  status: 'success',
180
323
  latencyMs,
181
324
  });
325
+ this.events.emitEvent({
326
+ type: 'attempt.success',
327
+ provider: providerName,
328
+ operation: 'createSubscription',
329
+ reference: params.reference,
330
+ durationMs: latencyMs,
331
+ attempt: attempts.length,
332
+ timestamp: new Date().toISOString(),
333
+ });
334
+ this.events.emitEvent({
335
+ type: 'request.success',
336
+ provider: providerName,
337
+ operation: 'createSubscription',
338
+ reference: params.reference,
339
+ durationMs: latencyMs,
340
+ timestamp: new Date().toISOString(),
341
+ });
342
+ await this.recordLedgerEntry({
343
+ id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
344
+ timestamp: new Date().toISOString(),
345
+ operation: 'createSubscription',
346
+ provider: providerName,
347
+ reference: params.reference,
348
+ providerId: result.id,
349
+ status: 'success',
350
+ amount: params.amount,
351
+ currency: params.currency,
352
+ durationMs: latencyMs,
353
+ });
182
354
  return result;
183
355
  }
184
356
  catch (error) {
@@ -194,6 +366,30 @@ class PayBridgeRouter {
194
366
  errorMessage: sanitizeErrorMessage(error.message),
195
367
  latencyMs,
196
368
  });
369
+ this.events.emitEvent({
370
+ type: 'attempt.rate_limited',
371
+ provider: providerName,
372
+ operation: 'createSubscription',
373
+ reference: params.reference,
374
+ durationMs: latencyMs,
375
+ errorCode: 'RATE_LIMITED',
376
+ errorMessage: sanitizeErrorMessage(error.message),
377
+ attempt: attempts.length,
378
+ timestamp: new Date().toISOString(),
379
+ });
380
+ await this.recordLedgerEntry({
381
+ id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
382
+ timestamp: new Date().toISOString(),
383
+ operation: 'createSubscription',
384
+ provider: providerName,
385
+ reference: params.reference,
386
+ status: 'rate_limited',
387
+ amount: params.amount,
388
+ currency: params.currency,
389
+ durationMs: latencyMs,
390
+ errorCode: 'RATE_LIMITED',
391
+ errorMessage: sanitizeErrorMessage(error.message),
392
+ });
197
393
  }
198
394
  else {
199
395
  if (breaker)
@@ -209,6 +405,32 @@ class PayBridgeRouter {
209
405
  errorMessage: sanitizeErrorMessage(error.message),
210
406
  latencyMs,
211
407
  });
408
+ const eventType = error instanceof fetch_1.FetchTimeoutError ? 'attempt.timeout' : 'attempt.failure';
409
+ this.events.emitEvent({
410
+ type: eventType,
411
+ provider: providerName,
412
+ operation: 'createSubscription',
413
+ reference: params.reference,
414
+ durationMs: latencyMs,
415
+ errorCode,
416
+ errorMessage: sanitizeErrorMessage(error.message),
417
+ attempt: attempts.length,
418
+ timestamp: new Date().toISOString(),
419
+ });
420
+ const ledgerStatus = error instanceof fetch_1.FetchTimeoutError ? 'timeout' : 'failed';
421
+ await this.recordLedgerEntry({
422
+ id: `${providerName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
423
+ timestamp: new Date().toISOString(),
424
+ operation: 'createSubscription',
425
+ provider: providerName,
426
+ reference: params.reference,
427
+ status: ledgerStatus,
428
+ amount: params.amount,
429
+ currency: params.currency,
430
+ durationMs: latencyMs,
431
+ errorCode,
432
+ errorMessage: sanitizeErrorMessage(error.message),
433
+ });
212
434
  }
213
435
  if (!this.fallback.enabled || attempts.length >= (this.fallback.maxAttempts ?? 3)) {
214
436
  break;
@@ -216,6 +438,13 @@ class PayBridgeRouter {
216
438
  await this.sleep(this.fallback.retryDelayMs ?? 250);
217
439
  }
218
440
  }
441
+ this.events.emitEvent({
442
+ type: 'request.failure',
443
+ operation: 'createSubscription',
444
+ reference: params.reference,
445
+ errorMessage: lastError?.message || 'All providers failed',
446
+ timestamp: new Date().toISOString(),
447
+ });
219
448
  throw new routing_types_1.RoutingError(`All providers failed for subscription: ${lastError?.message || 'Unknown error'}`, attempts);
220
449
  }
221
450
  async getPayment(id, provider) {
@@ -269,6 +498,13 @@ class PayBridgeRouter {
269
498
  const ttlMs = 24 * 60 * 60 * 1000;
270
499
  const isNew = await this.idempotencyStore.recordIfNew(key, ttlMs);
271
500
  if (!isNew) {
501
+ this.events.emitEvent({
502
+ type: 'webhook.duplicate',
503
+ provider: providerName,
504
+ operation: 'parseWebhook',
505
+ reference: eventId,
506
+ timestamp: new Date().toISOString(),
507
+ });
272
508
  throw new WebhookDuplicateError(providerName, eventId);
273
509
  }
274
510
  }
@@ -300,5 +536,21 @@ class PayBridgeRouter {
300
536
  sleep(ms) {
301
537
  return new Promise(resolve => setTimeout(resolve, ms));
302
538
  }
539
+ async recordLedgerEntry(entry) {
540
+ if (!this.ledger)
541
+ return;
542
+ try {
543
+ await this.ledger.append(entry);
544
+ }
545
+ catch (err) {
546
+ this.events.emitEvent({
547
+ type: 'attempt.failure',
548
+ operation: entry.operation,
549
+ provider: entry.provider,
550
+ errorMessage: 'Ledger write failed',
551
+ timestamp: new Date().toISOString(),
552
+ });
553
+ }
554
+ }
303
555
  }
304
556
  exports.PayBridgeRouter = PayBridgeRouter;
@@ -0,0 +1,8 @@
1
+ import type { RedisLike } from './redis';
2
+ import type { LedgerStore } from '../ledger';
3
+ export interface RedisLedgerStoreOptions {
4
+ redis: RedisLike;
5
+ keyPrefix?: string;
6
+ maxEntries?: number;
7
+ }
8
+ export declare function createRedisLedgerStore(opts: RedisLedgerStoreOptions): LedgerStore;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRedisLedgerStore = createRedisLedgerStore;
4
+ function createRedisLedgerStore(opts) {
5
+ const prefix = opts.keyPrefix ?? 'pb:ledger:';
6
+ const maxEntries = opts.maxEntries ?? 100000;
7
+ const listKey = `${prefix}list`;
8
+ const refIndexPrefix = `${prefix}ref:`;
9
+ if (!opts.redis.lpush || !opts.redis.ltrim || !opts.redis.lrange || !opts.redis.sadd || !opts.redis.smembers || !opts.redis.expire) {
10
+ throw new Error('Redis client does not support required list/set operations for ledger store');
11
+ }
12
+ return {
13
+ async append(entry) {
14
+ const serialized = JSON.stringify(entry);
15
+ await opts.redis.lpush(listKey, serialized);
16
+ await opts.redis.ltrim(listKey, 0, maxEntries - 1);
17
+ if (entry.reference) {
18
+ const refKey = `${refIndexPrefix}${entry.reference}`;
19
+ await opts.redis.sadd(refKey, entry.id);
20
+ await opts.redis.expire(refKey, 86400 * 30);
21
+ }
22
+ },
23
+ async query(filter) {
24
+ let entries = [];
25
+ if (filter.reference) {
26
+ const refKey = `${refIndexPrefix}${filter.reference}`;
27
+ const ids = await opts.redis.smembers(refKey);
28
+ if (!ids || ids.length === 0)
29
+ return [];
30
+ const allEntries = await opts.redis.lrange(listKey, 0, -1);
31
+ entries = allEntries
32
+ .map((s) => JSON.parse(s))
33
+ .filter((e) => ids.includes(e.id));
34
+ }
35
+ else {
36
+ const allEntries = await opts.redis.lrange(listKey, 0, -1);
37
+ entries = allEntries.map((s) => JSON.parse(s));
38
+ }
39
+ if (filter.provider)
40
+ entries = entries.filter(e => e.provider === filter.provider);
41
+ if (filter.status)
42
+ entries = entries.filter(e => e.status === filter.status);
43
+ if (filter.fromTimestamp)
44
+ entries = entries.filter(e => e.timestamp >= filter.fromTimestamp);
45
+ if (filter.toTimestamp)
46
+ entries = entries.filter(e => e.timestamp <= filter.toTimestamp);
47
+ if (filter.limit)
48
+ entries = entries.slice(0, filter.limit);
49
+ return entries;
50
+ },
51
+ };
52
+ }
@@ -10,6 +10,12 @@ export interface RedisLike {
10
10
  get(key: string): Promise<string | null>;
11
11
  set(key: string, value: string, ...args: any[]): Promise<any>;
12
12
  del(key: string): Promise<any>;
13
+ lpush?(key: string, ...values: string[]): Promise<number>;
14
+ ltrim?(key: string, start: number, stop: number): Promise<string>;
15
+ lrange?(key: string, start: number, stop: number): Promise<string[]>;
16
+ sadd?(key: string, ...members: string[]): Promise<number>;
17
+ smembers?(key: string): Promise<string[]>;
18
+ expire?(key: string, seconds: number): Promise<number>;
13
19
  }
14
20
  export interface RedisStoreOptions {
15
21
  prefix?: string;
@@ -0,0 +1,9 @@
1
+ export interface SpanLike {
2
+ setAttribute(key: string, value: string | number | boolean): void;
3
+ recordException?(error: Error): void;
4
+ end(): void;
5
+ }
6
+ export interface TracerLike {
7
+ startSpan(name: string, attributes?: Record<string, string | number | boolean>): SpanLike;
8
+ }
9
+ export declare const noopTracer: TracerLike;
package/dist/tracer.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noopTracer = void 0;
4
+ exports.noopTracer = {
5
+ startSpan: () => ({
6
+ setAttribute: () => { },
7
+ recordException: () => { },
8
+ end: () => { },
9
+ }),
10
+ };
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.1",
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",