paybridge 0.11.0 → 0.12.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 +87 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/router.d.ts +2 -1
- package/dist/router.js +26 -14
- package/dist/routing-types.d.ts +1 -1
- package/dist/stores/postgres-ledger.d.ts +9 -0
- package/dist/stores/postgres-ledger.js +113 -0
- package/dist/stores/postgres.d.ts +7 -0
- package/dist/stores/postgres.js +2 -0
- package/dist/strategies.d.ts +17 -0
- package/dist/strategies.js +70 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -141,6 +141,65 @@ Reconciled: 5
|
|
|
141
141
|
|
|
142
142
|
Exit code 1 if any mismatch, 0 if clean. Add `--webhook-url` to POST mismatch reports to Slack/Discord/your ops channel. Use `--json` for pipeline integration.
|
|
143
143
|
|
|
144
|
+
## Postgres Ledger
|
|
145
|
+
|
|
146
|
+
The ledger stores every payment attempt and outcome. In-memory and Redis adapters are ephemeral. For durable transaction history, use the Postgres adapter.
|
|
147
|
+
|
|
148
|
+
### Schema Setup
|
|
149
|
+
|
|
150
|
+
Create the table once via your migration tool:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { getPostgresLedgerTableSql } from 'paybridge';
|
|
154
|
+
|
|
155
|
+
const sql = getPostgresLedgerTableSql();
|
|
156
|
+
await pool.query(sql);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or run the SQL manually:
|
|
160
|
+
|
|
161
|
+
```sql
|
|
162
|
+
CREATE TABLE paybridge_ledger (
|
|
163
|
+
id TEXT PRIMARY KEY,
|
|
164
|
+
timestamp TIMESTAMPTZ NOT NULL,
|
|
165
|
+
operation TEXT NOT NULL,
|
|
166
|
+
provider TEXT NOT NULL,
|
|
167
|
+
reference TEXT,
|
|
168
|
+
provider_id TEXT,
|
|
169
|
+
status TEXT NOT NULL,
|
|
170
|
+
amount NUMERIC,
|
|
171
|
+
currency TEXT,
|
|
172
|
+
duration_ms INTEGER,
|
|
173
|
+
error_code TEXT,
|
|
174
|
+
error_message TEXT,
|
|
175
|
+
metadata JSONB
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
CREATE INDEX idx_paybridge_ledger_provider_timestamp
|
|
179
|
+
ON paybridge_ledger (provider, timestamp DESC);
|
|
180
|
+
CREATE INDEX idx_paybridge_ledger_reference
|
|
181
|
+
ON paybridge_ledger (reference) WHERE reference IS NOT NULL;
|
|
182
|
+
CREATE INDEX idx_paybridge_ledger_status
|
|
183
|
+
ON paybridge_ledger (status);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Usage
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { Pool } from 'pg';
|
|
190
|
+
import { PayBridgeRouter, createPostgresLedgerStore } from 'paybridge';
|
|
191
|
+
|
|
192
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
193
|
+
const ledger = createPostgresLedgerStore({ pool });
|
|
194
|
+
|
|
195
|
+
const router = new PayBridgeRouter({
|
|
196
|
+
providers: [...],
|
|
197
|
+
ledger,
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Compatible with `pg` (`Pool`) and `postgres` (porsager) adapter wrappers. No runtime dep on `pg` — bring your own client.
|
|
202
|
+
|
|
144
203
|
## Quick Start
|
|
145
204
|
|
|
146
205
|
> **Upgrading from 0.1 or 0.2?** See [docs/migration.md](docs/migration.md).
|
|
@@ -612,6 +671,34 @@ const router = new PayBridgeRouter({
|
|
|
612
671
|
|
|
613
672
|
The Redis adapter works with both `ioredis` and `redis` (node-redis v4+) clients. State is eventually consistent across instances — race conditions during state transitions may cause a few extra failures, but correctness is preserved.
|
|
614
673
|
|
|
674
|
+
### Intelligent routing — successRate strategy
|
|
675
|
+
|
|
676
|
+
Static fee tables lie. PayBridge routes by actual outcomes when you give it ledger access:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
import { PayBridgeRouter, createSuccessRateStrategy, createPostgresLedgerStore } from 'paybridge';
|
|
680
|
+
import { Pool } from 'pg';
|
|
681
|
+
|
|
682
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
683
|
+
const ledger = createPostgresLedgerStore({ pool });
|
|
684
|
+
|
|
685
|
+
const strategy = createSuccessRateStrategy({
|
|
686
|
+
ledger,
|
|
687
|
+
windowMs: 24 * 60 * 60 * 1000,
|
|
688
|
+
fallback: 'cheapest',
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const router = new PayBridgeRouter({
|
|
692
|
+
providers: [...],
|
|
693
|
+
strategy,
|
|
694
|
+
ledger,
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Providers are ranked by their real success rate from your own traffic. Below the minimum sample size, the configured fallback (default `cheapest`) takes over.
|
|
699
|
+
|
|
700
|
+
**Why this matters**: A provider with 1.4% fee but 92% success rate costs more per successful transaction than a 2.5% / 99.5% provider. `successRate` makes routing decisions based on real outcomes from your traffic, not static fee tables.
|
|
701
|
+
|
|
615
702
|
## Currency Handling
|
|
616
703
|
|
|
617
704
|
PayBridge **always uses major currency units** (rands, dollars) in the API:
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export * from './tracer';
|
|
|
22
22
|
export { createRedisCircuitBreakerStore, type RedisLike, type RedisStoreOptions } from './stores/redis';
|
|
23
23
|
export { createRedisIdempotencyStore, type RedisIdempotencyStoreOptions } from './stores/redis-idempotency';
|
|
24
24
|
export { createRedisLedgerStore, type RedisLedgerStoreOptions } from './stores/redis-ledger';
|
|
25
|
+
export { createPostgresLedgerStore, type PostgresLedgerStoreOptions, getCreateTableSql as getPostgresLedgerTableSql } from './stores/postgres-ledger';
|
|
26
|
+
export type { PgPoolLike, PgQueryResult } from './stores/postgres';
|
|
25
27
|
export { runReconcile, type ReconcileDeps } from './cli/reconcile';
|
|
26
28
|
export * from './cli/reconcile-types';
|
|
27
29
|
export declare class PayBridge {
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
20
20
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.PayBridge = exports.runReconcile = exports.createRedisLedgerStore = exports.createRedisIdempotencyStore = exports.createRedisCircuitBreakerStore = void 0;
|
|
23
|
+
exports.PayBridge = exports.runReconcile = exports.getPostgresLedgerTableSql = exports.createPostgresLedgerStore = exports.createRedisLedgerStore = exports.createRedisIdempotencyStore = exports.createRedisCircuitBreakerStore = void 0;
|
|
24
24
|
const softycomp_1 = require("./providers/softycomp");
|
|
25
25
|
const yoco_1 = require("./providers/yoco");
|
|
26
26
|
const ozow_1 = require("./providers/ozow");
|
|
@@ -54,6 +54,9 @@ var redis_idempotency_1 = require("./stores/redis-idempotency");
|
|
|
54
54
|
Object.defineProperty(exports, "createRedisIdempotencyStore", { enumerable: true, get: function () { return redis_idempotency_1.createRedisIdempotencyStore; } });
|
|
55
55
|
var redis_ledger_1 = require("./stores/redis-ledger");
|
|
56
56
|
Object.defineProperty(exports, "createRedisLedgerStore", { enumerable: true, get: function () { return redis_ledger_1.createRedisLedgerStore; } });
|
|
57
|
+
var postgres_ledger_1 = require("./stores/postgres-ledger");
|
|
58
|
+
Object.defineProperty(exports, "createPostgresLedgerStore", { enumerable: true, get: function () { return postgres_ledger_1.createPostgresLedgerStore; } });
|
|
59
|
+
Object.defineProperty(exports, "getPostgresLedgerTableSql", { enumerable: true, get: function () { return postgres_ledger_1.getCreateTableSql; } });
|
|
57
60
|
var reconcile_1 = require("./cli/reconcile");
|
|
58
61
|
Object.defineProperty(exports, "runReconcile", { enumerable: true, get: function () { return reconcile_1.runReconcile; } });
|
|
59
62
|
__exportStar(require("./cli/reconcile-types"), exports);
|
package/dist/router.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
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
|
+
import { type SuccessRateStrategy } from './strategies';
|
|
7
8
|
import type { IdempotencyStore } from './webhook-idempotency-store';
|
|
8
9
|
import { RouterEventEmitter } from './router-events';
|
|
9
10
|
import type { LedgerStore } from './ledger';
|
|
@@ -20,7 +21,7 @@ export interface PayBridgeRouterConfig {
|
|
|
20
21
|
weight?: number;
|
|
21
22
|
priority?: number;
|
|
22
23
|
}>;
|
|
23
|
-
strategy?: RoutingStrategy;
|
|
24
|
+
strategy?: RoutingStrategy | SuccessRateStrategy;
|
|
24
25
|
fallback?: FallbackConfig;
|
|
25
26
|
circuitBreakerStore?: import('./circuit-breaker-store').CircuitBreakerStore;
|
|
26
27
|
idempotencyStore?: IdempotencyStore;
|
package/dist/router.js
CHANGED
|
@@ -88,12 +88,18 @@ class PayBridgeRouter {
|
|
|
88
88
|
if (filtered.length === 0) {
|
|
89
89
|
throw new Error(`No providers support currency ${params.currency} with amount ${params.amount}`);
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
let ordered;
|
|
92
|
+
if (typeof this.strategy === 'string') {
|
|
93
|
+
const strategyFn = (0, strategies_1.getStrategy)(this.strategy);
|
|
94
|
+
ordered = strategyFn(filtered, context, () => {
|
|
95
|
+
const idx = this.rrIndex;
|
|
96
|
+
this.rrIndex = (this.rrIndex + 1) % filtered.length;
|
|
97
|
+
return idx;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
ordered = await this.strategy.order(filtered);
|
|
102
|
+
}
|
|
97
103
|
const attempts = [];
|
|
98
104
|
let lastError = null;
|
|
99
105
|
for (const providerMeta of ordered) {
|
|
@@ -111,7 +117,7 @@ class PayBridgeRouter {
|
|
|
111
117
|
const startTime = Date.now();
|
|
112
118
|
const span = this.tracer.startSpan('paybridge.router.createPayment', {
|
|
113
119
|
'paybridge.provider': providerName,
|
|
114
|
-
'paybridge.strategy': this.strategy,
|
|
120
|
+
'paybridge.strategy': typeof this.strategy === 'string' ? this.strategy : 'successRate',
|
|
115
121
|
'paybridge.attempt': attempts.length + 1,
|
|
116
122
|
});
|
|
117
123
|
this.events.emitEvent({
|
|
@@ -166,7 +172,7 @@ class PayBridgeRouter {
|
|
|
166
172
|
const routingMeta = {
|
|
167
173
|
attempts,
|
|
168
174
|
chosenProvider: providerName,
|
|
169
|
-
strategy: this.strategy,
|
|
175
|
+
strategy: typeof this.strategy === 'string' ? this.strategy : 'successRate',
|
|
170
176
|
};
|
|
171
177
|
span.end();
|
|
172
178
|
return {
|
|
@@ -283,12 +289,18 @@ class PayBridgeRouter {
|
|
|
283
289
|
if (filtered.length === 0) {
|
|
284
290
|
throw new Error(`No providers support currency ${params.currency} with amount ${params.amount}`);
|
|
285
291
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
+
let ordered;
|
|
293
|
+
if (typeof this.strategy === 'string') {
|
|
294
|
+
const strategyFn = (0, strategies_1.getStrategy)(this.strategy);
|
|
295
|
+
ordered = strategyFn(filtered, context, () => {
|
|
296
|
+
const idx = this.rrIndex;
|
|
297
|
+
this.rrIndex = (this.rrIndex + 1) % filtered.length;
|
|
298
|
+
return idx;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
ordered = await this.strategy.order(filtered);
|
|
303
|
+
}
|
|
292
304
|
const attempts = [];
|
|
293
305
|
let lastError = null;
|
|
294
306
|
for (const providerMeta of ordered) {
|
package/dist/routing-types.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface RoutingAttempt {
|
|
|
25
25
|
export interface RoutingMeta {
|
|
26
26
|
attempts: RoutingAttempt[];
|
|
27
27
|
chosenProvider: string;
|
|
28
|
-
strategy: RoutingStrategy;
|
|
28
|
+
strategy: RoutingStrategy | 'successRate';
|
|
29
29
|
}
|
|
30
30
|
export type RoutingStrategy = 'cheapest' | 'fastest' | 'priority' | 'round-robin';
|
|
31
31
|
export interface FallbackConfig {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LedgerStore } from '../ledger';
|
|
2
|
+
import type { PgPoolLike } from './postgres';
|
|
3
|
+
export interface PostgresLedgerStoreOptions {
|
|
4
|
+
pool: PgPoolLike;
|
|
5
|
+
tableName?: string;
|
|
6
|
+
schema?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getCreateTableSql(tableName?: string, schema?: string): string;
|
|
9
|
+
export declare function createPostgresLedgerStore(opts: PostgresLedgerStoreOptions): LedgerStore;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCreateTableSql = getCreateTableSql;
|
|
4
|
+
exports.createPostgresLedgerStore = createPostgresLedgerStore;
|
|
5
|
+
function getCreateTableSql(tableName = 'paybridge_ledger', schema = 'public') {
|
|
6
|
+
const fullTableName = `${schema}.${tableName}`;
|
|
7
|
+
return `CREATE TABLE ${fullTableName} (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
timestamp TIMESTAMPTZ NOT NULL,
|
|
10
|
+
operation TEXT NOT NULL,
|
|
11
|
+
provider TEXT NOT NULL,
|
|
12
|
+
reference TEXT,
|
|
13
|
+
provider_id TEXT,
|
|
14
|
+
status TEXT NOT NULL,
|
|
15
|
+
amount NUMERIC,
|
|
16
|
+
currency TEXT,
|
|
17
|
+
duration_ms INTEGER,
|
|
18
|
+
error_code TEXT,
|
|
19
|
+
error_message TEXT,
|
|
20
|
+
metadata JSONB
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX idx_${tableName}_provider_timestamp
|
|
24
|
+
ON ${fullTableName} (provider, timestamp DESC);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX idx_${tableName}_reference
|
|
27
|
+
ON ${fullTableName} (reference) WHERE reference IS NOT NULL;
|
|
28
|
+
|
|
29
|
+
CREATE INDEX idx_${tableName}_status
|
|
30
|
+
ON ${fullTableName} (status);`;
|
|
31
|
+
}
|
|
32
|
+
function createPostgresLedgerStore(opts) {
|
|
33
|
+
const tableName = opts.tableName ?? 'paybridge_ledger';
|
|
34
|
+
const schema = opts.schema ?? 'public';
|
|
35
|
+
const fullTableName = `${schema}.${tableName}`;
|
|
36
|
+
return {
|
|
37
|
+
async append(entry) {
|
|
38
|
+
const sql = `INSERT INTO ${fullTableName} (
|
|
39
|
+
id, timestamp, operation, provider, reference, provider_id,
|
|
40
|
+
status, amount, currency, duration_ms, error_code, error_message, metadata
|
|
41
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`;
|
|
42
|
+
const params = [
|
|
43
|
+
entry.id,
|
|
44
|
+
entry.timestamp,
|
|
45
|
+
entry.operation,
|
|
46
|
+
entry.provider,
|
|
47
|
+
entry.reference ?? null,
|
|
48
|
+
entry.providerId ?? null,
|
|
49
|
+
entry.status,
|
|
50
|
+
entry.amount ?? null,
|
|
51
|
+
entry.currency ?? null,
|
|
52
|
+
entry.durationMs ?? null,
|
|
53
|
+
entry.errorCode ?? null,
|
|
54
|
+
entry.errorMessage ?? null,
|
|
55
|
+
entry.metadata ? JSON.stringify(entry.metadata) : null,
|
|
56
|
+
];
|
|
57
|
+
await opts.pool.query(sql, params);
|
|
58
|
+
},
|
|
59
|
+
async query(filter) {
|
|
60
|
+
const whereClauses = [];
|
|
61
|
+
const params = [];
|
|
62
|
+
let paramIndex = 1;
|
|
63
|
+
if (filter.reference !== undefined) {
|
|
64
|
+
whereClauses.push(`reference = $${paramIndex++}`);
|
|
65
|
+
params.push(filter.reference);
|
|
66
|
+
}
|
|
67
|
+
if (filter.provider !== undefined) {
|
|
68
|
+
whereClauses.push(`provider = $${paramIndex++}`);
|
|
69
|
+
params.push(filter.provider);
|
|
70
|
+
}
|
|
71
|
+
if (filter.status !== undefined) {
|
|
72
|
+
whereClauses.push(`status = $${paramIndex++}`);
|
|
73
|
+
params.push(filter.status);
|
|
74
|
+
}
|
|
75
|
+
if (filter.fromTimestamp !== undefined) {
|
|
76
|
+
whereClauses.push(`timestamp >= $${paramIndex++}`);
|
|
77
|
+
params.push(filter.fromTimestamp);
|
|
78
|
+
}
|
|
79
|
+
if (filter.toTimestamp !== undefined) {
|
|
80
|
+
whereClauses.push(`timestamp <= $${paramIndex++}`);
|
|
81
|
+
params.push(filter.toTimestamp);
|
|
82
|
+
}
|
|
83
|
+
const whereClause = whereClauses.length > 0 ? 'WHERE ' + whereClauses.join(' AND ') : '';
|
|
84
|
+
const limitClause = filter.limit !== undefined ? `LIMIT $${paramIndex++}` : '';
|
|
85
|
+
if (filter.limit !== undefined) {
|
|
86
|
+
params.push(filter.limit);
|
|
87
|
+
}
|
|
88
|
+
const sql = `SELECT
|
|
89
|
+
id, timestamp, operation, provider, reference, provider_id,
|
|
90
|
+
status, amount, currency, duration_ms, error_code, error_message, metadata
|
|
91
|
+
FROM ${fullTableName}
|
|
92
|
+
${whereClause}
|
|
93
|
+
ORDER BY timestamp DESC
|
|
94
|
+
${limitClause}`.trim();
|
|
95
|
+
const result = await opts.pool.query(sql, params);
|
|
96
|
+
return result.rows.map(row => ({
|
|
97
|
+
id: row.id,
|
|
98
|
+
timestamp: row.timestamp,
|
|
99
|
+
operation: row.operation,
|
|
100
|
+
provider: row.provider,
|
|
101
|
+
reference: row.reference ?? undefined,
|
|
102
|
+
providerId: row.provider_id ?? undefined,
|
|
103
|
+
status: row.status,
|
|
104
|
+
amount: row.amount !== null ? parseFloat(row.amount) : undefined,
|
|
105
|
+
currency: row.currency ?? undefined,
|
|
106
|
+
durationMs: row.duration_ms ?? undefined,
|
|
107
|
+
errorCode: row.error_code ?? undefined,
|
|
108
|
+
errorMessage: row.error_message ?? undefined,
|
|
109
|
+
metadata: row.metadata ?? undefined,
|
|
110
|
+
}));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
package/dist/strategies.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { PayBridge } from './index';
|
|
5
5
|
import { Currency } from './types';
|
|
6
6
|
import { RoutingStrategy } from './routing-types';
|
|
7
|
+
import type { LedgerStore } from './ledger';
|
|
7
8
|
export interface ProviderWithMeta {
|
|
8
9
|
instance: PayBridge;
|
|
9
10
|
weight?: number;
|
|
@@ -16,3 +17,19 @@ export interface StrategyContext {
|
|
|
16
17
|
export type Strategy = (providers: ProviderWithMeta[], context: StrategyContext, getRRIndex?: () => number) => ProviderWithMeta[];
|
|
17
18
|
export declare const strategies: Record<RoutingStrategy, Strategy>;
|
|
18
19
|
export declare function getStrategy(name: RoutingStrategy): Strategy;
|
|
20
|
+
export interface SuccessRateStrategyOptions {
|
|
21
|
+
ledger: LedgerStore;
|
|
22
|
+
windowMs?: number;
|
|
23
|
+
cacheTtlMs?: number;
|
|
24
|
+
minSampleSize?: number;
|
|
25
|
+
fallback?: 'cheapest' | 'fastest' | 'priority' | 'round-robin';
|
|
26
|
+
}
|
|
27
|
+
export interface SuccessRateStrategy {
|
|
28
|
+
order: (providers: ProviderWithMeta[]) => Promise<ProviderWithMeta[]>;
|
|
29
|
+
refresh: () => Promise<void>;
|
|
30
|
+
getRates: () => Map<string, {
|
|
31
|
+
successRate: number;
|
|
32
|
+
sampleSize: number;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createSuccessRateStrategy(opts: SuccessRateStrategyOptions): SuccessRateStrategy;
|
package/dist/strategies.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.strategies = void 0;
|
|
7
7
|
exports.getStrategy = getStrategy;
|
|
8
|
+
exports.createSuccessRateStrategy = createSuccessRateStrategy;
|
|
8
9
|
exports.strategies = {
|
|
9
10
|
cheapest: (providers, context) => {
|
|
10
11
|
return [...providers].sort((a, b) => {
|
|
@@ -42,3 +43,72 @@ exports.strategies = {
|
|
|
42
43
|
function getStrategy(name) {
|
|
43
44
|
return exports.strategies[name];
|
|
44
45
|
}
|
|
46
|
+
function createSuccessRateStrategy(opts) {
|
|
47
|
+
const windowMs = opts.windowMs ?? 24 * 60 * 60 * 1000;
|
|
48
|
+
const cacheTtlMs = opts.cacheTtlMs ?? 60 * 1000;
|
|
49
|
+
const minSampleSize = opts.minSampleSize ?? 10;
|
|
50
|
+
const fallbackName = opts.fallback ?? 'cheapest';
|
|
51
|
+
const fallbackStrategy = exports.strategies[fallbackName];
|
|
52
|
+
let cachedRates = new Map();
|
|
53
|
+
let lastRefreshTime = 0;
|
|
54
|
+
async function computeRates() {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const fromTimestamp = new Date(now - windowMs).toISOString();
|
|
57
|
+
const entries = await opts.ledger.query({
|
|
58
|
+
fromTimestamp,
|
|
59
|
+
});
|
|
60
|
+
const providerStats = new Map();
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!providerStats.has(entry.provider)) {
|
|
63
|
+
providerStats.set(entry.provider, { total: 0, success: 0 });
|
|
64
|
+
}
|
|
65
|
+
const stats = providerStats.get(entry.provider);
|
|
66
|
+
stats.total++;
|
|
67
|
+
if (entry.status === 'success') {
|
|
68
|
+
stats.success++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const rates = new Map();
|
|
72
|
+
for (const [provider, stats] of providerStats) {
|
|
73
|
+
rates.set(provider, {
|
|
74
|
+
successRate: stats.total > 0 ? stats.success / stats.total : 0,
|
|
75
|
+
sampleSize: stats.total,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return rates;
|
|
79
|
+
}
|
|
80
|
+
async function ensureFreshCache() {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (now - lastRefreshTime > cacheTtlMs) {
|
|
83
|
+
cachedRates = await computeRates();
|
|
84
|
+
lastRefreshTime = now;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
async order(providers) {
|
|
89
|
+
await ensureFreshCache();
|
|
90
|
+
const highConfidence = [];
|
|
91
|
+
const lowConfidence = [];
|
|
92
|
+
for (const provider of providers) {
|
|
93
|
+
const providerName = provider.instance.getProviderName();
|
|
94
|
+
const stats = cachedRates.get(providerName);
|
|
95
|
+
if (stats && stats.sampleSize >= minSampleSize) {
|
|
96
|
+
highConfidence.push({ provider, rate: stats.successRate });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
lowConfidence.push(provider);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
highConfidence.sort((a, b) => b.rate - a.rate);
|
|
103
|
+
const sortedLowConfidence = fallbackStrategy(lowConfidence, { amount: 0, currency: 'USD' });
|
|
104
|
+
return [...highConfidence.map(h => h.provider), ...sortedLowConfidence];
|
|
105
|
+
},
|
|
106
|
+
async refresh() {
|
|
107
|
+
cachedRates = await computeRates();
|
|
108
|
+
lastRefreshTime = Date.now();
|
|
109
|
+
},
|
|
110
|
+
getRates() {
|
|
111
|
+
return new Map(cachedRates);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paybridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -49,7 +49,10 @@
|
|
|
49
49
|
"command-line",
|
|
50
50
|
"drift-detection",
|
|
51
51
|
"monitoring",
|
|
52
|
-
"api-validation"
|
|
52
|
+
"api-validation",
|
|
53
|
+
"postgres",
|
|
54
|
+
"success-rate",
|
|
55
|
+
"intelligent-routing"
|
|
53
56
|
],
|
|
54
57
|
"author": "Kobie Wentzel",
|
|
55
58
|
"license": "MIT",
|