Skip to main content

Payment state machines

Status: Proposed engineering contract. Provider/network rules, mandate language and collection cutoffs remain open decisions.

Distinct state machines​

Do not represent bill, attempt, collection, settlement and payout using one status.

EntityStatesTerminal or important transitions
BillRECEIVED, VALIDATED, SCHEDULED, COLLECTING, COLLECTED, FAILED, CANCELLED, EXCEPTIONRECEIVED → VALIDATED → SCHEDULED → COLLECTING; COLLECTED means collection confirmed, not payout completed
AttemptCREATED, SUBMITTED, PENDING, SUCCEEDED, DECLINED, FAILED, UNKNOWNUNKNOWN must be resolved by provider lookup/reconciliation; no blind resubmission
CollectionPENDING, CONFIRMED, SETTLED, ELIGIBLE, REVERSED, DISPUTEDOnly ELIGIBLE funds can be reserved for payout
PayoutDRAFT, RESERVED, SUBMITTED, PROCESSING, COMPLETED, FAILED, RETURNEDSubmitted is not completed; return is separate from failed initiation
MandatePENDING, ACTIVE, SUSPENDED, REVOKED, EXPIREDNo new attempts after revocation effective time

Refunds and disputes use their own state machines, including asynchronous provider outcomes.

Attempt decision logic​

  1. Acquire bill-level concurrency lock or equivalent transactional lease.
  2. Load latest bill version and mandate; reject stale job payloads.
  3. Check due date, collection window, amount ceiling, method eligibility, revocation and prior terminal success.
  4. Create attempt and provider idempotency key in one database transaction; enqueue external call via outbox.
  5. Submit to provider with stable key. On network timeout, mark UNKNOWN, not FAILED.
  6. Verify signed provider events, deduplicate by provider event ID, apply monotonic transitions.
  7. For eligible decline only, calculate next authorised method or retry with policy constraints.
  8. Once provider confirms settled eligible funds, create collection and ledger posting exactly once.

Retry classification​

OutcomeDefault behaviour
Hard decline / stolen or closed accountNo retry of that method
Insufficient funds / transient declineRetry only within provider rules, consent and merchant policy
Authentication requiredPrompt consumer; do not bypass required authentication
Timeout / unknownQuery provider before considering another attempt
Duplicate provider eventAcknowledge without side effects
Mandate revoked / bill cancelledStop future attempts, resolve any already in flight
Provider outageQueue/backoff with circuit breaker and cutoffs

Never assume a fixed number of retries is universally permissible. Record retry policy version and reason per decision.

Acceptance tests​

  • 100 duplicate bill requests create one bill.
  • Two simultaneous workers cannot charge the same bill twice.
  • Provider timeout followed by successful webhook creates one collection.
  • Webhook before synchronous API response is handled.
  • Replayed and out-of-order webhooks do not regress terminal state.
  • Revoked mandate prevents new attempts.
  • Successful collection cannot be re-attempted after delayed failure event.
  • Cancelled bill with in-flight attempt is sent for exception review, not assumed cancelled.