Payment Orchestrator
Routing rules engine. Selects PSP, runs cascade, delegates to the adapter.
RequestContext from the Router and returns an OrchestratorResult.Orchestration Flow
Receive normalized request from Router
Context includes user_id, brand_id, geo, direction, method, amount, currency, session_id.
Load PSP candidates from psp_configs
Query: WHERE brand_id = ? AND is_active = true AND (geo = ? OR geo IS NULL) AND direction IN (?, "both") ORDER BY priority ASC
SELECT * FROM psp_configs WHERE brand_id = :brand_id AND is_active = true AND direction IN (:direction, 'both') AND (geo = :geo OR geo IS NULL) AND :method = ANY(methods) ORDER BY priority ASC
Apply routing_rules overrides
Evaluate active rules for this brand, ordered by priority. First matching rule pins a specific psp_config_id. If no rule matches -- use the priority-ordered list from step 2.
Select first PSP from candidate list
The top candidate (lowest priority number) becomes the active PSP. Its psp_config_id is written to the transaction record immediately.
Resolve adapter and call initiateDeposit / initiateWithdrawal
Adapter is resolved by psp_name from the adapter registry. The call is synchronous -- Orchestrator awaits the UnifiedResponse.
const adapter = adapterRegistry.get(pspConfig.psp_name)
const response = await adapter.initiateDeposit({
transactionId: tx.id,
orderId: tx.psp_order_id,
amount: tx.amount_requested,
currency: tx.currency,
method: tx.method,
credentials: pspConfig.credentials,
})Handle adapter response
On success -- transition state machine to PROCESSING and return data to Router. On PSP error -- attempt cascade to next candidate (see Cascade Manager).
Return UnifiedResponse to Router
Router maps the UnifiedResponse to the HTTP response shape and sends it to the client.
Routing Dimensions
The Orchestrator builds the SQL query dynamically from the request context. All dimensions are applied simultaneously as AND conditions.
Brand
brand_id -- mandatory. Every PSP config is scoped to a brand.
GEO
player's country from request context. NULL in psp_configs matches any geo.
Direction
"deposit" | "withdrawal". Configs with direction="both" match either.
Currency
Filtered implicitly via method slug -- each method maps to a specific currency.
Method
method slug must be present in psp_config.methods[] array.
Priority
Lowest priority number wins within a brand+geo+direction group.
If no candidates are found after filtering -- the Orchestrator returns PSP_NOT_AVAILABLE (503). This means no active PSP is configured for the given brand/geo/method combination.
Cascade Behavior
If the first PSP fails or returns an error, the Orchestrator automatically tries the next candidate. The Cascade Manager handles the iteration logic. Maximum 3 attempts per request.
Adapter throws network / timeout error
Remove failed PSP from candidate list. Retry with next candidate. Log error to transaction_events.
PSP returns explicit rejection (e.g. currency not supported)
Treat as hard failure for this PSP. Cascade to next candidate. Do NOT retry the same PSP.
All candidates exhausted
Transition transaction to FAILED. Return error to Router. No further retries.
Cascade pseudocode
for (const pspConfig of candidates) {
try {
const result = await adapter.initiateDeposit(ctx, pspConfig)
await stateMachine.transition(tx.id, 'PROCESSING', { pspConfigId: pspConfig.id })
return result
} catch (err) {
await transactionEvents.log(tx.id, 'cascade_attempt_failed', { pspName: pspConfig.psp_name, err })
continue // try next candidate
}
}
// all candidates failed
await stateMachine.transition(tx.id, 'FAILED')
throw new OrchestratorError('PSP_ALL_FAILED')Internal Interface
The Orchestrator is not an HTTP service. It is called directly from the Router as a TypeScript module. The methods below are the module's public contract.
orchestrateDeposit(ctx: RequestContext): Promise<OrchestratorResult>Main entry point for deposit requests. Runs the full routing + adapter + cascade pipeline.
OrchestratorResult: { transactionId, status, walletAddress, destinationTag, expiresAt, amountCrypto }orchestrateWithdrawal(ctx: RequestContext): Promise<OrchestratorResult>Entry point for withdrawal requests. Phase 3 -- throws NotImplementedError in Phase 2.
OrchestratorResult: { transactionId, status }selectPsp(ctx: RequestContext): Promise<PspConfig[]>Queries psp_configs + routing_rules and returns ordered candidate list. Separated for testability.
PspConfig[] -- ordered by effective priority after rules appliedError Codes
| Code | HTTP | When |
|---|---|---|
PSP_NOT_AVAILABLE | 503 | No active PSP found for brand + geo + method combination |
PSP_ALL_FAILED | 502 | All cascade candidates exhausted without a successful response |
ADAPTER_TIMEOUT | 504 | PSP adapter did not respond within the configured timeout (default 30s) |
ADAPTER_ERROR | 502 | Adapter returned an error response that is not retryable |
STATE_WRITE_FAILED | 500 | Failed to persist transaction or state transition to DB |