paybridge 0.1.2 → 0.2.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 +87 -7
- package/dist/circuit-breaker-store.d.ts +27 -0
- package/dist/circuit-breaker-store.js +25 -0
- package/dist/circuit-breaker.d.ts +30 -0
- package/dist/circuit-breaker.js +86 -0
- package/dist/crypto/base.d.ts +15 -0
- package/dist/crypto/base.js +24 -0
- package/dist/crypto/index.d.ts +35 -0
- package/dist/crypto/index.js +95 -0
- package/dist/crypto/mock.d.ts +15 -0
- package/dist/crypto/mock.js +112 -0
- package/dist/crypto/moonpay.d.ts +33 -0
- package/dist/crypto/moonpay.js +251 -0
- package/dist/crypto/router.d.ts +36 -0
- package/dist/crypto/router.js +287 -0
- package/dist/crypto/types.d.ts +89 -0
- package/dist/crypto/types.js +5 -0
- package/dist/crypto/yellowcard.d.ts +56 -0
- package/dist/crypto/yellowcard.js +310 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +59 -3
- package/dist/providers/base.d.ts +5 -0
- package/dist/providers/flutterwave.d.ts +36 -0
- package/dist/providers/flutterwave.js +338 -0
- package/dist/providers/ozow.d.ts +20 -2
- package/dist/providers/ozow.js +161 -114
- package/dist/providers/payfast.d.ts +40 -0
- package/dist/providers/payfast.js +355 -0
- package/dist/providers/paystack.d.ts +37 -0
- package/dist/providers/paystack.js +335 -0
- package/dist/providers/peach.d.ts +50 -0
- package/dist/providers/peach.js +305 -0
- package/dist/providers/softycomp.d.ts +106 -0
- package/dist/providers/softycomp.js +234 -10
- package/dist/providers/stripe.d.ts +38 -0
- package/dist/providers/stripe.js +370 -0
- package/dist/providers/yoco.d.ts +12 -0
- package/dist/providers/yoco.js +159 -61
- package/dist/router.d.ts +33 -0
- package/dist/router.js +247 -0
- package/dist/routing-types.d.ts +39 -0
- package/dist/routing-types.js +14 -0
- package/dist/stores/redis.d.ts +30 -0
- package/dist/stores/redis.js +42 -0
- package/dist/strategies.d.ts +18 -0
- package/dist/strategies.js +44 -0
- package/dist/types.d.ts +4 -2
- package/package.json +7 -4
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PayBridgeRouter — Multi-provider routing with fallback
|
|
3
|
+
*/
|
|
4
|
+
import { PayBridge } from './index';
|
|
5
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent, Provider } from './types';
|
|
6
|
+
import { RoutingStrategy, FallbackConfig } from './routing-types';
|
|
7
|
+
export interface PayBridgeRouterConfig {
|
|
8
|
+
providers: Array<{
|
|
9
|
+
provider: PayBridge;
|
|
10
|
+
weight?: number;
|
|
11
|
+
priority?: number;
|
|
12
|
+
}>;
|
|
13
|
+
strategy?: RoutingStrategy;
|
|
14
|
+
fallback?: FallbackConfig;
|
|
15
|
+
circuitBreakerStore?: import('./circuit-breaker-store').CircuitBreakerStore;
|
|
16
|
+
}
|
|
17
|
+
export declare class PayBridgeRouter {
|
|
18
|
+
private providers;
|
|
19
|
+
private strategy;
|
|
20
|
+
private fallback;
|
|
21
|
+
private circuitBreakers;
|
|
22
|
+
private rrIndex;
|
|
23
|
+
private config;
|
|
24
|
+
constructor(config: PayBridgeRouterConfig);
|
|
25
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
26
|
+
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
27
|
+
getPayment(id: string, provider?: string): Promise<PaymentResult>;
|
|
28
|
+
refund(params: RefundParams, provider?: string): Promise<RefundResult>;
|
|
29
|
+
parseWebhook(body: any, headers: any, providerName: Provider): WebhookEvent;
|
|
30
|
+
verifyWebhook(body: any, headers: any, providerName: Provider): boolean;
|
|
31
|
+
private filterProviders;
|
|
32
|
+
private sleep;
|
|
33
|
+
}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PayBridgeRouter — Multi-provider routing with fallback
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PayBridgeRouter = void 0;
|
|
7
|
+
const routing_types_1 = require("./routing-types");
|
|
8
|
+
const strategies_1 = require("./strategies");
|
|
9
|
+
const circuit_breaker_1 = require("./circuit-breaker");
|
|
10
|
+
function sanitizeErrorMessage(msg) {
|
|
11
|
+
if (!msg)
|
|
12
|
+
return 'unknown error';
|
|
13
|
+
return msg
|
|
14
|
+
.replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[REDACTED]')
|
|
15
|
+
.replace(/(api[_-]?key|secret|token|password)["':=\s]+\S+/gi, '$1=[REDACTED]')
|
|
16
|
+
.slice(0, 500);
|
|
17
|
+
}
|
|
18
|
+
class PayBridgeRouter {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.rrIndex = 0;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.providers = config.providers.map(p => ({
|
|
23
|
+
instance: p.provider,
|
|
24
|
+
weight: p.weight,
|
|
25
|
+
priority: p.priority,
|
|
26
|
+
}));
|
|
27
|
+
this.strategy = config.strategy ?? 'cheapest';
|
|
28
|
+
this.fallback = {
|
|
29
|
+
enabled: config.fallback?.enabled ?? true,
|
|
30
|
+
maxAttempts: config.fallback?.maxAttempts ?? 3,
|
|
31
|
+
retryDelayMs: config.fallback?.retryDelayMs ?? 250,
|
|
32
|
+
};
|
|
33
|
+
this.circuitBreakers = new Map();
|
|
34
|
+
for (const p of this.providers) {
|
|
35
|
+
const name = p.instance.getProviderName();
|
|
36
|
+
this.circuitBreakers.set(name, new circuit_breaker_1.CircuitBreaker(name, {
|
|
37
|
+
store: config.circuitBreakerStore,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async createPayment(params) {
|
|
42
|
+
if (!Number.isFinite(params.amount) || params.amount <= 0) {
|
|
43
|
+
throw new Error('Invalid amount: must be a positive finite number');
|
|
44
|
+
}
|
|
45
|
+
const context = {
|
|
46
|
+
amount: params.amount,
|
|
47
|
+
currency: params.currency,
|
|
48
|
+
};
|
|
49
|
+
const filtered = this.filterProviders(params.currency, params.amount);
|
|
50
|
+
if (filtered.length === 0) {
|
|
51
|
+
throw new Error(`No providers support currency ${params.currency} with amount ${params.amount}`);
|
|
52
|
+
}
|
|
53
|
+
const strategyFn = (0, strategies_1.getStrategy)(this.strategy);
|
|
54
|
+
const ordered = strategyFn(filtered, context, () => {
|
|
55
|
+
const idx = this.rrIndex;
|
|
56
|
+
this.rrIndex = (this.rrIndex + 1) % filtered.length;
|
|
57
|
+
return idx;
|
|
58
|
+
});
|
|
59
|
+
const attempts = [];
|
|
60
|
+
let lastError = null;
|
|
61
|
+
for (const providerMeta of ordered) {
|
|
62
|
+
const providerName = providerMeta.instance.getProviderName();
|
|
63
|
+
const breaker = this.circuitBreakers.get(providerName);
|
|
64
|
+
if (breaker && (await breaker.isOpen())) {
|
|
65
|
+
attempts.push({
|
|
66
|
+
provider: providerName,
|
|
67
|
+
status: 'skipped',
|
|
68
|
+
errorMessage: 'Circuit breaker open',
|
|
69
|
+
latencyMs: 0,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
try {
|
|
75
|
+
const result = await providerMeta.instance.createPayment(params);
|
|
76
|
+
const latencyMs = Date.now() - startTime;
|
|
77
|
+
if (breaker)
|
|
78
|
+
await breaker.recordSuccess();
|
|
79
|
+
attempts.push({
|
|
80
|
+
provider: providerName,
|
|
81
|
+
status: 'success',
|
|
82
|
+
latencyMs,
|
|
83
|
+
});
|
|
84
|
+
const routingMeta = {
|
|
85
|
+
attempts,
|
|
86
|
+
chosenProvider: providerName,
|
|
87
|
+
strategy: this.strategy,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
...result,
|
|
91
|
+
routingMeta,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const latencyMs = Date.now() - startTime;
|
|
96
|
+
lastError = error;
|
|
97
|
+
if (breaker)
|
|
98
|
+
await breaker.recordFailure();
|
|
99
|
+
attempts.push({
|
|
100
|
+
provider: providerName,
|
|
101
|
+
status: 'failed',
|
|
102
|
+
errorCode: error.code,
|
|
103
|
+
errorMessage: sanitizeErrorMessage(error.message),
|
|
104
|
+
latencyMs,
|
|
105
|
+
});
|
|
106
|
+
if (!this.fallback.enabled || attempts.length >= (this.fallback.maxAttempts ?? 3)) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
await this.sleep(this.fallback.retryDelayMs ?? 250);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
|
|
113
|
+
}
|
|
114
|
+
async createSubscription(params) {
|
|
115
|
+
const context = {
|
|
116
|
+
amount: params.amount,
|
|
117
|
+
currency: params.currency,
|
|
118
|
+
};
|
|
119
|
+
const filtered = this.filterProviders(params.currency, params.amount);
|
|
120
|
+
if (filtered.length === 0) {
|
|
121
|
+
throw new Error(`No providers support currency ${params.currency} with amount ${params.amount}`);
|
|
122
|
+
}
|
|
123
|
+
const strategyFn = (0, strategies_1.getStrategy)(this.strategy);
|
|
124
|
+
const ordered = strategyFn(filtered, context, () => {
|
|
125
|
+
const idx = this.rrIndex;
|
|
126
|
+
this.rrIndex = (this.rrIndex + 1) % filtered.length;
|
|
127
|
+
return idx;
|
|
128
|
+
});
|
|
129
|
+
const attempts = [];
|
|
130
|
+
let lastError = null;
|
|
131
|
+
for (const providerMeta of ordered) {
|
|
132
|
+
const providerName = providerMeta.instance.getProviderName();
|
|
133
|
+
const breaker = this.circuitBreakers.get(providerName);
|
|
134
|
+
if (breaker && (await breaker.isOpen())) {
|
|
135
|
+
attempts.push({
|
|
136
|
+
provider: providerName,
|
|
137
|
+
status: 'skipped',
|
|
138
|
+
errorMessage: 'Circuit breaker open',
|
|
139
|
+
latencyMs: 0,
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
try {
|
|
145
|
+
const result = await providerMeta.instance.createSubscription(params);
|
|
146
|
+
const latencyMs = Date.now() - startTime;
|
|
147
|
+
if (breaker)
|
|
148
|
+
await breaker.recordSuccess();
|
|
149
|
+
attempts.push({
|
|
150
|
+
provider: providerName,
|
|
151
|
+
status: 'success',
|
|
152
|
+
latencyMs,
|
|
153
|
+
});
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const latencyMs = Date.now() - startTime;
|
|
158
|
+
lastError = error;
|
|
159
|
+
if (breaker)
|
|
160
|
+
await breaker.recordFailure();
|
|
161
|
+
attempts.push({
|
|
162
|
+
provider: providerName,
|
|
163
|
+
status: 'failed',
|
|
164
|
+
errorCode: error.code,
|
|
165
|
+
errorMessage: sanitizeErrorMessage(error.message),
|
|
166
|
+
latencyMs,
|
|
167
|
+
});
|
|
168
|
+
if (!this.fallback.enabled || attempts.length >= (this.fallback.maxAttempts ?? 3)) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
await this.sleep(this.fallback.retryDelayMs ?? 250);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw new routing_types_1.RoutingError(`All providers failed for subscription: ${lastError?.message || 'Unknown error'}`, attempts);
|
|
175
|
+
}
|
|
176
|
+
async getPayment(id, provider) {
|
|
177
|
+
if (provider) {
|
|
178
|
+
const providerMeta = this.providers.find(p => p.instance.getProviderName() === provider);
|
|
179
|
+
if (!providerMeta) {
|
|
180
|
+
throw new Error(`Provider ${provider} not found in router`);
|
|
181
|
+
}
|
|
182
|
+
return providerMeta.instance.getPayment(id);
|
|
183
|
+
}
|
|
184
|
+
let lastError = null;
|
|
185
|
+
for (const providerMeta of this.providers) {
|
|
186
|
+
try {
|
|
187
|
+
return await providerMeta.instance.getPayment(id);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
lastError = error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`Payment ${id} not found in any provider: ${lastError?.message || 'Unknown error'}`);
|
|
194
|
+
}
|
|
195
|
+
async refund(params, provider) {
|
|
196
|
+
if (provider) {
|
|
197
|
+
const providerMeta = this.providers.find(p => p.instance.getProviderName() === provider);
|
|
198
|
+
if (!providerMeta) {
|
|
199
|
+
throw new Error(`Provider ${provider} not found in router`);
|
|
200
|
+
}
|
|
201
|
+
return providerMeta.instance.refund(params);
|
|
202
|
+
}
|
|
203
|
+
let lastError = null;
|
|
204
|
+
for (const providerMeta of this.providers) {
|
|
205
|
+
try {
|
|
206
|
+
return await providerMeta.instance.refund(params);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
lastError = error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`Refund for payment ${params.paymentId} failed on all providers: ${lastError?.message || 'Unknown error'}`);
|
|
213
|
+
}
|
|
214
|
+
parseWebhook(body, headers, providerName) {
|
|
215
|
+
const providerMeta = this.providers.find(p => p.instance.getProviderName() === providerName);
|
|
216
|
+
if (!providerMeta) {
|
|
217
|
+
throw new Error(`Unknown provider for webhook: ${providerName}`);
|
|
218
|
+
}
|
|
219
|
+
return providerMeta.instance.parseWebhook(body, headers);
|
|
220
|
+
}
|
|
221
|
+
verifyWebhook(body, headers, providerName) {
|
|
222
|
+
const providerMeta = this.providers.find(p => p.instance.getProviderName() === providerName);
|
|
223
|
+
if (!providerMeta) {
|
|
224
|
+
throw new Error(`Unknown provider for webhook: ${providerName}`);
|
|
225
|
+
}
|
|
226
|
+
return providerMeta.instance.verifyWebhook(body, headers);
|
|
227
|
+
}
|
|
228
|
+
filterProviders(currency, amount) {
|
|
229
|
+
return this.providers.filter(p => {
|
|
230
|
+
const caps = p.instance.provider.getCapabilities();
|
|
231
|
+
if (!caps.currencies.includes(currency)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (caps.minAmount !== undefined && amount < caps.minAmount) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (caps.maxAmount !== undefined && amount > caps.maxAmount) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
sleep(ms) {
|
|
244
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
exports.PayBridgeRouter = PayBridgeRouter;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for payment routing
|
|
3
|
+
*/
|
|
4
|
+
import { Currency } from './types';
|
|
5
|
+
export interface ProviderFees {
|
|
6
|
+
fixed: number;
|
|
7
|
+
percent: number;
|
|
8
|
+
currency: Currency;
|
|
9
|
+
}
|
|
10
|
+
export interface ProviderCapabilities {
|
|
11
|
+
fees: ProviderFees;
|
|
12
|
+
currencies: string[];
|
|
13
|
+
minAmount?: number;
|
|
14
|
+
maxAmount?: number;
|
|
15
|
+
avgLatencyMs?: number;
|
|
16
|
+
country: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RoutingAttempt {
|
|
19
|
+
provider: string;
|
|
20
|
+
status: 'success' | 'failed' | 'skipped';
|
|
21
|
+
errorCode?: string;
|
|
22
|
+
errorMessage?: string;
|
|
23
|
+
latencyMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RoutingMeta {
|
|
26
|
+
attempts: RoutingAttempt[];
|
|
27
|
+
chosenProvider: string;
|
|
28
|
+
strategy: RoutingStrategy;
|
|
29
|
+
}
|
|
30
|
+
export type RoutingStrategy = 'cheapest' | 'fastest' | 'priority' | 'round-robin';
|
|
31
|
+
export interface FallbackConfig {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
maxAttempts?: number;
|
|
34
|
+
retryDelayMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare class RoutingError extends Error {
|
|
37
|
+
readonly attempts: RoutingAttempt[];
|
|
38
|
+
constructor(message: string, attempts: RoutingAttempt[]);
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Types for payment routing
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RoutingError = void 0;
|
|
7
|
+
class RoutingError extends Error {
|
|
8
|
+
constructor(message, attempts) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'RoutingError';
|
|
11
|
+
this.attempts = attempts;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.RoutingError = RoutingError;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed circuit breaker store adapter
|
|
3
|
+
*/
|
|
4
|
+
import { CircuitBreakerStore } from '../circuit-breaker-store';
|
|
5
|
+
/**
|
|
6
|
+
* Duck-typed Redis client interface.
|
|
7
|
+
* Compatible with both ioredis and node-redis v4+.
|
|
8
|
+
*/
|
|
9
|
+
export interface RedisLike {
|
|
10
|
+
get(key: string): Promise<string | null>;
|
|
11
|
+
set(key: string, value: string, ...args: any[]): Promise<any>;
|
|
12
|
+
del(key: string): Promise<any>;
|
|
13
|
+
}
|
|
14
|
+
export interface RedisStoreOptions {
|
|
15
|
+
prefix?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a Redis-backed circuit breaker store.
|
|
19
|
+
* Works with ioredis and node-redis v4+ (duck-types get/set/del).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import Redis from 'ioredis';
|
|
24
|
+
* import { createRedisCircuitBreakerStore } from 'paybridge';
|
|
25
|
+
*
|
|
26
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
27
|
+
* const store = createRedisCircuitBreakerStore(redis, { prefix: 'app:cb:' });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function createRedisCircuitBreakerStore(client: RedisLike, options?: RedisStoreOptions): CircuitBreakerStore;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis-backed circuit breaker store adapter
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createRedisCircuitBreakerStore = createRedisCircuitBreakerStore;
|
|
7
|
+
/**
|
|
8
|
+
* Create a Redis-backed circuit breaker store.
|
|
9
|
+
* Works with ioredis and node-redis v4+ (duck-types get/set/del).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import Redis from 'ioredis';
|
|
14
|
+
* import { createRedisCircuitBreakerStore } from 'paybridge';
|
|
15
|
+
*
|
|
16
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
17
|
+
* const store = createRedisCircuitBreakerStore(redis, { prefix: 'app:cb:' });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function createRedisCircuitBreakerStore(client, options = {}) {
|
|
21
|
+
const prefix = options.prefix ?? 'paybridge:cb:';
|
|
22
|
+
return {
|
|
23
|
+
async get(key) {
|
|
24
|
+
const raw = await client.get(prefix + key);
|
|
25
|
+
if (!raw)
|
|
26
|
+
return null;
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
},
|
|
29
|
+
async set(key, snapshot, ttlMs) {
|
|
30
|
+
const value = JSON.stringify(snapshot);
|
|
31
|
+
if (ttlMs !== undefined) {
|
|
32
|
+
await client.set(prefix + key, value, 'PX', ttlMs);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await client.set(prefix + key, value);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
async delete(key) {
|
|
39
|
+
await client.del(prefix + key);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing strategies for multi-provider payments
|
|
3
|
+
*/
|
|
4
|
+
import { PayBridge } from './index';
|
|
5
|
+
import { Currency } from './types';
|
|
6
|
+
import { RoutingStrategy } from './routing-types';
|
|
7
|
+
export interface ProviderWithMeta {
|
|
8
|
+
instance: PayBridge;
|
|
9
|
+
weight?: number;
|
|
10
|
+
priority?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface StrategyContext {
|
|
13
|
+
amount: number;
|
|
14
|
+
currency: Currency;
|
|
15
|
+
}
|
|
16
|
+
export type Strategy = (providers: ProviderWithMeta[], context: StrategyContext, getRRIndex?: () => number) => ProviderWithMeta[];
|
|
17
|
+
export declare const strategies: Record<RoutingStrategy, Strategy>;
|
|
18
|
+
export declare function getStrategy(name: RoutingStrategy): Strategy;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Routing strategies for multi-provider payments
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.strategies = void 0;
|
|
7
|
+
exports.getStrategy = getStrategy;
|
|
8
|
+
exports.strategies = {
|
|
9
|
+
cheapest: (providers, context) => {
|
|
10
|
+
return [...providers].sort((a, b) => {
|
|
11
|
+
const capsA = a.instance.provider.getCapabilities();
|
|
12
|
+
const capsB = b.instance.provider.getCapabilities();
|
|
13
|
+
const feeA = capsA.fees.fixed + (context.amount * capsA.fees.percent) / 100;
|
|
14
|
+
const feeB = capsB.fees.fixed + (context.amount * capsB.fees.percent) / 100;
|
|
15
|
+
return feeA - feeB;
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
fastest: (providers, _context) => {
|
|
19
|
+
return [...providers].sort((a, b) => {
|
|
20
|
+
const capsA = a.instance.provider.getCapabilities();
|
|
21
|
+
const capsB = b.instance.provider.getCapabilities();
|
|
22
|
+
const latencyA = capsA.avgLatencyMs ?? 1000;
|
|
23
|
+
const latencyB = capsB.avgLatencyMs ?? 1000;
|
|
24
|
+
return latencyA - latencyB;
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
priority: (providers, _context) => {
|
|
28
|
+
return [...providers].sort((a, b) => {
|
|
29
|
+
const priorityA = a.priority ?? 0;
|
|
30
|
+
const priorityB = b.priority ?? 0;
|
|
31
|
+
return priorityB - priorityA;
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
'round-robin': (providers, _context, getRRIndex) => {
|
|
35
|
+
if (!getRRIndex) {
|
|
36
|
+
return providers;
|
|
37
|
+
}
|
|
38
|
+
const idx = getRRIndex();
|
|
39
|
+
return [...providers.slice(idx), ...providers.slice(0, idx)];
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
function getStrategy(name) {
|
|
43
|
+
return exports.strategies[name];
|
|
44
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PayBridge — Unified payment SDK types
|
|
3
3
|
*/
|
|
4
|
-
export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach';
|
|
4
|
+
export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach' | 'flutterwave';
|
|
5
5
|
export type PaymentStatus = 'pending' | 'completed' | 'failed' | 'cancelled' | 'refunded';
|
|
6
6
|
export type SubscriptionInterval = 'weekly' | 'monthly' | 'yearly';
|
|
7
|
-
export type Currency = 'ZAR' | 'USD' | 'EUR' | 'GBP' | 'NGN';
|
|
7
|
+
export type Currency = 'ZAR' | 'USD' | 'EUR' | 'GBP' | 'NGN' | 'KES' | 'UGX' | 'GHS' | string;
|
|
8
8
|
export type WebhookEventType = 'payment.pending' | 'payment.completed' | 'payment.failed' | 'payment.cancelled' | 'subscription.created' | 'subscription.cancelled' | 'refund.completed';
|
|
9
9
|
export interface PayBridgeConfig {
|
|
10
10
|
/** Payment provider to use */
|
|
@@ -77,6 +77,8 @@ export interface PaymentResult {
|
|
|
77
77
|
expiresAt?: string;
|
|
78
78
|
/** Raw provider response */
|
|
79
79
|
raw?: any;
|
|
80
|
+
/** Routing metadata (populated by PayBridgeRouter) */
|
|
81
|
+
routingMeta?: import('./routing-types').RoutingMeta;
|
|
80
82
|
}
|
|
81
83
|
export interface CreateSubscriptionParams {
|
|
82
84
|
/** Amount in major currency unit (e.g. 299.00 for R299) */
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paybridge",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "One API
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"clean": "rm -rf dist",
|
|
10
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
11
|
+
"test": "tsc && node --test dist/tests/*.test.js",
|
|
12
|
+
"test:e2e:moonpay": "tsx tests/e2e/moonpay-sandbox.ts",
|
|
13
|
+
"test:e2e:yellowcard": "tsx tests/e2e/yellowcard-sandbox.ts"
|
|
11
14
|
},
|
|
12
15
|
"keywords": [
|
|
13
16
|
"payments",
|