Payment orchestration · Lifecycle, routing and failure safety
The 60-second model#
A payment orchestrator presents a unified integration while selecting/interacting with payment providers according to merchant configuration and eligibility. It must normalize results without losing provider-specific meaning. Merchant UX success, an orchestration API response, PSP authorization, capture and bank settlement are different observations.
Interviewers can assess whether you recognize financial integrity and unknown outcomes, not whether you have memorized a provider's API. The supplied JD establishes orchestration and multiple PSPs; the model below is general and must be checked against actual product contracts.
Participants and identifiers#
| Concept | Meaning | QA observation |
|---|---|---|
| Merchant | Business accepting payment | Tenant ownership, configuration, permissions |
| Customer / payer | Initiates the purchase | Correct amount, currency, redirects and communication |
| Orchestrator | Unified flow, provider selection and normalization | Correct route, stable operation identity, accurate final state |
| PSP / acquirer / issuer | Provider processing, merchant acquiring and customer bank roles | Provider response, scheme/network and issuer decision; roles vary by integration |
| Payment ID | Business operation in the platform | Remains correlated across attempts |
| Attempt ID | One provider-processing attempt | Route, timing, normalized and raw outcome |
| Provider reference | PSP-side identifier | Reconcile uncertain attempts and provider reports |
| Idempotency key | Contract-supported duplicate submission identity | Scope, same/different payload, concurrency and expiry behavior |
| Event ID | Callback/message identity | Deduplication and auditability; not necessarily one per payment |
Do not use “transaction ID” without identifying its system and grain. One payment may have several attempts, captures or refunds. A duplicate at one grain may be legitimate at another.
Lifecycle and invariants#
| Operation | Meaning | Required tests |
|---|---|---|
| Authorization | Approval/reservation, not settlement | Approve/decline, expiry, amount/currency, no assumed final transfer |
| Capture | Request to collect authorized funds | Full/partial capture, remaining amount, duplicate protection |
| Void / cancellation | Cancel before a relevant processing boundary | Allowed states, races with capture, provider confirmation |
| Settlement | Financial clearing/funding stage | Report reconciliation, fees/time windows and provider-specific timing |
| Refund | Return against a captured payment | Full/partial, cumulative cap, pending/failure, duplicate callback |
| Dispute / chargeback | Separate dispute process | State/notification/permissions; not identical to a voluntary refund |
Illustrative state graph: CREATED → PENDING → REQUIRES_ACTION / AUTHORIZED / DECLINED; AUTHORIZED → CAPTURED / VOIDED; refund has its own lifecycle. This is not an OMNO state specification. Ask whether automatic capture exists, which transitions are terminal, and how partial operations are represented.
Invariants: captured total does not exceed the authorized/allowed total under the contract; refunded total does not exceed eligible captured amount; currency is preserved; events do not duplicate ledger effects; tenant A cannot read/refund tenant B's payment; historical audit facts are not erased by a later state change.
Routing and fallback decisions#
| Condition | Test design | Unsafe shortcut |
|---|---|---|
| Merchant/currency/provider eligibility | Eligible candidate selected; ineligible providers excluded | Assume every provider supports every currency/method |
| Rule priority / default | Boundary and conflicting rule tests; no-match behavior | Only test first happy-path route |
| Weighted distribution | Controlled seed/config or statistically justified sample | Assert exact 50/50 from two random payments |
| Provider definitely unavailable before submission | Verify configured fallback policy | Treat every timeout as a safe decline |
| Provider outcome unknown after submission | Reconcile original attempt before another charge | Blindly route the same payment to another PSP |
| Hard decline / soft failure | Follow explicit retry/fallback policy and reason classification | Automatically bypass every decline |
| Config update | Version, cache propagation and rollback behavior | Change shared routing rules during unrelated tests |
Provider health, geography, currency, cost or historical acceptance can be possible routing inputs in an orchestrator. The JD does not confirm which OMNO implements. Present them as questions or hypothetical test dimensions.
Ambiguous timeout: the highest-value scenario#
Scenario: PSP A may have accepted a payment, but the orchestrator timed out before receiving its response. A second attempt through PSP B could create a duplicate charge.
Ideal reasoning: distinguish “not sent,” “explicitly declined,” and “sent but unknown.” Retain operation/attempt/provider references and query the supported PSP/reconciliation path. Keep an explicit unknown/pending state if necessary. Apply only the documented, enforced idempotency/retry/fallback policy. Verify late success/decline callbacks, callbacks arriving after a retry decision, duplicate events and one logical financial effect.
// Standalone decision model, illustrative policy rather than a payment engine.
export type Attempt =
| { kind: 'not-submitted'; reason: string }
| { kind: 'declined'; retryAllowed: boolean }
| { kind: 'unknown'; reference: string }
| { kind: 'succeeded'; reference: string };
export function nextAction(attempt: Attempt): string {
switch (attempt.kind) {
case 'not-submitted': return 'consult-fallback-policy';
case 'declined': return attempt.retryAllowed ? 'consult-retry-policy' : 'stop';
case 'unknown': return 'reconcile-original-attempt';
case 'succeeded': return 'record-success';
}
}
Wrong: “Retry until the payment passes.” Right: “Determine whether the first attempt committed and follow the agreed safe-retry contract.” An idempotency key at the orchestrator boundary does not automatically deduplicate two different providers unless the system implements that coordination.
Amounts, currency and concurrency#
Do not compare decimal currency with floating-point equality after repeated arithmetic. Use integer minor units or a defined decimal library; currency scale varies and must come from the contract. Check minimum/maximum/zero/negative values, precision, unsupported currency, large values, rounding rules, and JSON serialization limits. Do not assume every currency has two decimal places.
// Standalone exercise: amounts already expressed in declared integer minor units.
export function canRefund(captured: number, refunded: number, requested: number): boolean {
const values = [captured, refunded, requested];
if (!values.every(Number.isSafeInteger)) return false;
return captured >= 0 && refunded >= 0 && requested > 0 && refunded <= captured &&
requested <= captured - refunded;
}
The function checks a local invariant only. Two concurrent refunds can each pass a stale client-side check and together exceed the remaining balance. A backend transactional/concurrency mechanism must enforce the invariant. Test competing requests against one deliberately owned payment and inspect accepted totals and conflict responses.
3DS, redirects and sensitive data#
3-D Secure authentication can introduce browser challenges/redirects and asynchronous outcomes; it is not the same as card authorization or a guarantee of successful payment. In provider sandboxes test frictionless/challenge success, challenge failure, cancellation, timeout, return URL tampering and refresh/back behavior. Do not promise a specific challenge path for every issuer/provider.
Use only test-mode provider instructions and approved synthetic tokens. Prevent raw card/security-code data from appearing in logs, traces, screenshots, URLs or assertions. Do not store payment secrets in fixtures or commit saved authentication state. Compliance scope belongs to the actual integration/security process, not an invented interview claim.
A risk-first sandbox matrix#
| Priority | Scenario | Evidence |
|---|---|---|
| P0 | Approved payment | One business payment, correct amount/currency, agreed final state, proper merchant |
| P0 | Double-click / repeated request | Deduplication under actual key contract, one intended effect |
| P0 | Timeout after possible commit | Reconciliation and late callback; no blind second charge |
| P0 | Cross-merchant lookup/refund | No unauthorized data/action, safe error |
| P0 | Duplicate/out-of-order webhook | No state regression or duplicate ledger effect |
| P1 | Partial capture/refund limits | Correct cumulative totals and concurrency behavior |
| P1 | Provider outage/fallback | Correct eligible route and policy, preserved attempt identity |
| P1 | Routing config change | Controlled version/caching effects and isolated test configuration |
| P1 | UI says success but service pending | Truthful user feedback; authoritative state reconciliation |
Interview answer and references#
30 seconds: “For an orchestrator I test routing correctness and financial integrity. I separate authorization, capture and settlement, track payment versus provider-attempt identity, and prioritize duplicate submissions, uncertain PSP timeouts, webhooks and merchant isolation. A timeout is not proof of failure, so I reconcile before another financial attempt.”
90-second answer: how would you test fallback?
I ask which provider outcomes allow fallback and whether submission definitely happened. With controlled PSP simulators I cover success, explicit rejection, connection failure before submission and timeout after possible acceptance. I inspect route choice, attempt identity, normalized result and audit evidence. In the unknown case I verify the reconciliation and late-callback policy instead of requiring immediate fallback. I test duplicate submissions and conflicting callbacks, then confirm one logical charge in the authoritative sandbox evidence. Real PSP sandbox integration complements simulator tests because mocks cannot prove actual provider contracts or network configuration. I isolate merchant routing configuration and redact sensitive data from reports.
Memory hook: one payment, many observations; unknown is not declined. Provider-specific further reading: Stripe idempotency, Stripe webhooks. These illustrate one provider's contract, not OMNO's or every PSP's semantics. Next: test design.
Quick practice#
What is payment orchestration?
- A unified merchant integration coordinates provider selection and processing
- Verify normalization, configuration and financial effects across provider attempts.
Pitfall: Assuming every PSP has identical semantics.
Authorization versus capture versus settlement?
- Authorization approves or reserves under the contract, capture requests collection and settlement is a later financial stage
- Test each boundary and its own evidence.
Pitfall: Calling an authorization response settled funds.
Payment ID versus attempt ID?
- A payment is a business operation while an attempt represents one provider interaction
- Correlate provider references and effects without treating every attempt as a new purchase.
Pitfall: An ambiguous transaction ID with no system/grain.