@nehorai/payments 0.1.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/LICENSE +21 -0
- package/dist/config/index.cjs +116 -0
- package/dist/config/index.cjs.map +1 -0
- package/dist/config/index.d.cts +125 -0
- package/dist/config/index.d.ts +125 -0
- package/dist/config/index.js +83 -0
- package/dist/config/index.js.map +1 -0
- package/dist/factory.cjs +807 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +96 -0
- package/dist/factory.d.ts +96 -0
- package/dist/factory.js +777 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.cjs +1341 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1260 -0
- package/dist/index.js.map +1 -0
- package/dist/payment-orchestrator-CPaLmDM5.d.ts +404 -0
- package/dist/payment-orchestrator-Co_X6T_V.d.cts +404 -0
- package/dist/payment-types-68W-PlGg.d.cts +211 -0
- package/dist/payment-types-68W-PlGg.d.ts +211 -0
- package/dist/providers/interfaces/index.cjs +19 -0
- package/dist/providers/interfaces/index.cjs.map +1 -0
- package/dist/providers/interfaces/index.d.cts +80 -0
- package/dist/providers/interfaces/index.d.ts +80 -0
- package/dist/providers/interfaces/index.js +1 -0
- package/dist/providers/interfaces/index.js.map +1 -0
- package/dist/repository/interfaces/index.cjs +19 -0
- package/dist/repository/interfaces/index.cjs.map +1 -0
- package/dist/repository/interfaces/index.d.cts +556 -0
- package/dist/repository/interfaces/index.d.ts +556 -0
- package/dist/repository/interfaces/index.js +1 -0
- package/dist/repository/interfaces/index.js.map +1 -0
- package/dist/routing-engine.interface-DJzGXor9.d.cts +194 -0
- package/dist/routing-engine.interface-h9_GmQ4b.d.ts +194 -0
- package/dist/services/index.cjs +806 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.cts +75 -0
- package/dist/services/index.d.ts +75 -0
- package/dist/services/index.js +763 -0
- package/dist/services/index.js.map +1 -0
- package/dist/state-machine-Cu6_qKnv.d.cts +109 -0
- package/dist/state-machine-Cu6_qKnv.d.ts +109 -0
- package/dist/types/index.cjs +173 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +127 -0
- package/dist/types/index.d.ts +127 -0
- package/dist/types/index.js +130 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.cjs +167 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +102 -0
- package/dist/utils/index.d.ts +102 -0
- package/dist/utils/index.js +127 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
// src/services/payment-orchestrator.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/config/payment-config.ts
|
|
5
|
+
function createConfigFromEnv(mapping) {
|
|
6
|
+
if (!mapping) {
|
|
7
|
+
return createPartialConfig({});
|
|
8
|
+
}
|
|
9
|
+
const providers = {};
|
|
10
|
+
for (const providerMapping of mapping.providers) {
|
|
11
|
+
const config = {};
|
|
12
|
+
let hasValue = false;
|
|
13
|
+
for (const [configKey, envVarName] of Object.entries(providerMapping.keys)) {
|
|
14
|
+
const value = process.env[envVarName];
|
|
15
|
+
if (value?.trim()) {
|
|
16
|
+
config[configKey] = value;
|
|
17
|
+
hasValue = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (providerMapping.requiredKeys) {
|
|
21
|
+
const allRequired = providerMapping.requiredKeys.every(
|
|
22
|
+
(key) => config[key] && typeof config[key] === "string" && config[key].trim()
|
|
23
|
+
);
|
|
24
|
+
if (allRequired) {
|
|
25
|
+
providers[providerMapping.providerName] = config;
|
|
26
|
+
}
|
|
27
|
+
} else if (hasValue) {
|
|
28
|
+
providers[providerMapping.providerName] = config;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const environment = process.env[mapping.environmentVar ?? "PAYMENT_ENVIRONMENT"] ?? "sandbox";
|
|
32
|
+
const defaultCurrency = process.env[mapping.defaultCurrencyVar ?? "DEFAULT_CURRENCY"] ?? "USD";
|
|
33
|
+
return {
|
|
34
|
+
providers,
|
|
35
|
+
environment,
|
|
36
|
+
defaultCurrency
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function createConfig(config) {
|
|
40
|
+
validateConfig(config);
|
|
41
|
+
return {
|
|
42
|
+
providers: { ...config.providers },
|
|
43
|
+
environment: config.environment,
|
|
44
|
+
defaultCurrency: config.defaultCurrency
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createPartialConfig(partial) {
|
|
48
|
+
return {
|
|
49
|
+
providers: partial.providers ?? {},
|
|
50
|
+
environment: partial.environment ?? "sandbox",
|
|
51
|
+
defaultCurrency: partial.defaultCurrency ?? "USD"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function getConfiguredProviders(config) {
|
|
55
|
+
const availability = {};
|
|
56
|
+
for (const [name, providerConfig] of Object.entries(config.providers)) {
|
|
57
|
+
availability[name] = providerConfig !== void 0 && Object.keys(providerConfig).length > 0;
|
|
58
|
+
}
|
|
59
|
+
return availability;
|
|
60
|
+
}
|
|
61
|
+
function getConfiguredProviderList(config) {
|
|
62
|
+
const availability = getConfiguredProviders(config);
|
|
63
|
+
return Object.entries(availability).filter(([, isAvailable]) => isAvailable).map(([name]) => name);
|
|
64
|
+
}
|
|
65
|
+
function isProductionReady(config) {
|
|
66
|
+
const providers = getConfiguredProviderList(config);
|
|
67
|
+
return config.environment === "production" && providers.length > 0;
|
|
68
|
+
}
|
|
69
|
+
function validateConfig(config) {
|
|
70
|
+
const providers = getConfiguredProviderList(config);
|
|
71
|
+
if (providers.length === 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Payment configuration error: At least one payment provider must be configured"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/services/circuit-breaker-storage.interface.ts
|
|
79
|
+
function createDefaultState(provider) {
|
|
80
|
+
return {
|
|
81
|
+
provider,
|
|
82
|
+
state: "closed",
|
|
83
|
+
failureCount: 0,
|
|
84
|
+
successCount: 0,
|
|
85
|
+
lastFailure: null,
|
|
86
|
+
openedAt: null,
|
|
87
|
+
nextRetryAt: null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function isCircuitOpen(state) {
|
|
91
|
+
return state?.state === "open";
|
|
92
|
+
}
|
|
93
|
+
function shouldAttemptHalfOpen(state) {
|
|
94
|
+
if (!state || state.state !== "open") return false;
|
|
95
|
+
if (!state.nextRetryAt) return false;
|
|
96
|
+
return Date.now() >= state.nextRetryAt.getTime();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/services/in-memory-storage.ts
|
|
100
|
+
var InMemoryCircuitBreakerStorage = class {
|
|
101
|
+
internalStates;
|
|
102
|
+
constructor() {
|
|
103
|
+
this.internalStates = /* @__PURE__ */ new Map();
|
|
104
|
+
}
|
|
105
|
+
async getState(provider) {
|
|
106
|
+
return this.internalStates.get(provider) ?? null;
|
|
107
|
+
}
|
|
108
|
+
async setState(provider, state) {
|
|
109
|
+
this.internalStates.set(provider, { ...state });
|
|
110
|
+
}
|
|
111
|
+
async getAllStates() {
|
|
112
|
+
return new Map(this.internalStates);
|
|
113
|
+
}
|
|
114
|
+
async deleteState(provider) {
|
|
115
|
+
this.internalStates.delete(provider);
|
|
116
|
+
}
|
|
117
|
+
async getOpenCircuits() {
|
|
118
|
+
const open = [];
|
|
119
|
+
for (const [provider, state] of this.internalStates) {
|
|
120
|
+
if (state.state === "open") {
|
|
121
|
+
open.push(provider);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return open;
|
|
125
|
+
}
|
|
126
|
+
async isHealthy() {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clear all stored states (useful for testing)
|
|
131
|
+
*/
|
|
132
|
+
clear() {
|
|
133
|
+
this.internalStates.clear();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get current state count (useful for testing/debugging)
|
|
137
|
+
*/
|
|
138
|
+
get size() {
|
|
139
|
+
return this.internalStates.size;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Synchronous state access (for backward compatibility with CircuitBreaker.isOpen)
|
|
143
|
+
*
|
|
144
|
+
* Note: Only available on InMemoryCircuitBreakerStorage.
|
|
145
|
+
* Database-backed storage cannot provide sync access.
|
|
146
|
+
*/
|
|
147
|
+
getStateSync(provider) {
|
|
148
|
+
return this.internalStates.get(provider) ?? null;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var defaultStorage = null;
|
|
152
|
+
function getInMemoryStorage() {
|
|
153
|
+
if (!defaultStorage) {
|
|
154
|
+
defaultStorage = new InMemoryCircuitBreakerStorage();
|
|
155
|
+
}
|
|
156
|
+
return defaultStorage;
|
|
157
|
+
}
|
|
158
|
+
function resetInMemoryStorage() {
|
|
159
|
+
if (defaultStorage) {
|
|
160
|
+
defaultStorage.clear();
|
|
161
|
+
}
|
|
162
|
+
defaultStorage = null;
|
|
163
|
+
}
|
|
164
|
+
async function migrateFromLegacyMap(legacyStates) {
|
|
165
|
+
const storage = new InMemoryCircuitBreakerStorage();
|
|
166
|
+
for (const [provider, state] of legacyStates) {
|
|
167
|
+
const record = {
|
|
168
|
+
provider: state.provider,
|
|
169
|
+
state: state.state,
|
|
170
|
+
failureCount: state.failureCount,
|
|
171
|
+
successCount: state.successCount,
|
|
172
|
+
lastFailure: state.lastFailure,
|
|
173
|
+
openedAt: state.openedAt,
|
|
174
|
+
nextRetryAt: state.nextRetryAt
|
|
175
|
+
};
|
|
176
|
+
await storage.setState(provider, record);
|
|
177
|
+
}
|
|
178
|
+
return storage;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/services/circuit-breaker.ts
|
|
182
|
+
var DEFAULT_CONFIG = {
|
|
183
|
+
failureThreshold: 5,
|
|
184
|
+
resetTimeoutMs: 6e4,
|
|
185
|
+
// 1 minute
|
|
186
|
+
halfOpenMaxRequests: 3
|
|
187
|
+
};
|
|
188
|
+
var CircuitBreaker = class {
|
|
189
|
+
config;
|
|
190
|
+
storage;
|
|
191
|
+
constructor(deps = {}) {
|
|
192
|
+
this.config = { ...DEFAULT_CONFIG, ...deps.config };
|
|
193
|
+
this.storage = deps.storage ?? new InMemoryCircuitBreakerStorage();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if a request can be executed for a provider
|
|
197
|
+
*/
|
|
198
|
+
async canExecute(provider) {
|
|
199
|
+
const state = await this.getState(provider);
|
|
200
|
+
switch (state.state) {
|
|
201
|
+
case "closed":
|
|
202
|
+
return true;
|
|
203
|
+
case "open":
|
|
204
|
+
if (state.nextRetryAt && Date.now() >= state.nextRetryAt.getTime()) {
|
|
205
|
+
await this.transitionTo(provider, "half_open");
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
case "half_open":
|
|
210
|
+
return state.failureCount < this.config.halfOpenMaxRequests;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Record a successful request
|
|
215
|
+
*/
|
|
216
|
+
async recordSuccess(provider) {
|
|
217
|
+
const state = await this.getState(provider);
|
|
218
|
+
if (state.state === "half_open") {
|
|
219
|
+
state.successCount++;
|
|
220
|
+
if (state.successCount >= this.config.halfOpenMaxRequests) {
|
|
221
|
+
await this.transitionTo(provider, "closed");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
} else if (state.state === "closed") {
|
|
225
|
+
state.failureCount = 0;
|
|
226
|
+
}
|
|
227
|
+
await this.storage.setState(provider, state);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Record a failed request
|
|
231
|
+
*/
|
|
232
|
+
async recordFailure(provider) {
|
|
233
|
+
const state = await this.getState(provider);
|
|
234
|
+
state.failureCount++;
|
|
235
|
+
state.lastFailure = /* @__PURE__ */ new Date();
|
|
236
|
+
if (state.state === "half_open") {
|
|
237
|
+
await this.transitionTo(provider, "open");
|
|
238
|
+
} else if (state.state === "closed") {
|
|
239
|
+
if (state.failureCount >= this.config.failureThreshold) {
|
|
240
|
+
await this.transitionTo(provider, "open");
|
|
241
|
+
} else {
|
|
242
|
+
await this.storage.setState(provider, state);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
await this.storage.setState(provider, state);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get current state for a provider
|
|
250
|
+
*/
|
|
251
|
+
async getState(provider) {
|
|
252
|
+
const stored = await this.storage.getState(provider);
|
|
253
|
+
return stored ?? createDefaultState(provider);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Check if circuit is open (provider unavailable)
|
|
257
|
+
*/
|
|
258
|
+
isOpen(provider) {
|
|
259
|
+
return this.isOpenSync(provider);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Async version of isOpen
|
|
263
|
+
*/
|
|
264
|
+
async isOpenAsync(provider) {
|
|
265
|
+
const state = await this.getState(provider);
|
|
266
|
+
return state.state === "open";
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Manually reset a provider's circuit
|
|
270
|
+
*/
|
|
271
|
+
async reset(provider) {
|
|
272
|
+
await this.storage.setState(provider, createDefaultState(provider));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get all providers with open circuits
|
|
276
|
+
*/
|
|
277
|
+
async getOpenCircuits() {
|
|
278
|
+
return this.storage.getOpenCircuits();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get the storage instance (useful for testing)
|
|
282
|
+
*/
|
|
283
|
+
getStorage() {
|
|
284
|
+
return this.storage;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get configuration (useful for testing)
|
|
288
|
+
*/
|
|
289
|
+
getConfig() {
|
|
290
|
+
return { ...this.config };
|
|
291
|
+
}
|
|
292
|
+
// ==========================================================================
|
|
293
|
+
// Private Methods
|
|
294
|
+
// ==========================================================================
|
|
295
|
+
async transitionTo(provider, newState) {
|
|
296
|
+
const state = await this.getState(provider);
|
|
297
|
+
const now = /* @__PURE__ */ new Date();
|
|
298
|
+
state.state = newState;
|
|
299
|
+
if (newState === "open") {
|
|
300
|
+
state.openedAt = now;
|
|
301
|
+
state.nextRetryAt = new Date(now.getTime() + this.config.resetTimeoutMs);
|
|
302
|
+
console.warn(
|
|
303
|
+
`[CIRCUIT_BREAKER] Circuit OPENED for ${provider}. Retry at: ${state.nextRetryAt.toISOString()}`
|
|
304
|
+
);
|
|
305
|
+
} else if (newState === "half_open") {
|
|
306
|
+
state.failureCount = 0;
|
|
307
|
+
state.successCount = 0;
|
|
308
|
+
console.info(`[CIRCUIT_BREAKER] Circuit HALF_OPEN for ${provider}`);
|
|
309
|
+
} else if (newState === "closed") {
|
|
310
|
+
state.failureCount = 0;
|
|
311
|
+
state.successCount = 0;
|
|
312
|
+
state.openedAt = null;
|
|
313
|
+
state.nextRetryAt = null;
|
|
314
|
+
console.info(`[CIRCUIT_BREAKER] Circuit CLOSED for ${provider}`);
|
|
315
|
+
}
|
|
316
|
+
await this.storage.setState(provider, state);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Synchronous check for backward compatibility
|
|
320
|
+
* Note: This always returns false for database-backed storage
|
|
321
|
+
*/
|
|
322
|
+
isOpenSync(provider) {
|
|
323
|
+
if (this.storage instanceof InMemoryCircuitBreakerStorage) {
|
|
324
|
+
const state = this.storage.getStateSync(provider);
|
|
325
|
+
return state?.state === "open";
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var circuitBreakerInstance = null;
|
|
331
|
+
var defaultStorage2 = null;
|
|
332
|
+
function getCircuitBreaker(config) {
|
|
333
|
+
if (!circuitBreakerInstance) {
|
|
334
|
+
if (!defaultStorage2) {
|
|
335
|
+
defaultStorage2 = new InMemoryCircuitBreakerStorage();
|
|
336
|
+
}
|
|
337
|
+
circuitBreakerInstance = new CircuitBreaker({
|
|
338
|
+
storage: defaultStorage2,
|
|
339
|
+
config
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return circuitBreakerInstance;
|
|
343
|
+
}
|
|
344
|
+
function createCircuitBreaker(deps = {}) {
|
|
345
|
+
return new CircuitBreaker(deps);
|
|
346
|
+
}
|
|
347
|
+
function resetCircuitBreaker() {
|
|
348
|
+
circuitBreakerInstance = null;
|
|
349
|
+
if (defaultStorage2) {
|
|
350
|
+
defaultStorage2.clear();
|
|
351
|
+
}
|
|
352
|
+
defaultStorage2 = null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/services/routing-engine.ts
|
|
356
|
+
function matchCardBinToRule(bin, rules) {
|
|
357
|
+
if (!bin || bin.length < 6) return null;
|
|
358
|
+
const binPrefix = bin.substring(0, 6);
|
|
359
|
+
for (const rule of rules) {
|
|
360
|
+
for (const range of rule.ranges) {
|
|
361
|
+
if (binPrefix >= range.start && binPrefix <= range.end) {
|
|
362
|
+
return { rule, issuer: range.issuer, country: range.country };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function getOptimalProviderFromPriorities(matchedBin, currency, requiresRecurring, availableProviders, priorities) {
|
|
369
|
+
const candidates = priorities.filter(
|
|
370
|
+
(p) => availableProviders.includes(p.provider)
|
|
371
|
+
);
|
|
372
|
+
if (candidates.length === 0) return availableProviders[0] ?? null;
|
|
373
|
+
const suitable = candidates.filter((p) => {
|
|
374
|
+
if (!p.supportsCurrency.includes(currency)) return false;
|
|
375
|
+
if (requiresRecurring && !p.supportsRecurring) return false;
|
|
376
|
+
return true;
|
|
377
|
+
});
|
|
378
|
+
if (suitable.length === 0) {
|
|
379
|
+
return candidates.sort((a, b) => a.priority - b.priority)[0]?.provider ?? null;
|
|
380
|
+
}
|
|
381
|
+
if (matchedBin) {
|
|
382
|
+
const localProviders = suitable.filter((p) => p.isLocalGateway);
|
|
383
|
+
if (localProviders.length > 0) {
|
|
384
|
+
return localProviders.sort((a, b) => a.priority - b.priority)[0].provider;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return suitable.sort((a, b) => a.priority - b.priority)[0].provider;
|
|
388
|
+
}
|
|
389
|
+
function getFallbackProvidersFromPriorities(primaryProvider, availableProviders, priorities) {
|
|
390
|
+
return priorities.filter(
|
|
391
|
+
(p) => p.provider !== primaryProvider && availableProviders.includes(p.provider)
|
|
392
|
+
).sort((a, b) => a.priority - b.priority).map((p) => p.provider);
|
|
393
|
+
}
|
|
394
|
+
function getProviderFeeFromPriorities(provider, priorities) {
|
|
395
|
+
const config = priorities.find((p) => p.provider === provider);
|
|
396
|
+
return config?.maxFeePercent ?? 3;
|
|
397
|
+
}
|
|
398
|
+
var RoutingEngine = class {
|
|
399
|
+
config;
|
|
400
|
+
circuitBreaker;
|
|
401
|
+
routingRules;
|
|
402
|
+
constructor(deps = {}) {
|
|
403
|
+
this.config = deps.config ?? createPartialConfig({});
|
|
404
|
+
this.circuitBreaker = deps.circuitBreaker ?? getCircuitBreaker();
|
|
405
|
+
this.routingRules = deps.routingRules ?? {};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Determine optimal provider for a transaction
|
|
409
|
+
*/
|
|
410
|
+
async route(context) {
|
|
411
|
+
const availability = getConfiguredProviders(this.config);
|
|
412
|
+
const availableProviders = this.getProviderList(availability);
|
|
413
|
+
if (availableProviders.length === 0) {
|
|
414
|
+
throw new Error("No payment providers configured");
|
|
415
|
+
}
|
|
416
|
+
if (context.savedPaymentMethodId && context.savedPaymentMethodProvider) {
|
|
417
|
+
return this.routeToSavedMethodProvider(context, availableProviders);
|
|
418
|
+
}
|
|
419
|
+
const binMatch = context.cardBin && this.routingRules.cardBinRules ? matchCardBinToRule(context.cardBin, this.routingRules.cardBinRules) : null;
|
|
420
|
+
const currencyRule = this.routingRules.currencyRules?.find(
|
|
421
|
+
(r) => r.currency === context.amount.currency
|
|
422
|
+
);
|
|
423
|
+
const healthyProviders = await this.getHealthyProviders(availableProviders);
|
|
424
|
+
const availableHealthy = availableProviders.filter((p) => healthyProviders.includes(p));
|
|
425
|
+
const effectiveProviders = availableHealthy.length > 0 ? availableHealthy : availableProviders;
|
|
426
|
+
if (binMatch && effectiveProviders.includes(binMatch.rule.preferredProvider)) {
|
|
427
|
+
const provider2 = binMatch.rule.preferredProvider;
|
|
428
|
+
const fallbacks2 = this.getFallbackProviders(provider2, availableProviders);
|
|
429
|
+
const feePercent = this.getProviderFee(provider2);
|
|
430
|
+
return {
|
|
431
|
+
provider: provider2,
|
|
432
|
+
reason: binMatch.issuer ? `Card (${binMatch.issuer}) matched BIN rule, routed to preferred provider` : "Card matched BIN rule, routed to preferred provider",
|
|
433
|
+
fallbackProviders: fallbacks2,
|
|
434
|
+
estimatedFeePercent: feePercent,
|
|
435
|
+
metadata: {
|
|
436
|
+
matchedBinRule: true,
|
|
437
|
+
cardIssuer: binMatch.issuer,
|
|
438
|
+
cardCountry: binMatch.country
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (currencyRule && effectiveProviders.includes(currencyRule.preferredProvider)) {
|
|
443
|
+
const provider2 = currencyRule.preferredProvider;
|
|
444
|
+
const fallbacks2 = this.getFallbackProviders(provider2, availableProviders);
|
|
445
|
+
const feePercent = this.getProviderFee(provider2);
|
|
446
|
+
return {
|
|
447
|
+
provider: provider2,
|
|
448
|
+
reason: `Currency ${context.amount.currency} routed to preferred provider`,
|
|
449
|
+
fallbackProviders: fallbacks2,
|
|
450
|
+
estimatedFeePercent: feePercent
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const priorities = this.routingRules.providerPriorities;
|
|
454
|
+
if (priorities && priorities.length > 0) {
|
|
455
|
+
const provider2 = getOptimalProviderFromPriorities(
|
|
456
|
+
!!binMatch,
|
|
457
|
+
context.amount.currency,
|
|
458
|
+
context.isRecurring,
|
|
459
|
+
effectiveProviders,
|
|
460
|
+
priorities
|
|
461
|
+
);
|
|
462
|
+
if (provider2) {
|
|
463
|
+
const fallbacks2 = getFallbackProvidersFromPriorities(provider2, availableProviders, priorities);
|
|
464
|
+
const feePercent = getProviderFeeFromPriorities(provider2, priorities);
|
|
465
|
+
return {
|
|
466
|
+
provider: provider2,
|
|
467
|
+
reason: `Selected ${provider2} based on priority rules`,
|
|
468
|
+
fallbackProviders: fallbacks2,
|
|
469
|
+
estimatedFeePercent: feePercent,
|
|
470
|
+
metadata: {
|
|
471
|
+
matchedBinRule: !!binMatch,
|
|
472
|
+
cardIssuer: binMatch?.issuer,
|
|
473
|
+
cardCountry: binMatch?.country
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const provider = effectiveProviders[0];
|
|
479
|
+
const fallbacks = effectiveProviders.slice(1);
|
|
480
|
+
return {
|
|
481
|
+
provider,
|
|
482
|
+
reason: `Default routing to ${provider}`,
|
|
483
|
+
fallbackProviders: fallbacks,
|
|
484
|
+
estimatedFeePercent: this.getProviderFee(provider)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Get next provider after a failure
|
|
489
|
+
*/
|
|
490
|
+
async getFailoverProvider(failedProvider, _context) {
|
|
491
|
+
const availability = getConfiguredProviders(this.config);
|
|
492
|
+
const availableProviders = this.getProviderList(availability);
|
|
493
|
+
const healthyProviders = await this.getHealthyProviders(availableProviders);
|
|
494
|
+
const fallbacks = this.getFallbackProviders(failedProvider, availableProviders);
|
|
495
|
+
for (const provider of fallbacks) {
|
|
496
|
+
if (healthyProviders.includes(provider)) {
|
|
497
|
+
return provider;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return fallbacks[0] ?? null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Check if a card BIN matches any configured rule
|
|
504
|
+
*/
|
|
505
|
+
matchCardBin(bin) {
|
|
506
|
+
if (!this.routingRules.cardBinRules) return false;
|
|
507
|
+
return matchCardBinToRule(bin, this.routingRules.cardBinRules) !== null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get all available (healthy) providers
|
|
511
|
+
*/
|
|
512
|
+
async getAvailableProviders() {
|
|
513
|
+
const availability = getConfiguredProviders(this.config);
|
|
514
|
+
const configured = this.getProviderList(availability);
|
|
515
|
+
return this.getHealthyProviders(configured);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Quick recommendation without full context
|
|
519
|
+
*/
|
|
520
|
+
async getQuickRecommendation(currency, isRecurring) {
|
|
521
|
+
const availability = getConfiguredProviders(this.config);
|
|
522
|
+
const available = this.getProviderList(availability);
|
|
523
|
+
if (available.length === 0) return null;
|
|
524
|
+
const currencyRule = this.routingRules.currencyRules?.find(
|
|
525
|
+
(r) => r.currency === currency
|
|
526
|
+
);
|
|
527
|
+
if (currencyRule && available.includes(currencyRule.preferredProvider)) {
|
|
528
|
+
return currencyRule.preferredProvider;
|
|
529
|
+
}
|
|
530
|
+
const priorities = this.routingRules.providerPriorities;
|
|
531
|
+
if (priorities && priorities.length > 0) {
|
|
532
|
+
return getOptimalProviderFromPriorities(false, currency, isRecurring, available, priorities);
|
|
533
|
+
}
|
|
534
|
+
return available[0] ?? null;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get current configuration (for testing/debugging)
|
|
538
|
+
*/
|
|
539
|
+
getConfig() {
|
|
540
|
+
return this.config;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Get circuit breaker instance (for testing/debugging)
|
|
544
|
+
*/
|
|
545
|
+
getCircuitBreaker() {
|
|
546
|
+
return this.circuitBreaker;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get routing rules (for testing/debugging)
|
|
550
|
+
*/
|
|
551
|
+
getRoutingRules() {
|
|
552
|
+
return this.routingRules;
|
|
553
|
+
}
|
|
554
|
+
// ==========================================================================
|
|
555
|
+
// Private Methods
|
|
556
|
+
// ==========================================================================
|
|
557
|
+
getProviderList(availability) {
|
|
558
|
+
const providers = [];
|
|
559
|
+
for (const [name, isAvailable] of Object.entries(availability)) {
|
|
560
|
+
if (isAvailable) providers.push(name);
|
|
561
|
+
}
|
|
562
|
+
return providers;
|
|
563
|
+
}
|
|
564
|
+
async getHealthyProviders(providers) {
|
|
565
|
+
const healthy = [];
|
|
566
|
+
for (const provider of providers) {
|
|
567
|
+
const isOpen = await this.circuitBreaker.isOpenAsync(provider);
|
|
568
|
+
if (!isOpen) {
|
|
569
|
+
healthy.push(provider);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return healthy;
|
|
573
|
+
}
|
|
574
|
+
routeToSavedMethodProvider(context, availableProviders) {
|
|
575
|
+
const provider = context.savedPaymentMethodProvider;
|
|
576
|
+
if (!availableProviders.includes(provider)) {
|
|
577
|
+
throw new Error(`Saved payment method provider '${provider}' is not available`);
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
provider,
|
|
581
|
+
reason: "Using saved payment method provider",
|
|
582
|
+
fallbackProviders: [],
|
|
583
|
+
estimatedFeePercent: this.getProviderFee(provider)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
getFallbackProviders(primaryProvider, availableProviders) {
|
|
587
|
+
const priorities = this.routingRules.providerPriorities;
|
|
588
|
+
if (priorities && priorities.length > 0) {
|
|
589
|
+
return getFallbackProvidersFromPriorities(primaryProvider, availableProviders, priorities);
|
|
590
|
+
}
|
|
591
|
+
return availableProviders.filter((p) => p !== primaryProvider);
|
|
592
|
+
}
|
|
593
|
+
getProviderFee(provider) {
|
|
594
|
+
const priorities = this.routingRules.providerPriorities;
|
|
595
|
+
if (priorities && priorities.length > 0) {
|
|
596
|
+
return getProviderFeeFromPriorities(provider, priorities);
|
|
597
|
+
}
|
|
598
|
+
return 3;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
var routingEngineInstance = null;
|
|
602
|
+
function getRoutingEngine() {
|
|
603
|
+
if (!routingEngineInstance) {
|
|
604
|
+
routingEngineInstance = new RoutingEngine();
|
|
605
|
+
}
|
|
606
|
+
return routingEngineInstance;
|
|
607
|
+
}
|
|
608
|
+
function createRoutingEngine(deps = {}) {
|
|
609
|
+
return new RoutingEngine(deps);
|
|
610
|
+
}
|
|
611
|
+
function resetRoutingEngine() {
|
|
612
|
+
routingEngineInstance = null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/services/payment-orchestrator.ts
|
|
616
|
+
var PaymentOrchestrator = class {
|
|
617
|
+
providers;
|
|
618
|
+
routingEngine;
|
|
619
|
+
circuitBreaker;
|
|
620
|
+
constructor(deps) {
|
|
621
|
+
this.providers = deps.providers;
|
|
622
|
+
this.routingEngine = deps.routingEngine ?? getRoutingEngine();
|
|
623
|
+
this.circuitBreaker = deps.circuitBreaker ?? getCircuitBreaker();
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Initiate a new payment
|
|
627
|
+
*/
|
|
628
|
+
async initiatePayment(params) {
|
|
629
|
+
const internalPaymentId = `pay_${randomUUID()}`;
|
|
630
|
+
const idempotencyKey = `idem_${randomUUID()}`;
|
|
631
|
+
try {
|
|
632
|
+
const routing = await this.routingEngine.route({
|
|
633
|
+
userId: params.userId,
|
|
634
|
+
amount: params.amount,
|
|
635
|
+
cardBin: params.cardBin,
|
|
636
|
+
preferredProvider: params.preferredProvider,
|
|
637
|
+
isRecurring: params.transactionType !== "one_time_purchase"
|
|
638
|
+
});
|
|
639
|
+
const provider = this.getProvider(routing.provider);
|
|
640
|
+
if (!provider) {
|
|
641
|
+
return this.tryFailover(params, routing, internalPaymentId);
|
|
642
|
+
}
|
|
643
|
+
if (!await this.circuitBreaker.canExecute(routing.provider)) {
|
|
644
|
+
return this.tryFailover(params, routing, internalPaymentId);
|
|
645
|
+
}
|
|
646
|
+
const result = await provider.createPaymentIntent({
|
|
647
|
+
amount: params.amount,
|
|
648
|
+
userId: params.userId,
|
|
649
|
+
idempotencyKey,
|
|
650
|
+
description: params.description,
|
|
651
|
+
metadata: params.metadata,
|
|
652
|
+
returnUrl: params.returnUrl,
|
|
653
|
+
captureMethod: params.autoCapture ? "automatic" : "manual"
|
|
654
|
+
});
|
|
655
|
+
if (!result.success) {
|
|
656
|
+
await this.circuitBreaker.recordFailure(routing.provider);
|
|
657
|
+
return this.tryFailover(params, routing, internalPaymentId);
|
|
658
|
+
}
|
|
659
|
+
await this.circuitBreaker.recordSuccess(routing.provider);
|
|
660
|
+
return {
|
|
661
|
+
success: true,
|
|
662
|
+
transactionId: result.providerIntentId,
|
|
663
|
+
internalPaymentId,
|
|
664
|
+
provider: routing.provider,
|
|
665
|
+
clientSecret: result.clientSecret,
|
|
666
|
+
redirectUrl: result.redirectUrl
|
|
667
|
+
};
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return {
|
|
670
|
+
success: false,
|
|
671
|
+
transactionId: "",
|
|
672
|
+
internalPaymentId,
|
|
673
|
+
provider: "unknown",
|
|
674
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Confirm a payment after user authorization
|
|
680
|
+
*/
|
|
681
|
+
async confirmPayment(params) {
|
|
682
|
+
const provider = this.getProvider(params.provider);
|
|
683
|
+
if (!provider) {
|
|
684
|
+
return {
|
|
685
|
+
success: false,
|
|
686
|
+
transactionId: params.transactionId,
|
|
687
|
+
status: "failed",
|
|
688
|
+
error: `Provider ${params.provider} not available`
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const result = await provider.authorize({
|
|
692
|
+
providerIntentId: params.providerIntentId,
|
|
693
|
+
idempotencyKey: `auth_${params.internalPaymentId}`
|
|
694
|
+
});
|
|
695
|
+
if (!result.success) {
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
transactionId: params.transactionId,
|
|
699
|
+
status: "failed",
|
|
700
|
+
error: result.error
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
success: true,
|
|
705
|
+
transactionId: params.transactionId,
|
|
706
|
+
status: result.status ?? "authorized"
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Capture an authorized payment (J5 completion)
|
|
711
|
+
*/
|
|
712
|
+
async capturePayment(params) {
|
|
713
|
+
const provider = this.getProvider(params.provider);
|
|
714
|
+
if (!provider) {
|
|
715
|
+
return {
|
|
716
|
+
success: false,
|
|
717
|
+
status: "failed",
|
|
718
|
+
error: `Provider ${params.provider} not available`
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const result = await provider.capture({
|
|
722
|
+
providerIntentId: params.providerIntentId,
|
|
723
|
+
authorizationCode: params.providerIntentId,
|
|
724
|
+
amount: params.amount,
|
|
725
|
+
idempotencyKey: `cap_${params.transactionId}`
|
|
726
|
+
});
|
|
727
|
+
if (!result.success) {
|
|
728
|
+
return {
|
|
729
|
+
success: false,
|
|
730
|
+
status: "failed",
|
|
731
|
+
error: result.error
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
success: true,
|
|
736
|
+
status: "captured",
|
|
737
|
+
capturedAmount: result.capturedAmount
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Get the routing engine (for testing/debugging)
|
|
742
|
+
*/
|
|
743
|
+
getRoutingEngine() {
|
|
744
|
+
return this.routingEngine;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Get the circuit breaker (for testing/debugging)
|
|
748
|
+
*/
|
|
749
|
+
getCircuitBreaker() {
|
|
750
|
+
return this.circuitBreaker;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Get available providers (for testing/debugging)
|
|
754
|
+
*/
|
|
755
|
+
getProviders() {
|
|
756
|
+
return new Map(this.providers);
|
|
757
|
+
}
|
|
758
|
+
// ==========================================================================
|
|
759
|
+
// Private Methods
|
|
760
|
+
// ==========================================================================
|
|
761
|
+
getProvider(name) {
|
|
762
|
+
return this.providers.get(name);
|
|
763
|
+
}
|
|
764
|
+
async tryFailover(params, routing, internalPaymentId) {
|
|
765
|
+
for (const fallback of routing.fallbackProviders) {
|
|
766
|
+
const provider = this.getProvider(fallback);
|
|
767
|
+
if (!provider) continue;
|
|
768
|
+
if (!await this.circuitBreaker.canExecute(fallback)) continue;
|
|
769
|
+
const result = await provider.createPaymentIntent({
|
|
770
|
+
amount: params.amount,
|
|
771
|
+
userId: params.userId,
|
|
772
|
+
idempotencyKey: `idem_${randomUUID()}`,
|
|
773
|
+
description: params.description,
|
|
774
|
+
metadata: params.metadata,
|
|
775
|
+
returnUrl: params.returnUrl,
|
|
776
|
+
captureMethod: params.autoCapture ? "automatic" : "manual"
|
|
777
|
+
});
|
|
778
|
+
if (result.success) {
|
|
779
|
+
await this.circuitBreaker.recordSuccess(fallback);
|
|
780
|
+
return {
|
|
781
|
+
success: true,
|
|
782
|
+
transactionId: result.providerIntentId,
|
|
783
|
+
internalPaymentId,
|
|
784
|
+
provider: fallback,
|
|
785
|
+
clientSecret: result.clientSecret
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
await this.circuitBreaker.recordFailure(fallback);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
transactionId: "",
|
|
793
|
+
internalPaymentId,
|
|
794
|
+
provider: routing.provider,
|
|
795
|
+
error: "All payment providers failed"
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
function createPaymentOrchestrator(deps) {
|
|
800
|
+
return new PaymentOrchestrator(deps);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/factory.ts
|
|
804
|
+
function createPaymentServices(options) {
|
|
805
|
+
const config = options.config ?? createPartialConfig({});
|
|
806
|
+
const providers = options.providers;
|
|
807
|
+
const webhookHandlers = options.webhookHandlers ?? /* @__PURE__ */ new Map();
|
|
808
|
+
if (providers.size === 0) {
|
|
809
|
+
throw new Error("No payment providers provided. Pass at least one provider instance.");
|
|
810
|
+
}
|
|
811
|
+
const circuitBreakerStorage = options.circuitBreakerStorage ?? new InMemoryCircuitBreakerStorage();
|
|
812
|
+
const circuitBreaker = createCircuitBreaker({
|
|
813
|
+
storage: circuitBreakerStorage,
|
|
814
|
+
config: options.circuitBreaker
|
|
815
|
+
});
|
|
816
|
+
const routingEngine = createRoutingEngine({
|
|
817
|
+
config,
|
|
818
|
+
circuitBreaker,
|
|
819
|
+
routingRules: options.routingRules
|
|
820
|
+
});
|
|
821
|
+
const orchestrator = new PaymentOrchestrator({
|
|
822
|
+
providers,
|
|
823
|
+
routingEngine,
|
|
824
|
+
circuitBreaker
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
orchestrator,
|
|
828
|
+
routingEngine,
|
|
829
|
+
circuitBreaker,
|
|
830
|
+
providers,
|
|
831
|
+
webhookHandlers,
|
|
832
|
+
config
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function registerProvider(services, name, provider, webhookHandler) {
|
|
836
|
+
const newProviders = new Map(services.providers);
|
|
837
|
+
newProviders.set(name, provider);
|
|
838
|
+
const newWebhookHandlers = new Map(services.webhookHandlers);
|
|
839
|
+
if (webhookHandler) {
|
|
840
|
+
newWebhookHandlers.set(name, webhookHandler);
|
|
841
|
+
}
|
|
842
|
+
const orchestrator = new PaymentOrchestrator({
|
|
843
|
+
providers: newProviders,
|
|
844
|
+
routingEngine: services.routingEngine,
|
|
845
|
+
circuitBreaker: services.circuitBreaker
|
|
846
|
+
});
|
|
847
|
+
return {
|
|
848
|
+
...services,
|
|
849
|
+
orchestrator,
|
|
850
|
+
providers: newProviders,
|
|
851
|
+
webhookHandlers: newWebhookHandlers
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
var servicesInstance = null;
|
|
855
|
+
function getPaymentServices(config) {
|
|
856
|
+
if (!servicesInstance) {
|
|
857
|
+
if (!config) {
|
|
858
|
+
throw new Error("PaymentServices not initialized. Call with config on first use.");
|
|
859
|
+
}
|
|
860
|
+
servicesInstance = createPaymentServices(config);
|
|
861
|
+
}
|
|
862
|
+
return servicesInstance;
|
|
863
|
+
}
|
|
864
|
+
function resetPaymentServices() {
|
|
865
|
+
servicesInstance = null;
|
|
866
|
+
resetCircuitBreaker();
|
|
867
|
+
resetRoutingEngine();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/types/state-machine.ts
|
|
871
|
+
var TERMINAL_STATES = [
|
|
872
|
+
"voided",
|
|
873
|
+
"failed",
|
|
874
|
+
"expired",
|
|
875
|
+
"fully_refunded"
|
|
876
|
+
];
|
|
877
|
+
var SUCCESS_STATES = [
|
|
878
|
+
"captured",
|
|
879
|
+
"partially_refunded",
|
|
880
|
+
"fully_refunded"
|
|
881
|
+
];
|
|
882
|
+
var HOLD_STATES = [
|
|
883
|
+
"authorized"
|
|
884
|
+
];
|
|
885
|
+
var VALID_TRANSITIONS = {
|
|
886
|
+
created: ["INITIATE", "AUTHORIZE_PENDING", "AUTHORIZE_FAILED"],
|
|
887
|
+
pending_authorization: ["AUTHORIZE_SUCCESS", "AUTHORIZE_FAILED", "EXPIRED"],
|
|
888
|
+
authorized: ["CAPTURE_STARTED", "VOID_SUCCESS", "VOID_FAILED", "EXPIRED"],
|
|
889
|
+
capturing: ["CAPTURE_SUCCESS", "CAPTURE_FAILED"],
|
|
890
|
+
captured: ["PARTIAL_REFUND", "FULL_REFUND"],
|
|
891
|
+
voided: [],
|
|
892
|
+
// Terminal state
|
|
893
|
+
failed: [],
|
|
894
|
+
// Terminal state
|
|
895
|
+
expired: [],
|
|
896
|
+
// Terminal state
|
|
897
|
+
partially_refunded: ["PARTIAL_REFUND", "FULL_REFUND"],
|
|
898
|
+
fully_refunded: []
|
|
899
|
+
// Terminal state
|
|
900
|
+
};
|
|
901
|
+
var EVENT_TO_STATE = {
|
|
902
|
+
INITIATE: "pending_authorization",
|
|
903
|
+
AUTHORIZE_PENDING: "pending_authorization",
|
|
904
|
+
AUTHORIZE_SUCCESS: "authorized",
|
|
905
|
+
AUTHORIZE_FAILED: "failed",
|
|
906
|
+
CAPTURE_STARTED: "capturing",
|
|
907
|
+
CAPTURE_SUCCESS: "captured",
|
|
908
|
+
CAPTURE_FAILED: "failed",
|
|
909
|
+
VOID_SUCCESS: "voided",
|
|
910
|
+
VOID_FAILED: "authorized",
|
|
911
|
+
// Remain authorized if void fails
|
|
912
|
+
EXPIRED: "expired",
|
|
913
|
+
PARTIAL_REFUND: "partially_refunded",
|
|
914
|
+
FULL_REFUND: "fully_refunded"
|
|
915
|
+
};
|
|
916
|
+
function canTransition(currentStatus, event) {
|
|
917
|
+
return VALID_TRANSITIONS[currentStatus].includes(event);
|
|
918
|
+
}
|
|
919
|
+
function getNextStatus(currentStatus, event) {
|
|
920
|
+
if (!canTransition(currentStatus, event)) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
return EVENT_TO_STATE[event];
|
|
924
|
+
}
|
|
925
|
+
function isTerminalState(status) {
|
|
926
|
+
return TERMINAL_STATES.includes(status);
|
|
927
|
+
}
|
|
928
|
+
function isSuccessState(status) {
|
|
929
|
+
return SUCCESS_STATES.includes(status);
|
|
930
|
+
}
|
|
931
|
+
function isHoldState(status) {
|
|
932
|
+
return HOLD_STATES.includes(status);
|
|
933
|
+
}
|
|
934
|
+
function canRefund(status) {
|
|
935
|
+
return status === "captured" || status === "partially_refunded";
|
|
936
|
+
}
|
|
937
|
+
function canCapture(status) {
|
|
938
|
+
return status === "authorized";
|
|
939
|
+
}
|
|
940
|
+
function canVoid(status) {
|
|
941
|
+
return status === "authorized";
|
|
942
|
+
}
|
|
943
|
+
function attemptTransition(currentStatus, event) {
|
|
944
|
+
const nextStatus = getNextStatus(currentStatus, event);
|
|
945
|
+
if (nextStatus === null) {
|
|
946
|
+
return {
|
|
947
|
+
success: false,
|
|
948
|
+
previousStatus: currentStatus,
|
|
949
|
+
newStatus: currentStatus,
|
|
950
|
+
event,
|
|
951
|
+
error: `Invalid transition: ${currentStatus} -> ${event}`
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
success: true,
|
|
956
|
+
previousStatus: currentStatus,
|
|
957
|
+
newStatus: nextStatus,
|
|
958
|
+
event
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
var DEFAULT_AUTH_HOLD_DAYS = 7;
|
|
962
|
+
function calculateCaptureDeadline(authorizedAt, holdDays = DEFAULT_AUTH_HOLD_DAYS) {
|
|
963
|
+
const deadline = new Date(authorizedAt);
|
|
964
|
+
deadline.setDate(deadline.getDate() + holdDays);
|
|
965
|
+
return deadline;
|
|
966
|
+
}
|
|
967
|
+
function isAuthorizationExpired(captureDeadline) {
|
|
968
|
+
return /* @__PURE__ */ new Date() > captureDeadline;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/utils/idempotency.ts
|
|
972
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
973
|
+
function generateInternalPaymentId() {
|
|
974
|
+
return `pay_${randomUUID2()}`;
|
|
975
|
+
}
|
|
976
|
+
function generateIdempotencyKey() {
|
|
977
|
+
return `idem_${randomUUID2()}`;
|
|
978
|
+
}
|
|
979
|
+
function generateDeterministicKey(...components) {
|
|
980
|
+
const data = components.join(":");
|
|
981
|
+
const hash = createHash("sha256").update(data).digest("hex");
|
|
982
|
+
return `idem_${hash.substring(0, 32)}`;
|
|
983
|
+
}
|
|
984
|
+
function generateOperationKey(operation, transactionId) {
|
|
985
|
+
return `${operation}_${transactionId}`;
|
|
986
|
+
}
|
|
987
|
+
function isValidIdempotencyKey(key) {
|
|
988
|
+
return /^idem_[a-f0-9-]{32,36}$/.test(key);
|
|
989
|
+
}
|
|
990
|
+
function isValidInternalPaymentId(id) {
|
|
991
|
+
return /^pay_[a-f0-9-]{36}$/.test(id);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/utils/signature-verification.ts
|
|
995
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
996
|
+
function verifyStripeStyleSignature(payload, signature, secret, tolerance) {
|
|
997
|
+
try {
|
|
998
|
+
const elements = signature.split(",");
|
|
999
|
+
const timestamp = elements.find((e) => e.startsWith("t="))?.slice(2);
|
|
1000
|
+
const sig = elements.find((e) => e.startsWith("v1="))?.slice(3);
|
|
1001
|
+
if (!timestamp || !sig) {
|
|
1002
|
+
return { valid: false, error: "Invalid signature format" };
|
|
1003
|
+
}
|
|
1004
|
+
const timestampNum = parseInt(timestamp, 10);
|
|
1005
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1006
|
+
if (Math.abs(now - timestampNum) > tolerance) {
|
|
1007
|
+
return { valid: false, error: "Timestamp outside tolerance" };
|
|
1008
|
+
}
|
|
1009
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
1010
|
+
const expectedSig = createHmac("sha256", secret).update(signedPayload).digest("hex");
|
|
1011
|
+
const valid = timingSafeEqual(
|
|
1012
|
+
Buffer.from(sig),
|
|
1013
|
+
Buffer.from(expectedSig)
|
|
1014
|
+
);
|
|
1015
|
+
return { valid };
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
return {
|
|
1018
|
+
valid: false,
|
|
1019
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function verifySortedFieldsHmacSignature(payload, signature, secret, _tolerance) {
|
|
1024
|
+
try {
|
|
1025
|
+
const data = JSON.parse(payload);
|
|
1026
|
+
const sortedKeys = Object.keys(data).sort();
|
|
1027
|
+
const sortedPayload = sortedKeys.map((k) => `${k}=${data[k]}`).join("&");
|
|
1028
|
+
const expectedSig = createHmac("sha256", secret).update(sortedPayload).digest("hex");
|
|
1029
|
+
const valid = timingSafeEqual(
|
|
1030
|
+
Buffer.from(signature.toLowerCase()),
|
|
1031
|
+
Buffer.from(expectedSig)
|
|
1032
|
+
);
|
|
1033
|
+
return { valid };
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
return {
|
|
1036
|
+
valid: false,
|
|
1037
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function verifyHmacSha256Signature(payload, signature, secret, _tolerance) {
|
|
1042
|
+
try {
|
|
1043
|
+
const expectedSig = createHmac("sha256", secret).update(payload).digest("hex");
|
|
1044
|
+
const valid = timingSafeEqual(
|
|
1045
|
+
Buffer.from(signature.toLowerCase()),
|
|
1046
|
+
Buffer.from(expectedSig.toLowerCase())
|
|
1047
|
+
);
|
|
1048
|
+
return { valid };
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
return {
|
|
1051
|
+
valid: false,
|
|
1052
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
var verifierRegistry = /* @__PURE__ */ new Map();
|
|
1057
|
+
function registerSignatureVerifier(provider, verifier) {
|
|
1058
|
+
verifierRegistry.set(provider, verifier);
|
|
1059
|
+
}
|
|
1060
|
+
function getSignatureVerifier(provider) {
|
|
1061
|
+
return verifierRegistry.get(provider);
|
|
1062
|
+
}
|
|
1063
|
+
function verifyWebhookSignature(params) {
|
|
1064
|
+
const { provider, payload, signature, secret, tolerance = 300 } = params;
|
|
1065
|
+
if (!signature || !secret) {
|
|
1066
|
+
return { valid: false, error: "Missing signature or secret" };
|
|
1067
|
+
}
|
|
1068
|
+
const verifier = verifierRegistry.get(provider);
|
|
1069
|
+
if (verifier) {
|
|
1070
|
+
return verifier(payload, signature, secret, tolerance);
|
|
1071
|
+
}
|
|
1072
|
+
return verifyHmacSha256Signature(payload, signature, secret, tolerance);
|
|
1073
|
+
}
|
|
1074
|
+
function getSignatureHeaderName(provider) {
|
|
1075
|
+
return `x-${provider}-signature`;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/repository/memory/in-memory-transaction.ts
|
|
1079
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1080
|
+
var InMemoryTransactionRepository = class {
|
|
1081
|
+
transactions = /* @__PURE__ */ new Map();
|
|
1082
|
+
async findById(id) {
|
|
1083
|
+
return this.transactions.get(id) ?? null;
|
|
1084
|
+
}
|
|
1085
|
+
async create(data) {
|
|
1086
|
+
const now = /* @__PURE__ */ new Date();
|
|
1087
|
+
const transaction = {
|
|
1088
|
+
id: randomUUID3(),
|
|
1089
|
+
internalPaymentId: data.internalPaymentId,
|
|
1090
|
+
idempotencyKey: data.idempotencyKey ?? null,
|
|
1091
|
+
userId: data.userId,
|
|
1092
|
+
transactionType: data.transactionType,
|
|
1093
|
+
status: data.status ?? "created",
|
|
1094
|
+
amountMinor: data.amountMinor,
|
|
1095
|
+
currency: data.currency,
|
|
1096
|
+
originalAmountMinor: null,
|
|
1097
|
+
originalCurrency: null,
|
|
1098
|
+
currencyConversionRate: null,
|
|
1099
|
+
provider: data.provider,
|
|
1100
|
+
providerTransactionId: null,
|
|
1101
|
+
providerAuthorizationCode: null,
|
|
1102
|
+
providerMetadata: null,
|
|
1103
|
+
authorizedAt: null,
|
|
1104
|
+
capturedAt: null,
|
|
1105
|
+
voidedAt: null,
|
|
1106
|
+
captureDeadline: null,
|
|
1107
|
+
refundedAmountMinor: 0,
|
|
1108
|
+
lastRefundAt: null,
|
|
1109
|
+
taxInvoiceStatus: "pending",
|
|
1110
|
+
taxInvoiceNumber: null,
|
|
1111
|
+
taxInvoiceUrl: null,
|
|
1112
|
+
failureCode: null,
|
|
1113
|
+
failureMessage: null,
|
|
1114
|
+
failureDetails: null,
|
|
1115
|
+
description: data.description ?? null,
|
|
1116
|
+
metadata: data.metadata ?? null,
|
|
1117
|
+
createdAt: now,
|
|
1118
|
+
updatedAt: now
|
|
1119
|
+
};
|
|
1120
|
+
this.transactions.set(transaction.id, transaction);
|
|
1121
|
+
return transaction;
|
|
1122
|
+
}
|
|
1123
|
+
async update(id, data) {
|
|
1124
|
+
const existing = this.transactions.get(id);
|
|
1125
|
+
if (!existing) return null;
|
|
1126
|
+
const updated = {
|
|
1127
|
+
...existing,
|
|
1128
|
+
...data,
|
|
1129
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1130
|
+
};
|
|
1131
|
+
this.transactions.set(id, updated);
|
|
1132
|
+
return updated;
|
|
1133
|
+
}
|
|
1134
|
+
async delete(id) {
|
|
1135
|
+
return this.transactions.delete(id);
|
|
1136
|
+
}
|
|
1137
|
+
async findByInternalPaymentId(internalPaymentId) {
|
|
1138
|
+
for (const tx of this.transactions.values()) {
|
|
1139
|
+
if (tx.internalPaymentId === internalPaymentId) return tx;
|
|
1140
|
+
}
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
async findByIdempotencyKey(idempotencyKey) {
|
|
1144
|
+
for (const tx of this.transactions.values()) {
|
|
1145
|
+
if (tx.idempotencyKey === idempotencyKey) return tx;
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
async findByProviderTransactionId(provider, providerTransactionId) {
|
|
1150
|
+
for (const tx of this.transactions.values()) {
|
|
1151
|
+
if (tx.provider === provider && tx.providerTransactionId === providerTransactionId) return tx;
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
async findMany(filter, pagination) {
|
|
1156
|
+
let results = Array.from(this.transactions.values());
|
|
1157
|
+
if (filter.userId) results = results.filter((t) => t.userId === filter.userId);
|
|
1158
|
+
if (filter.status) {
|
|
1159
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
1160
|
+
results = results.filter((t) => statuses.includes(t.status));
|
|
1161
|
+
}
|
|
1162
|
+
if (filter.provider) {
|
|
1163
|
+
const providers = Array.isArray(filter.provider) ? filter.provider : [filter.provider];
|
|
1164
|
+
results = results.filter((t) => providers.includes(t.provider));
|
|
1165
|
+
}
|
|
1166
|
+
const total = results.length;
|
|
1167
|
+
const limit = pagination?.limit ?? 50;
|
|
1168
|
+
const offset = pagination?.offset ?? 0;
|
|
1169
|
+
const data = results.slice(offset, offset + limit);
|
|
1170
|
+
return { data, total, limit, offset, hasMore: offset + limit < total };
|
|
1171
|
+
}
|
|
1172
|
+
async findByUserId(userId, pagination) {
|
|
1173
|
+
return this.findMany({ userId }, pagination);
|
|
1174
|
+
}
|
|
1175
|
+
async updateStatus(id, status, additionalData) {
|
|
1176
|
+
return this.update(id, { ...additionalData, status });
|
|
1177
|
+
}
|
|
1178
|
+
async incrementRefundedAmount(id, amountMinor) {
|
|
1179
|
+
const existing = this.transactions.get(id);
|
|
1180
|
+
if (!existing) return null;
|
|
1181
|
+
return this.update(id, {
|
|
1182
|
+
refundedAmountMinor: existing.refundedAmountMinor + amountMinor,
|
|
1183
|
+
lastRefundAt: /* @__PURE__ */ new Date()
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
async findExpiredAuthorizations(beforeDate) {
|
|
1187
|
+
return Array.from(this.transactions.values()).filter(
|
|
1188
|
+
(t) => t.status === "authorized" && t.captureDeadline && t.captureDeadline < beforeDate
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
async countByStatus(_filter) {
|
|
1192
|
+
const counts = {};
|
|
1193
|
+
for (const tx of this.transactions.values()) {
|
|
1194
|
+
counts[tx.status] = (counts[tx.status] ?? 0) + 1;
|
|
1195
|
+
}
|
|
1196
|
+
return counts;
|
|
1197
|
+
}
|
|
1198
|
+
/** Clear all stored transactions (useful for testing) */
|
|
1199
|
+
clear() {
|
|
1200
|
+
this.transactions.clear();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
export {
|
|
1204
|
+
CircuitBreaker,
|
|
1205
|
+
InMemoryCircuitBreakerStorage,
|
|
1206
|
+
InMemoryTransactionRepository,
|
|
1207
|
+
PaymentOrchestrator,
|
|
1208
|
+
RoutingEngine,
|
|
1209
|
+
SUCCESS_STATES,
|
|
1210
|
+
TERMINAL_STATES,
|
|
1211
|
+
attemptTransition,
|
|
1212
|
+
calculateCaptureDeadline,
|
|
1213
|
+
canCapture,
|
|
1214
|
+
canRefund,
|
|
1215
|
+
canTransition,
|
|
1216
|
+
canVoid,
|
|
1217
|
+
createCircuitBreaker,
|
|
1218
|
+
createConfig,
|
|
1219
|
+
createConfigFromEnv,
|
|
1220
|
+
createDefaultState,
|
|
1221
|
+
createPartialConfig,
|
|
1222
|
+
createPaymentOrchestrator,
|
|
1223
|
+
createPaymentServices,
|
|
1224
|
+
createRoutingEngine,
|
|
1225
|
+
generateDeterministicKey,
|
|
1226
|
+
generateIdempotencyKey,
|
|
1227
|
+
generateInternalPaymentId,
|
|
1228
|
+
generateOperationKey,
|
|
1229
|
+
getCircuitBreaker,
|
|
1230
|
+
getConfiguredProviderList,
|
|
1231
|
+
getConfiguredProviders,
|
|
1232
|
+
getInMemoryStorage,
|
|
1233
|
+
getNextStatus,
|
|
1234
|
+
getPaymentServices,
|
|
1235
|
+
getRoutingEngine,
|
|
1236
|
+
getSignatureHeaderName,
|
|
1237
|
+
getSignatureVerifier,
|
|
1238
|
+
isAuthorizationExpired,
|
|
1239
|
+
isCircuitOpen,
|
|
1240
|
+
isHoldState,
|
|
1241
|
+
isProductionReady,
|
|
1242
|
+
isSuccessState,
|
|
1243
|
+
isTerminalState,
|
|
1244
|
+
isValidIdempotencyKey,
|
|
1245
|
+
isValidInternalPaymentId,
|
|
1246
|
+
migrateFromLegacyMap,
|
|
1247
|
+
registerProvider,
|
|
1248
|
+
registerSignatureVerifier,
|
|
1249
|
+
resetCircuitBreaker,
|
|
1250
|
+
resetInMemoryStorage,
|
|
1251
|
+
resetPaymentServices,
|
|
1252
|
+
resetRoutingEngine,
|
|
1253
|
+
shouldAttemptHalfOpen,
|
|
1254
|
+
validateConfig,
|
|
1255
|
+
verifyHmacSha256Signature,
|
|
1256
|
+
verifySortedFieldsHmacSignature,
|
|
1257
|
+
verifyStripeStyleSignature,
|
|
1258
|
+
verifyWebhookSignature
|
|
1259
|
+
};
|
|
1260
|
+
//# sourceMappingURL=index.js.map
|