OctoWiki

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:

statussub_statusSet where
PENDINGINITIALcreate (create_voucher_order.go:203), reset
PENDINGVENDOR_ORDER_PENDINGprocessOrderDelivery default (:958)
DELIVEREDCOMPLETEDprocessOrderDelivery (:948)
PARTIALLY_DELIVEREDPARTIALprocessOrderDelivery (:953)
CANCELLEDREFUNDEDadmin cancel (admin_ui.go:2734)
CANCELLED/FAILEDREFUNDEDadmin refund flips the sub (admin_ui.go:3071)

The order status is recomputed, not transitionedgetOrderDeliveryStatus (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_CREATED with vendor_order_id. Skip-if-done keys on the vendor-assigned vendor_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 via checkExistingVendorOrder instead of creating a dupe. Inline-voucher vendors jump straight to DELIVERED, skipping CP3.
  • CP3 (assign vouchers) → DELIVERED/COMPLETED (note: jumps CREATED→COMPLETED — VENDOR_VOUCHER_FETCHED is skipped). ErrShouldReCreate clears vendor_order_id+ref (magic "NULL"), bounded by recreateBudget = 5; on exhaustion the order parks at PENDING for admin (never auto-FAILED). ErrVendorOrderTerminal → items FAILED.

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.

ValueTerminalLive?
PENDINGno✅ initial + retry-eligible
DELIVEREDyes
FAILEDyes✅ triggers auto-refund
CANCELLEDyes
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:

MachineDead value(s)
Voucher order statusFAILED (never written), PLACED (dashboard/test only)
Voucher order subPROCESSING, RECONCILED, BULK_LIMIT_EXCEEDED, GV_LINK_PENDING/CLAIMED/UNCLAIMED, VENDOR_VOUCHER_FETCHED (read-only), NOT_REFUNDED (item pre-seed only)
Voucher orderAllowedSubStatusTransitions map — entirely dead (not the guard)
Voucher linksCLAIMED/PROCESSING/ERROR never written here (external claim frontend)
RechargeRECHARGED; the "PROCESSING" literal guard
eSIM subNOT_REFUNDED, COMPLETED
Payout statusqueued/processing/settling/action_required/completed/failed/expired unreachable (engine dead); refund never written
Payout eventsall 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

On this page