No transaction yet. Pick a scenario and run a payment.
Run a payment to see the calls it makes.
The same seed and the same faults produce the same run, delays included.
Authorization never answers. The request may still have been processed.
Authorization returns 502. Nothing happened on the acquirer side.
The capture call never answers. Nobody knows whether the money moved.
The settlement webhook is delivered twice, sharing one event id.
A late authorization webhook lands after settlement has completed.
Faults are read only by the acquirer adapters. The orchestration code cannot see this panel — it reacts to what the provider interface returns.
Nothing run yet in this session.
No transaction yet. Pick a scenario and run a payment.
Run a payment to see the calls it makes.
Every state change goes through one transition table. A payment cannot reach COMPLETED without having been CAPTURED, and no branch of the code can invent a shortcut, because there is no code path that writes a state directly.
DECLINED and FAILED are separate terminal states. One is a decision the acquirer made about the money; the other is infrastructure falling over. Retrying the second is correct. Retrying the first is, at best, rude.
Outbound, every authorization attempt carries the same key, so an acquirer that already decided replays that decision instead of charging twice. Inbound, every webhook carries an event id, so a redelivery is recognised and dropped.
Three attempts per provider, doubling the wait each time, with half the window randomised. Jitter is not decoration: without it, everything that failed together retries together, and the recovering provider is knocked over by its own clients.
When a call times out, the request may well have been processed — the answer is what got lost. That is why a timed-out capture moves to CAPTURE_PENDING rather than FAILED, and why retrying one is only safe behind an idempotency key.
An unavailable provider is not retried, it is replaced. But the second acquirer has never seen the idempotency key the first one holds, so failover trades one risk for another — which is exactly why it is reserved for providers that are genuinely down.
Settlement arrives by webhook, whenever it arrives. The payment sits at CAPTURED in the meantime, which is correct rather than stale. A late webhook aiming at a stage already passed is refused by the same table that governs everything else.
When nobody knows what happened, the answer is to go and ask, not to guess. Reconciliation queries the provider and accepts three answers: it was captured, it was not, or it is still unknown — and the third leaves the payment exactly where it was.
Nothing stores a status field. The payment is whatever reducing its events produces, which is why a page reload reconstructs it exactly, and why the timeline can never disagree with the badge next to it.