State Machines
Every order/resource state machine — voucher orders (order-level + item checkpoints + voucher links), recharge (mobile & gaming), eSIM (status/sub-status/activation), and payouts — with enum tables, transition tables, diagrams, and the dead-state catalog.
Every state machine in the platform, with the exact (status, sub_status) pairs that are actually written and which enum values are dead. Each resource has its own machine; they don't share code.
A lot of enum values are dead
Across all four machines, many declared statuses/sub-statuses are never written — reserved, half-built, or transition-map-only. Each section flags them, and there's a consolidated dead-state catalog at the end. When reading the code, don't assume an enum value is reachable just because it's defined.
Detailed operational traces live in Order Lifecycle, Top-up & eSIM Flows, Payouts, and Admin Order Actions. This page is the formal state reference.
Voucher orders
Three nested machines: the order, its items (3-checkpoint), and — for link orders — voucher_links.
Order-level
OrderStatus (enum.go:13) × OrderSubStatus (enum.go:58). The only pairs written at runtime:
| status | sub_status | Set where |
|---|---|---|
PENDING | INITIAL | create (create_voucher_order.go:203), reset |
PENDING | VENDOR_ORDER_PENDING | processOrderDelivery default (:958) |
DELIVERED | COMPLETED | processOrderDelivery (:948) |
PARTIALLY_DELIVERED | PARTIAL | processOrderDelivery (:953) |
CANCELLED | REFUNDED | admin cancel (admin_ui.go:2734) |
CANCELLED/FAILED | REFUNDED | admin refund flips the sub (admin_ui.go:3071) |
The order status is recomputed, not transitioned — getOrderDeliveryStatus (create_voucher_order.go:272) is a pure count-based function returning only PENDING / DELIVERED / PARTIALLY_DELIVERED:
count != Quantity→ PENDING (partial item creation).- delivered==0 → PENDING; delivered>0 with any pending OR failed → PENDING (failed items keep the order retryable).
- delivered>0 AND cancelled>0 AND no pending/failed → PARTIALLY_DELIVERED — the only path to partial.
- all delivered → DELIVERED.
Order-level FAILED is unreachable
No runtime path sets an orders row to FAILED — items fail (VENDOR_ORDER_FAILED), but the order stays PENDING (retryable). FAILED is only read (the admin-refund gate at admin_ui.go:2953 even checks for it, but nothing produces it — a dead branch). The only terminal non-success at the order level is admin CANCELLED. Likewise PLACED is never written to an order (dashboard buckets + tests only). And AllowedSubStatusTransitions (order.go:22) is dead code — it's not the enforced guard; the checkpoints + recompute are.
Item checkpoints
Items progress INITIAL → VENDOR_ORDER_PENDING → VENDOR_ORDER_CREATED → COMPLETED (or → VENDOR_ORDER_FAILED) through 3 idempotent checkpoints (bulk_vendor_order_checkpoints.go / individual_vendor_order_checkpoints.go). Dispatch groups items by vendor, then vendor.IsBulk picks bulk (whole group per call) vs individual (per item, parallelism PLimit).
- CP1 (auth + availability) →
VENDOR_ORDER_PENDING. Skip-if-done: skips items already pending/created. - CP2 (create vendor order) →
VENDOR_ORDER_CREATEDwithvendor_order_id. Skip-if-done keys on the vendor-assignedvendor_order_id(not the ref code) so a lost response never duplicates. Ref-support vendors (e.g. Wupex) pre-persist the reference code before CreateOrder so a retry recovers viacheckExistingVendorOrderinstead of creating a dupe. Inline-voucher vendors jump straight to DELIVERED, skipping CP3. - CP3 (assign vouchers) →
DELIVERED/COMPLETED(note: jumps CREATED→COMPLETED —VENDOR_VOUCHER_FETCHEDis skipped).ErrShouldReCreateclearsvendor_order_id+ref (magic"NULL"), bounded by recreateBudget = 5; on exhaustion the order parks at PENDING for admin (never auto-FAILED).ErrVendorOrderTerminal→ items FAILED.
Voucher links (GV-Link)
Link orders skip items/vendors entirely — createVoucherLinks writes one voucher_links row per unit, UNCLAIMED, expires_at = now+1yr, encrypted token + (non-PUBLIC) a 6-digit PIN. Order status derives from links: all UNCLAIMED/CLAIMED and count==Quantity → DELIVERED; else PENDING. Links never produce PARTIALLY_DELIVERED, and async is disabled for links.
VoucherLinkStatus (voucher_link.go:10): only UNCLAIMED is written by this repo — CLAIMED happens in the external claim frontend (Grasshopper); REFUNDED/NOT_REFUNDED are read to hide refunded links; PROCESSING/ERROR are unused here.
Recharge (mobile & gaming top-up)
Mobile and gaming share one machine
Both flavors are the same recharges row and the same states. They differ only in: catalog category (TopupType MOBILE / GAMING / UTILITY), the input-field schema (topup_product_input_fields — phone/MSISDN vs player/server/zone), whether the mobile lookup pre-step runs, and the topup_unit display. No status/transition code branches on TopupType. LookupMobileNumber is a read-only pre-order helper (MOBILE only) that never touches the state machine.
RechargeStatus (enum.go:319). There is no sub_status column — the admin "refunded" flag rides on a [REFUNDED] prefix in status_text.
| Value | Terminal | Live? |
|---|---|---|
PENDING | no | ✅ initial + retry-eligible |
DELIVERED | yes | ✅ |
FAILED | yes | ✅ triggers auto-refund |
CANCELLED | yes | ✅ |
RECHARGED | — | ☠️ dead — never written |
Create inserts PENDING, then the inline vendor call classifies (nil/transport error → stays PENDING for the cron). The cron path applyTopupVendorStatus is row-locked with a terminal short-circuit and auto-refunds on FAILED inside the lock. The "PROCESSING" literal guard at topup.go:711 is dead — no mapper ever yields it.
The webhook path (ProcessDirectTopUpWebhook) is the exception — not row-locked, no terminal short-circuit, no auto-refund. See Top-up & eSIM → concurrency hazard.
eSIM
Three independent axes on esim_orders: status, sub_status, activation_status.
Status (esim_order.go:14): PENDING / DELIVERED / FAILED / CANCELLED (last three terminal). Sub-status (esim_order.go:39): INITIAL, VENDOR_ORDER_PENDING, VENDOR_ORDER_CREATED, VENDOR_CODE_FETCHED, VENDOR_ORDER_FAILED, REFUNDED — all live; NOT_REFUNDED and COMPLETED are dead (never assigned).
Dispatch is a two-phase poll-before-recreate (DispatchEsimOrder): poll by vendor_order_id, then by merchant_ref (definitive not-found), then recreate with EsimRecreateBudget = 5 (parks 6h on exhaustion). persistDispatchOutcome is row-locked with a terminal short-circuit — the load-bearing double-refund guard — and auto-refunds on FAILED inside the lock. The delivered webhook maps to PROCESSING (not terminal) — a nudge, since it can't carry the activation code.
Activation (esim_order.go:89), the post-delivery axis:
Driven by the esim-installation-poll (5-min) and esim-expiry (daily) crons — see Cron Catalog.
Payouts
The engine is designed but dead
The payout status enum is a full pipeline, but only created and cancelled are reachable at runtime. QueuePayout has no caller, and the payout-processor + payout-status-sync crons are never registered — so created → queued → processing → settling → completed/failed never happens. A created payout debits the wallet then either gets cancelled or sits in created forever. Refund and expiry are vaporware (column/event/status exist, nothing writes them). See Payouts.
PayoutStatus (payout.go:17): created, queued, processing, settling, action_required, completed, failed, cancelled, expired. IsCancellable = created/queued/processing/action_required (not settling).
Only two PayoutEvents ever hit the DB: payout.created and payout.cancelled. Provider status maps (Merit/Ledig → internal) exist but are only reachable through the dead sync cron.
Scheduled payouts are, by contrast, fully live (driven by the registered scheduled-payout-processor):
ScheduledPayoutStatus: active / paused / completed / cancelled / exhausted.
Each fire calls CreatePayout — so every successful scheduled execution produces another stuck created payout. Execution rows record success/failed/skipped.
Dead-state catalog
Everything declared but never written at runtime, in one place — safe to prune once confirmed:
| Machine | Dead value(s) |
|---|---|
| Voucher order status | FAILED (never written), PLACED (dashboard/test only) |
| Voucher order sub | PROCESSING, RECONCILED, BULK_LIMIT_EXCEEDED, GV_LINK_PENDING/CLAIMED/UNCLAIMED, VENDOR_VOUCHER_FETCHED (read-only), NOT_REFUNDED (item pre-seed only) |
| Voucher order | AllowedSubStatusTransitions map — entirely dead (not the guard) |
| Voucher links | CLAIMED/PROCESSING/ERROR never written here (external claim frontend) |
| Recharge | RECHARGED; the "PROCESSING" literal guard |
| eSIM sub | NOT_REFUNDED, COMPLETED |
| Payout status | queued/processing/settling/action_required/completed/failed/expired unreachable (engine dead); refund never written |
| Payout events | all except payout.created + payout.cancelled |
Key files
- Enums:
database/models/{enum,esim_order,payout,scheduled_payout,voucher_link}.go - Voucher:
http/handler/{create_voucher_order,bulk_vendor_order_checkpoints,individual_vendor_order_checkpoints,order_state_machine,order}.go - Recharge/eSIM:
services/{topup_order,esim_order}.go,http/handler/{topup,esim}.go,services/vendor_webhook_service.go - Payout:
services/{payout_service,scheduled_payout_service}.go
Top-up & eSIM Flows
The two single-unit order resources alongside the voucher engine — recharges and esim_orders. Their sync/hybrid-sync flows, the recreate-budget recovery model, the delivered=PROCESSING nudge, retry crons, and where they diverge from vouchers.
Inventory Flow
How pre-loaded voucher codes move DB → Valkey pools → orders. The exact pool key formats, the ZSET FEFO (first-expiry) allocation, the two-phase reserved/general algorithm, prefetch, import, tagging, and the ALLOCATED leak.