@etohq/connector-engine 1.5.1-alpha.4

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.
Files changed (77) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +253 -0
  5. package/dist/engine/clean-connector-engine.d.ts +81 -0
  6. package/dist/engine/clean-connector-engine.d.ts.map +1 -0
  7. package/dist/engine/clean-connector-engine.js +350 -0
  8. package/dist/engine/clean-connector-engine.js.map +1 -0
  9. package/dist/engine/connector-engine-impl.d.ts +73 -0
  10. package/dist/engine/connector-engine-impl.d.ts.map +1 -0
  11. package/dist/engine/connector-engine-impl.js +332 -0
  12. package/dist/engine/connector-engine-impl.js.map +1 -0
  13. package/dist/engine/connector-engine.d.ts +54 -0
  14. package/dist/engine/connector-engine.d.ts.map +1 -0
  15. package/dist/engine/connector-engine.js +694 -0
  16. package/dist/engine/connector-engine.js.map +1 -0
  17. package/dist/engine/index.d.ts +7 -0
  18. package/dist/engine/index.d.ts.map +1 -0
  19. package/dist/engine/index.js +10 -0
  20. package/dist/engine/index.js.map +1 -0
  21. package/dist/engine/routing-engine.d.ts +26 -0
  22. package/dist/engine/routing-engine.d.ts.map +1 -0
  23. package/dist/engine/routing-engine.js +329 -0
  24. package/dist/engine/routing-engine.js.map +1 -0
  25. package/dist/examples/booking-connector-example.d.ts +7 -0
  26. package/dist/examples/booking-connector-example.d.ts.map +1 -0
  27. package/dist/examples/booking-connector-example.js +221 -0
  28. package/dist/examples/booking-connector-example.js.map +1 -0
  29. package/dist/examples/dynamic-methods-example.d.ts +7 -0
  30. package/dist/examples/dynamic-methods-example.d.ts.map +1 -0
  31. package/dist/examples/dynamic-methods-example.js +163 -0
  32. package/dist/examples/dynamic-methods-example.js.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +14 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/types/base-plugin.d.ts +170 -0
  38. package/dist/types/base-plugin.d.ts.map +1 -0
  39. package/dist/types/base-plugin.js +68 -0
  40. package/dist/types/base-plugin.js.map +1 -0
  41. package/dist/types/connector-plugin.d.ts +22 -0
  42. package/dist/types/connector-plugin.d.ts.map +1 -0
  43. package/dist/types/connector-plugin.js +11 -0
  44. package/dist/types/connector-plugin.js.map +1 -0
  45. package/dist/types/engine.d.ts +223 -0
  46. package/dist/types/engine.d.ts.map +1 -0
  47. package/dist/types/engine.js +7 -0
  48. package/dist/types/engine.js.map +1 -0
  49. package/dist/types/index.d.ts +5 -0
  50. package/dist/types/index.d.ts.map +1 -0
  51. package/dist/types/index.js +9 -0
  52. package/dist/types/index.js.map +1 -0
  53. package/dist/types/operation-groups.d.ts +78 -0
  54. package/dist/types/operation-groups.d.ts.map +1 -0
  55. package/dist/types/operation-groups.js +60 -0
  56. package/dist/types/operation-groups.js.map +1 -0
  57. package/dist/types/routing-config.d.ts +116 -0
  58. package/dist/types/routing-config.d.ts.map +1 -0
  59. package/dist/types/routing-config.js +6 -0
  60. package/dist/types/routing-config.js.map +1 -0
  61. package/dist/utils/create-connector-engine.d.ts +31 -0
  62. package/dist/utils/create-connector-engine.d.ts.map +1 -0
  63. package/dist/utils/create-connector-engine.js +30 -0
  64. package/dist/utils/create-connector-engine.js.map +1 -0
  65. package/examples/booking-example.ts +168 -0
  66. package/examples/booking-test.ts +231 -0
  67. package/hyperswitch-example.ts +263 -0
  68. package/jest.config.js +2 -0
  69. package/package.json +54 -0
  70. package/src/engine/clean-connector-engine.ts +726 -0
  71. package/src/engine/index.ts +13 -0
  72. package/src/engine/routing-engine.ts +394 -0
  73. package/src/index.ts +32 -0
  74. package/src/types/connector-plugin.ts +34 -0
  75. package/src/types/index.ts +5 -0
  76. package/src/types/routing-config.ts +196 -0
  77. package/tsconfig.json +3 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fileoverview Core Connector Engine
3
+ * @description Clean plugin-first connector engine
4
+ */
5
+
6
+ export { ConnectorEngine } from "./clean-connector-engine";
7
+ export type {
8
+ ConnectorRegistry,
9
+ OperationGroup,
10
+ OperationGroups,
11
+ EngineConfig,
12
+ GroupResult
13
+ } from "./clean-connector-engine";
@@ -0,0 +1,394 @@
1
+ /**
2
+ * @fileoverview Hyperswitch-Inspired Routing Engine
3
+ */
4
+
5
+ import {
6
+ RoutingConfig,
7
+ RoutingContext,
8
+ RoutingDecision,
9
+ ConnectorMetrics,
10
+ RoutingRule,
11
+ RoutingEngine as IRoutingEngine
12
+ } from "../types/routing-config";
13
+
14
+ export class RoutingEngine implements IRoutingEngine {
15
+ private metrics: Map<string, ConnectorMetrics> = new Map();
16
+ private circuitBreakers: Map<string, { state: string; failures: number; lastFailure?: Date }> = new Map();
17
+
18
+ async route(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
19
+ // 1. Check for routing override
20
+ if (context.forceConnector) {
21
+ return {
22
+ selected_connector: context.forceConnector,
23
+ algorithm_used: 'priority',
24
+ fallback_used: false,
25
+ decision_timestamp: new Date().toISOString(),
26
+ routing_profile_id: config.profile_id,
27
+ alternative_connectors: []
28
+ };
29
+ }
30
+
31
+ // 2. Apply routing algorithm
32
+ switch (config.algorithm.type) {
33
+ case 'priority':
34
+ return await this.routeByPriority(context, config);
35
+
36
+ case 'volume_split':
37
+ return await this.routeByVolumeSplit(context, config);
38
+
39
+ case 'advanced':
40
+ return await this.routeByRules(context, config);
41
+
42
+ case 'performance':
43
+ return await this.routeByPerformance(context, config);
44
+
45
+ case 'cost_optimization':
46
+ return await this.routeByCost(context, config);
47
+
48
+ default:
49
+ throw new Error(`Unsupported routing algorithm: ${config.algorithm.type}`);
50
+ }
51
+ }
52
+
53
+ private async routeByPriority(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
54
+ // Find applicable rules
55
+ const applicableRules = this.findApplicableRules(context, config.algorithm.data);
56
+
57
+ if (applicableRules.length === 0) {
58
+ return this.fallbackRouting(context, config);
59
+ }
60
+
61
+ // Sort connectors by priority
62
+ const sortedConnectors = applicableRules[0].connectors
63
+ .filter(c => this.isConnectorHealthy(c.connector))
64
+ .sort((a, b) => (b.priority || 0) - (a.priority || 0));
65
+
66
+ if (sortedConnectors.length === 0) {
67
+ return this.fallbackRouting(context, config);
68
+ }
69
+
70
+ return {
71
+ selected_connector: sortedConnectors[0].connector,
72
+ merchant_connector_id: sortedConnectors[0].merchant_connector_id,
73
+ algorithm_used: 'priority',
74
+ rule_applied: applicableRules[0].name,
75
+ fallback_used: false,
76
+ decision_timestamp: new Date().toISOString(),
77
+ routing_profile_id: config.profile_id,
78
+ alternative_connectors: sortedConnectors.slice(1).map(c => c.connector)
79
+ };
80
+ }
81
+
82
+ private async routeByVolumeSplit(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
83
+ const applicableRules = this.findApplicableRules(context, config.algorithm.data);
84
+
85
+ if (applicableRules.length === 0) {
86
+ return this.fallbackRouting(context, config);
87
+ }
88
+
89
+ // Weighted random selection based on volume split
90
+ const connectors = applicableRules[0].connectors.filter(c => this.isConnectorHealthy(c.connector));
91
+ const totalWeight = connectors.reduce((sum, c) => sum + (c.weight || 1), 0);
92
+ const random = Math.random() * totalWeight;
93
+
94
+ let currentWeight = 0;
95
+ for (const connector of connectors) {
96
+ currentWeight += connector.weight || 1;
97
+ if (random <= currentWeight) {
98
+ return {
99
+ selected_connector: connector.connector,
100
+ merchant_connector_id: connector.merchant_connector_id,
101
+ algorithm_used: 'volume_split',
102
+ rule_applied: applicableRules[0].name,
103
+ fallback_used: false,
104
+ decision_timestamp: new Date().toISOString(),
105
+ routing_profile_id: config.profile_id,
106
+ alternative_connectors: connectors.filter(c => c.connector !== connector.connector).map(c => c.connector)
107
+ };
108
+ }
109
+ }
110
+
111
+ return this.fallbackRouting(context, config);
112
+ }
113
+
114
+ private async routeByRules(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
115
+ // Advanced rule-based routing with complex conditions
116
+ for (const rule of config.algorithm.data) {
117
+ if (this.evaluateRule(context, rule)) {
118
+ const healthyConnectors = rule.connectors.filter(c => this.isConnectorHealthy(c.connector));
119
+
120
+ if (healthyConnectors.length > 0) {
121
+ // Use priority within the rule
122
+ const selectedConnector = healthyConnectors.sort((a, b) => (b.priority || 0) - (a.priority || 0))[0];
123
+
124
+ return {
125
+ selected_connector: selectedConnector.connector,
126
+ merchant_connector_id: selectedConnector.merchant_connector_id,
127
+ algorithm_used: 'advanced',
128
+ rule_applied: rule.name,
129
+ fallback_used: false,
130
+ decision_timestamp: new Date().toISOString(),
131
+ routing_profile_id: config.profile_id,
132
+ alternative_connectors: healthyConnectors.slice(1).map(c => c.connector)
133
+ };
134
+ }
135
+ }
136
+ }
137
+
138
+ return this.fallbackRouting(context, config);
139
+ }
140
+
141
+ private async routeByPerformance(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
142
+ const applicableRules = this.findApplicableRules(context, config.algorithm.data);
143
+
144
+ if (applicableRules.length === 0) {
145
+ return this.fallbackRouting(context, config);
146
+ }
147
+
148
+ // Sort by success rate
149
+ const connectors = applicableRules[0].connectors
150
+ .filter(c => this.isConnectorHealthy(c.connector))
151
+ .sort((a, b) => {
152
+ const aMetrics = this.metrics.get(a.connector);
153
+ const bMetrics = this.metrics.get(b.connector);
154
+ return (bMetrics?.success_rate || 0) - (aMetrics?.success_rate || 0);
155
+ });
156
+
157
+ if (connectors.length === 0) {
158
+ return this.fallbackRouting(context, config);
159
+ }
160
+
161
+ return {
162
+ selected_connector: connectors[0].connector,
163
+ merchant_connector_id: connectors[0].merchant_connector_id,
164
+ algorithm_used: 'performance',
165
+ rule_applied: applicableRules[0].name,
166
+ fallback_used: false,
167
+ decision_timestamp: new Date().toISOString(),
168
+ routing_profile_id: config.profile_id,
169
+ alternative_connectors: connectors.slice(1).map(c => c.connector)
170
+ };
171
+ }
172
+
173
+ private async routeByCost(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision> {
174
+ // Cost-based routing (would need cost data from connectors)
175
+ return this.routeByPriority(context, config);
176
+ }
177
+
178
+ private findApplicableRules(context: RoutingContext, rules: RoutingRule[]): RoutingRule[] {
179
+ return rules.filter(rule => rule.enabled && this.evaluateRule(context, rule));
180
+ }
181
+
182
+ private evaluateRule(context: RoutingContext, rule: RoutingRule): boolean {
183
+ const conditions = rule.conditions;
184
+
185
+ // Resource type check
186
+ if (conditions.resourceType && context.resourceType && !conditions.resourceType.includes(context.resourceType)) {
187
+ return false;
188
+ }
189
+
190
+ // Region check
191
+ if (conditions.region && context.region && !conditions.region.includes(context.region)) {
192
+ return false;
193
+ }
194
+
195
+ // Resource size range check
196
+ if (conditions.resourceSize && context.resourceSize !== undefined) {
197
+ if (conditions.resourceSize.min && context.resourceSize < conditions.resourceSize.min) return false;
198
+ if (conditions.resourceSize.max && context.resourceSize > conditions.resourceSize.max) return false;
199
+ }
200
+
201
+ // Country check
202
+ if (conditions.country && context.country && !conditions.country.includes(context.country)) {
203
+ return false;
204
+ }
205
+
206
+ // Tenant check
207
+ if (conditions.tenantId && context.tenantId && !conditions.tenantId.includes(context.tenantId)) {
208
+ return false;
209
+ }
210
+
211
+ // Priority check
212
+ if (conditions.priority && context.priority && !conditions.priority.includes(context.priority)) {
213
+ return false;
214
+ }
215
+
216
+ return true;
217
+ }
218
+
219
+ private isConnectorHealthy(connectorId: string): boolean {
220
+ const circuitBreaker = this.circuitBreakers.get(connectorId);
221
+ return !circuitBreaker || circuitBreaker.state !== 'open';
222
+ }
223
+
224
+ private fallbackRouting(context: RoutingContext, config: RoutingConfig): RoutingDecision {
225
+ if (!config.fallback_routing?.enabled || !config.fallback_routing.connectors.length) {
226
+ throw new Error('No healthy connectors available and fallback routing disabled');
227
+ }
228
+
229
+ const healthyFallbacks = config.fallback_routing.connectors.filter(c => this.isConnectorHealthy(c));
230
+
231
+ if (healthyFallbacks.length === 0) {
232
+ throw new Error('No healthy fallback connectors available');
233
+ }
234
+
235
+ return {
236
+ selected_connector: healthyFallbacks[0],
237
+ algorithm_used: 'priority',
238
+ fallback_used: true,
239
+ decision_timestamp: new Date().toISOString(),
240
+ routing_profile_id: config.profile_id,
241
+ alternative_connectors: healthyFallbacks.slice(1)
242
+ };
243
+ }
244
+
245
+ async updateMetrics(connectorId: string, success: boolean, responseTime: number, resourceSize?: number): Promise<void> {
246
+ // Try to get the existing metrics or create a new, fully-typed record
247
+ const existing = this.metrics.get(connectorId) || {
248
+ connector_name: connectorId,
249
+ success_rate: 0,
250
+ total_volume: 0,
251
+ successful_payments: 0,
252
+ failed_payments: 0,
253
+ avg_response_time_ms: 0,
254
+ p95_response_time_ms: 0,
255
+ total_amount_processed: 0,
256
+ avg_ticket_size: 0,
257
+ window_start: new Date().toISOString(),
258
+ window_end: new Date().toISOString(),
259
+ circuit_breaker_status: 'closed' as const,
260
+ consecutive_failures: 0,
261
+ // Required additional properties to fulfill ConnectorMetrics interface
262
+ total_requests: 0,
263
+ successful_requests: 0,
264
+ failed_requests: 0,
265
+ total_resources_processed: 0,
266
+ avg_resource_size: 0,
267
+ };
268
+
269
+ // Incremental updates
270
+ existing.total_volume = (existing.total_volume || 0) + 1;
271
+ existing.total_requests = (existing.total_requests || 0) + 1;
272
+ existing.total_resources_processed = (existing.total_resources_processed || 0) + (resourceSize || 0);
273
+
274
+ existing.total_amount_processed = (existing.total_amount_processed || 0) + (resourceSize || 0);
275
+
276
+ if (success) {
277
+ existing.successful_payments = (existing.successful_payments || 0) + 1;
278
+ existing.successful_requests = (existing.successful_requests || 0) + 1;
279
+ } else {
280
+ existing.failed_payments = (existing.failed_payments || 0) + 1;
281
+ existing.failed_requests = (existing.failed_requests || 0) + 1;
282
+ }
283
+
284
+ existing.success_rate =
285
+ (existing.successful_payments || 0) / (existing.total_volume || 1);
286
+ existing.avg_ticket_size =
287
+ (existing.total_amount_processed || 0) / (existing.total_volume || 1);
288
+
289
+ existing.avg_resource_size =
290
+ (existing.total_resources_processed || 0) / (existing.total_requests || 1);
291
+
292
+ // For response times, maintain a very basic moving average
293
+ if (existing.avg_response_time_ms && existing.total_volume > 1) {
294
+ existing.avg_response_time_ms =
295
+ ((existing.avg_response_time_ms * (existing.total_volume - 1)) + responseTime) /
296
+ existing.total_volume;
297
+ } else {
298
+ existing.avg_response_time_ms = responseTime;
299
+ }
300
+
301
+ // For P95: This would need proper history tracking. Use the current value as a placeholder.
302
+ existing.p95_response_time_ms = existing.avg_response_time_ms;
303
+
304
+ // Optionally update window timestamps
305
+ existing.window_end = new Date().toISOString();
306
+
307
+ // The actual circuit breaker status and consecutive_failures will be updated in the updateCircuitBreaker call below
308
+ // but set them from existing state (if available)
309
+ const cb = this.circuitBreakers.get(connectorId);
310
+ if (cb) {
311
+ existing.circuit_breaker_status = cb.state as typeof existing.circuit_breaker_status;
312
+ existing.consecutive_failures = cb.failures || 0;
313
+ }
314
+
315
+ this.metrics.set(connectorId, existing);
316
+
317
+
318
+ // Update circuit breaker after metrics
319
+ this.updateCircuitBreaker(connectorId, success);
320
+
321
+ // Set updated circuit breaker fields again after it might have changed
322
+ const cbUpdated = this.circuitBreakers.get(connectorId);
323
+ if (cbUpdated) {
324
+ existing.circuit_breaker_status = cbUpdated.state as typeof existing.circuit_breaker_status;
325
+ existing.consecutive_failures = cbUpdated.failures || 0;
326
+ this.metrics.set(connectorId, existing);
327
+ }
328
+ }
329
+
330
+ private updateCircuitBreaker(connectorId: string, success: boolean): void {
331
+ const cb = this.circuitBreakers.get(connectorId) || { state: 'closed', failures: 0 };
332
+
333
+ if (success) {
334
+ cb.failures = 0;
335
+ cb.state = 'closed';
336
+ } else {
337
+ cb.failures++;
338
+ cb.lastFailure = new Date();
339
+
340
+ if (cb.failures >= 5) { // Configurable threshold
341
+ cb.state = 'open';
342
+ }
343
+ }
344
+
345
+ this.circuitBreakers.set(connectorId, cb);
346
+ }
347
+
348
+ async getConnectorHealth(connectorId: string): Promise<ConnectorMetrics> {
349
+ return this.metrics.get(connectorId) || {
350
+ connector_name: connectorId,
351
+ success_rate: 0,
352
+ total_volume: 0,
353
+ successful_payments: 0,
354
+ failed_payments: 0,
355
+ avg_response_time_ms: 0,
356
+ p95_response_time_ms: 0,
357
+ total_amount_processed: 0,
358
+ avg_ticket_size: 0,
359
+ window_start: new Date().toISOString(),
360
+ window_end: new Date().toISOString(),
361
+ circuit_breaker_status: 'closed' as const,
362
+ consecutive_failures: 0,
363
+
364
+ // Add missing required properties for ConnectorMetrics type
365
+ total_requests: 0,
366
+ successful_requests: 0,
367
+ failed_requests: 0,
368
+ total_resources_processed: 0,
369
+ avg_resource_size: 0,
370
+ };
371
+
372
+ }
373
+
374
+ async validateConfig(config: RoutingConfig): Promise<{ valid: boolean; errors: string[] }> {
375
+ const errors: string[] = [];
376
+
377
+ if (!config.profile_id) {
378
+ errors.push('Profile ID is required');
379
+ }
380
+
381
+ if (!config.algorithm?.type) {
382
+ errors.push('Routing algorithm type is required');
383
+ }
384
+
385
+ if (!config.algorithm?.data?.length) {
386
+ errors.push('At least one routing rule is required');
387
+ }
388
+
389
+ return {
390
+ valid: errors.length === 0,
391
+ errors
392
+ };
393
+ }
394
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @fileoverview ETO Framework Connector Engine
3
+ * @description Clean plugin-first connector engine with Hyperswitch-style routing
4
+ */
5
+
6
+ // ===== CORE ENGINE =====
7
+ export { ConnectorEngine } from "./engine/clean-connector-engine";
8
+ export type {
9
+ ConnectorRegistry,
10
+ OperationGroup,
11
+ OperationGroups,
12
+ EngineConfig,
13
+ GroupResult
14
+ } from "./engine/clean-connector-engine";
15
+
16
+ // ===== PLUGIN TYPES =====
17
+ export {
18
+ type ConnectorPlugin,
19
+ type OperationType,
20
+ AbstractConnectorPlugin
21
+ } from "./types/connector-plugin";
22
+
23
+ // ===== ROUTING TYPES =====
24
+ export type {
25
+ RoutingConfig,
26
+ RoutingContext,
27
+ RoutingDecision,
28
+ ConnectorMetrics,
29
+ RoutingRule,
30
+ ConnectorChoice,
31
+ RoutingAlgorithm
32
+ } from "./types/routing-config";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview Clean Connector Plugin Design - First Principles
3
+ */
4
+
5
+ // ===== OPERATION TYPE =====
6
+ export interface OperationType {
7
+ input: any;
8
+ output: any;
9
+ }
10
+
11
+ // ===== CONNECTOR PLUGIN =====
12
+ export interface ConnectorPlugin<TOperations extends Record<string, OperationType> = Record<string, OperationType>> {
13
+ getName(): string;
14
+ getVersion(): string;
15
+
16
+ // Operations receive config at execution time
17
+ operations: {
18
+ [K in keyof TOperations]: (
19
+ input: TOperations[K]['input'],
20
+ config: any
21
+ ) => Promise<TOperations[K]['output']>
22
+ };
23
+ }
24
+
25
+ // ===== ABSTRACT BASE =====
26
+ export abstract class AbstractConnectorPlugin<TOperations extends Record<string, OperationType> = Record<string, OperationType>>
27
+ implements ConnectorPlugin<TOperations> {
28
+
29
+ abstract getName(): string;
30
+ abstract getVersion(): string;
31
+ abstract operations: {
32
+ [K in keyof TOperations]: (input: TOperations[K]['input'], config: any) => Promise<TOperations[K]['output']>
33
+ };
34
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Clean Connector Engine Types
3
+ */
4
+
5
+ export { type ConnectorPlugin, type OperationType, AbstractConnectorPlugin } from "./connector-plugin";
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @fileoverview Generic Routing Configuration (Not Payment-Specific)
3
+ */
4
+
5
+ // ===== GENERIC ROUTING CONTEXT =====
6
+ export interface RoutingContext {
7
+ // Generic operation context
8
+ operation?: string;
9
+ domain?: string;
10
+
11
+ // Geographic context
12
+ region?: string;
13
+ country?: string;
14
+
15
+ // Business context
16
+ userId?: string;
17
+ tenantId?: string;
18
+ priority?: 'low' | 'normal' | 'high';
19
+
20
+ // Resource context
21
+ resourceType?: string;
22
+ resourceSize?: number;
23
+
24
+ // Custom metadata for any domain
25
+ metadata?: Record<string, any>;
26
+
27
+ // Routing overrides
28
+ forceConnector?: string;
29
+ skipRouting?: boolean;
30
+ }
31
+
32
+ // ===== GENERIC ROUTING RULE =====
33
+ export interface RoutingRule {
34
+ name: string;
35
+ description?: string;
36
+
37
+ // Generic conditions (not payment-specific)
38
+ conditions: {
39
+ // Resource conditions
40
+ resourceType?: string[];
41
+ resourceSize?: { min?: number; max?: number };
42
+
43
+ // Geographic conditions
44
+ region?: string[];
45
+ country?: string[];
46
+
47
+ // Business conditions
48
+ tenantId?: string[];
49
+ priority?: string[];
50
+
51
+ // Time-based conditions
52
+ timeOfDay?: { start?: string; end?: string };
53
+ dayOfWeek?: string[];
54
+
55
+ // Custom field conditions
56
+ customFields?: Record<string, any>;
57
+
58
+ // Metadata conditions
59
+ metadata?: Record<string, any>;
60
+ };
61
+
62
+ // Connector selection
63
+ connectors: ConnectorChoice[];
64
+
65
+ // Rule metadata
66
+ enabled: boolean;
67
+ created_at: string;
68
+ modified_at: string;
69
+ }
70
+
71
+ // ===== CONNECTOR CHOICE =====
72
+ export interface ConnectorChoice {
73
+ connector: string;
74
+ merchant_connector_id?: string;
75
+
76
+ // Selection criteria
77
+ priority?: number;
78
+ weight?: number;
79
+
80
+ // Resource constraints
81
+ maxConcurrency?: number;
82
+ maxResourceSize?: number;
83
+
84
+ // Geographic constraints
85
+ supportedRegions?: string[];
86
+ supportedCountries?: string[];
87
+ }
88
+
89
+ // ===== ROUTING ALGORITHM =====
90
+ export type RoutingAlgorithm =
91
+ | 'priority' // Priority-based routing
92
+ | 'volume_split' // Volume-based distribution
93
+ | 'advanced' // Rule-based routing
94
+ | 'cost_optimization' // Cost-aware routing
95
+ | 'performance' // Performance-based routing
96
+ | 'load_balancing' // Load balancing
97
+ | 'region_based' // Geographic routing
98
+
99
+ // ===== ROUTING CONFIGURATION =====
100
+ export interface RoutingConfig {
101
+ profile_id: string;
102
+ name: string;
103
+ description?: string;
104
+
105
+ // Generic routing algorithm
106
+ algorithm: {
107
+ type: RoutingAlgorithm;
108
+ data: RoutingRule[];
109
+ };
110
+
111
+ // Fallback configuration
112
+ fallback_routing?: {
113
+ enabled: boolean;
114
+ connectors: string[];
115
+ };
116
+
117
+ // Circuit breaker
118
+ circuit_breaker?: {
119
+ failure_threshold: number;
120
+ recovery_time_seconds: number;
121
+ max_retries: number;
122
+ };
123
+
124
+ // Metadata
125
+ created_at: string;
126
+ modified_at: string;
127
+ version: number;
128
+ }
129
+
130
+ // ===== CONNECTOR METRICS =====
131
+ export interface ConnectorMetrics {
132
+ connector_name: string;
133
+
134
+ // Success metrics
135
+ success_rate: number;
136
+ total_requests: number;
137
+ successful_requests: number;
138
+ failed_requests: number;
139
+
140
+ // Volume metrics (for payment-specific routing)
141
+ total_volume?: number;
142
+ successful_payments?: number;
143
+ failed_payments?: number;
144
+ total_amount_processed?: number;
145
+ avg_ticket_size?: number;
146
+
147
+ // Performance metrics
148
+ avg_response_time_ms: number;
149
+ p95_response_time_ms: number;
150
+
151
+ // Resource metrics
152
+ total_resources_processed: number;
153
+ avg_resource_size: number;
154
+
155
+ // Time window
156
+ window_start: string;
157
+ window_end: string;
158
+
159
+ // Circuit breaker state
160
+ circuit_breaker_status: 'closed' | 'open' | 'half_open';
161
+ consecutive_failures: number;
162
+ last_failure_time?: string;
163
+ }
164
+
165
+ // ===== ROUTING DECISION =====
166
+ export interface RoutingDecision {
167
+ selected_connector: string;
168
+ merchant_connector_id?: string;
169
+
170
+ // Decision context
171
+ algorithm_used: RoutingAlgorithm;
172
+ rule_applied?: string;
173
+ fallback_used: boolean;
174
+
175
+ // Routing metadata
176
+ decision_timestamp: string;
177
+ routing_profile_id: string;
178
+
179
+ // Alternative connectors (for fallback)
180
+ alternative_connectors: string[];
181
+ }
182
+
183
+ // ===== ROUTING ENGINE INTERFACE =====
184
+ export interface RoutingEngine {
185
+ // Route request to best connector
186
+ route(context: RoutingContext, config: RoutingConfig): Promise<RoutingDecision>;
187
+
188
+ // Update connector metrics
189
+ updateMetrics(connectorId: string, success: boolean, responseTime: number, resourceSize?: number): Promise<void>;
190
+
191
+ // Get connector health
192
+ getConnectorHealth(connectorId: string): Promise<ConnectorMetrics>;
193
+
194
+ // Validate routing config
195
+ validateConfig(config: RoutingConfig): Promise<{ valid: boolean; errors: string[] }>;
196
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../_tsconfig.base.json"
3
+ }