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.
| Entity | States | Terminal or important transitions |
|---|---|---|
| Bill | RECEIVED, VALIDATED, SCHEDULED, COLLECTING, COLLECTED, FAILED, CANCELLED, EXCEPTION | RECEIVED → VALIDATED → SCHEDULED → COLLECTING; COLLECTED means collection confirmed, not payout completed |
| Attempt | CREATED, SUBMITTED, PENDING, SUCCEEDED, DECLINED, FAILED, UNKNOWN | UNKNOWN must be resolved by provider lookup/reconciliation; no blind resubmission |
| Collection | PENDING, CONFIRMED, SETTLED, ELIGIBLE, REVERSED, DISPUTED | Only ELIGIBLE funds can be reserved for payout |
| Payout | DRAFT, RESERVED, SUBMITTED, PROCESSING, COMPLETED, FAILED, RETURNED | Submitted is not completed; return is separate from failed initiation |
| Mandate | PENDING, ACTIVE, SUSPENDED, REVOKED, EXPIRED | No new attempts after revocation effective time |
Refunds and disputes use their own state machines, including asynchronous provider outcomes.
Attempt decision logic
- Acquire bill-level concurrency lock or equivalent transactional lease.
- Load latest bill version and mandate; reject stale job payloads.
- Check due date, collection window, amount ceiling, method eligibility, revocation and prior terminal success.
- Create attempt and provider idempotency key in one database transaction; enqueue external call via outbox.
- Submit to provider with stable key. On network timeout, mark UNKNOWN, not FAILED.
- Verify signed provider events, deduplicate by provider event ID, apply monotonic transitions.
- For eligible decline only, calculate next authorised method or retry with policy constraints.
- Once provider confirms settled eligible funds, create collection and ledger posting exactly once.
Retry classification
| Outcome | Default behaviour |
|---|---|
| Hard decline / stolen or closed account | No retry of that method |
| Insufficient funds / transient decline | Retry only within provider rules, consent and merchant policy |
| Authentication required | Prompt consumer; do not bypass required authentication |
| Timeout / unknown | Query provider before considering another attempt |
| Duplicate provider event | Acknowledge without side effects |
| Mandate revoked / bill cancelled | Stop future attempts, resolve any already in flight |
| Provider outage | Queue/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.