by Intelliverse X

The Master Machine Lifecycle

One canonical state machine for every Kiosk-X machine, from capital to decommission, across every installation flavor. Model: app/lifecycle.py. API: app/routes/lifecycle.py. Tests: tests/test_machine_lifecycle.py. Live proof: scripts/lifecycle_flavors_live.py.

Scope note: docs/lifecycle-matrix.md is the device-protocol matrix (first boot, dispense, OTA per Reyeah/ZHZN dialect). This document is the business lifecycle — where a machine is in its LIFE, not whether its motors spin.

The canonical state machine

preorder → ordered → production → shipped → delivered
         → installing → onboarding → live
                                      live ⇄ maintenance   (claims/repair)
                                      live ⇄ relocating    (site moves)
   any non-terminal → retired | resold | returned          (end of life)

Generated diagram

Generated from the live transition tables by app/graph_spec.py — do not edit; run python scripts/gen_state_machine_docs.py to regenerate (CI enforces freshness).

stateDiagram-v2 direction LR [*] --> preorder : dfy / self_install / delegated / concierge [*] --> onboarding : existing preorder : Pre-ordered ordered : Ordered production : In production shipped : Shipped delivered : Delivered installing : Installing onboarding : Onboarding live : Live & selling maintenance : In maintenance relocating : Relocating retired : Retired resold : Resold returned : Returned preorder --> ordered ordered --> production production --> shipped shipped --> delivered delivered --> installing installing --> onboarding onboarding --> live live --> maintenance live --> relocating maintenance --> live relocating --> onboarding relocating --> live note right of live Any non-terminal stage may exit to retired / resold / returned (end of tenure). Pre-live chain allows forward jumps (skip-fill, e.g. ordered → shipped). end note retired --> [*] resold --> [*] returned --> [*]

The same spec (JSON graph + Mermaid) is served live at GET /api/v1/meta/state-machines/machine_lifecycle, and the super-admin console renders it with per-stage live counts. Step replay of any machine's event trail: GET /api/v1/machines/{no}/lifecycle/replay.

Protection claims (maintenance linkage)

Generated from the live transition tables by app/graph_spec.py — do not edit; run python scripts/gen_state_machine_docs.py to regenerate (CI enforces freshness).

stateDiagram-v2 direction LR [*] --> open open --> approved open --> denied approved --> in_repair approved --> resolved approved --> denied in_repair --> resolved resolved --> [*] denied --> [*]

Consumption, not duplication

The DFY deal stage machine (app/dfy.py) and the manufacturer order pipeline (app/dfy_manufacturing.py, deal["manufacturing"]) stay the source of truth for their own flows. lifecycle.sync_from_deal maps them onto canonical stages idempotently and forward-only:

Fragment Canonical stage
deal pre_orderedsigned preorder
deal ready_to_ship / mfg placed, confirmed ordered
mfg in_production, qa production
deal/mfg shipped shipped
mfg delivered delivered
deal installing / mfg installed installing / onboarding
deal/mfg live live

A stale fragment can never rewind the canonical stage. The record migrates from the DFY placeholder pin to the real serial when the wizard links it (serial.linked event). Machines that predate this model backfill lazily on first touch (a claimed machine with settled vends backfills at live; otherwise at onboarding).

Installation flavors

Every record carries the flavor that brought the machine onto the platform, with per-flavor entry points and next actions:

Flavor Entry point Detection
dfy deal → preorder/ordered at pin pre-provision autopilot/placement capability on
self_install deal (order) → ordered; wizard install installMode self, autopilot off
delegated deal → install work order (dfy_install) installMode team / work order present
existing claim → onboarding (order/ship never happened on our books) claimed serial with no deal
concierge any of the above, team-driven dfy_modes concierge active (deal or machine)

Hardware variant (reyeah/zhzn) rides the record too: from the deal's install.hardwareVariant, from the ZHZN gateway tagging (/zhzn/* traffic), or inferred from the Reyeah /apk/apkUp build report.

Protection-plan offer triggers (per flavor)

Crossing a flavor's trigger stage opens the Machine Protection Plan offer (app/protection_offer.py, docs/protection-plan-coverage.md): dfy/concierge/delegated at delivered (exposure starts when freight lands, before install completes); self_install/existing at live (onboarding completion — the guaranteed in-app moment; existing machines never had a delivery on our books). Skip-fill jumps that pass the trigger still fire; loop returns (maintenance → live, relocating → live) never do. Offers, acceptances, declines, and signatures all land on the machine's event trail (protection.offered/.accepted/.declined/.signed), and machines already covered / mid-signature / recently declined are never re-asked.

Onboarding checks (flavor-agnostic first boot)

onboarding → live auto-advances when ALL pass (any lifecycle read reconciles): claimed (not the unclaimed pool), online (APK/agent pointed at the Kiosk-X cloud), paymentsReady (a real reader binding or a supported live QR configuration), planogramLoaded (stock on a usable aisle), boardCanDispense, addressConfirmed, and installationConfirmed. Follow the address and installation guide for the last two. Production refuses forced completion; force is restricted to full administrators in a non-production sandbox. Resolve incomplete readiness, address and on-site evidence through the normal installation workflow. Existing live machines retain their stage and hardware-based sellability while missing location evidence is displayed. Completing a move clears old placement evidence and returns to onboarding until the new site is confirmed.

Lifecycle × flavor matrix — BEFORE → AFTER

Legend: ✅ handled · 🟡 partial/fragmented · ❌ missing.

Stage \ Flavor DFY/autopilot Self-install Delegated Existing (Reyeah/ZHZN) Concierge
preorder 🟡 deal stage only → ✅ 🟡 → ✅ 🟡 → ✅ n/a 🟡 → ✅
ordered/production 🟡 mfg block on deal (no machine view) → ✅ consumed 🟡 → ✅ 🟡 → ✅ n/a 🟡 → ✅
shipped/delivered 🟡 deal shipped (no delivered) → ✅ 🟡 → ✅ 🟡 → ✅ n/a 🟡 → ✅
installing 🟡 deal installing → ✅ 🟡 wizard → ✅ 🟡 WO states unstitched → ✅ stitched (source.workOrderId) n/a 🟡 → ✅
onboarding 🟡 install checks (deal-only) → ✅ 🟡 → ✅ 🟡 → ✅ ❌ nothing after claim → ✅ checklist + auto-advance 🟡 → ✅
live 🟡 dfy.state/online only → ✅ 🟡 online only → ✅ 🟡 → ✅ 🟡 online only → ✅ 🟡 → ✅
maintenance (claims) ❌ no claim workflow → ✅ claims: open→approved→in_repair→resolved/denied + repair WO ❌ → ✅ ❌ → ✅ ❌ → ✅ ❌ → ✅
relocating ❌ advisory only (Yield Scout) → ✅ relocate start/complete mutation ❌ → ✅ ❌ → ✅ ❌ → ✅ ❌ → ✅
retired/resold/returned 🟡 bare decommission → ✅ retire/resell/return + fresh tenure on re-claim 🟡 → ✅ 🟡 → ✅ 🟡 → ✅ 🟡 → ✅

Before: presence lived on machine.status (online/offline), acquisition on the DFY deal, manufacturing on deal["manufacturing"], install progress on work orders, EOL as a one-shot decommission — no single answer to "where is this machine in its life?", and nothing at all for existing-machine onboarding progress, claims, or relocation.

API surface

Endpoint What
GET /api/v1/machines/{no}/lifecycle full view: stage, flavor, rail, legal next, next actions, checks, open claims, event trail (reads reconcile fragments)
POST /api/v1/machines/{no}/lifecycle/transition explicit transition; force admin-only, audited
GET/POST /api/v1/machines/{no}/lifecycle/onboarding[/complete] first-boot checklist / explicit go-live
POST /api/v1/machines/{no}/lifecycle/relocate[/complete] relocation loop; completion writes the new location
POST /api/v1/machines/{no}/lifecycle/retire mode: retired | resold | returned (decommissions + closes tenure)
POST/GET /api/v1/machines/{no}/claims, GET /api/v1/claims, POST /api/v1/claims/{id}/advance protection-plan claims (coverage-gated via app/protection.py; opens/closes a protection_repair work order)
GET/POST /api/v1/machines/{no}/protection-offer[/accept\|/decline] the lifecycle-triggered protection-plan offer at delivery / install completion (app/protection_offer.py; consent-gated accept → Documenso e-sign → rider activates)
GET /api/v1/fleet/lifecycle stage × flavor rollup + per-machine rows

Machine payloads (GET /api/v1/machines[...]) embed a compact lifecycle block (stage, flavor, labels, hardware, terminal).

Persistence & concurrency posture

Stores dfy_lifecycle / dfy_lifecycle_claims live on the FLEET tier, reset-proof, riding the existing dfy_ peer-apply dispatch (registered into dfy.DFY_STORES and appended to persistence.FLEET_STORES at import — zero edits to app/persistence.py, same pattern as dfy_superadmin). Hooks (on_machine_registered, on_machine_decommissioned, on_nayax_bound, on_restock, ZHZN hardware tagging) never raise into their host flows.

Operator surfaces

Proof

Live-run ledger — api.kiosk-x.ai, run 8657613949 (2026-08-12)

Cell Status Evidence
existing-reyeah PASS 866908657613950: first-contact → claim (onboarding) → Nayax + restock → LIVE auto-advance; 4-event trail
relocate PASS live → relocating → live; machine.location updated to the destination
claims-gate PASS claim without coverage → 409 with enroll pointer (full covered-claim loop proven in pytest)
fsm-guard PASS live → shipped refused: 409 naming the legal targets
eol-resale PASS resold → serial unclaimed → second operator re-claims → FRESH tenure at onboarding
rollup PASS operator rollup byStage={live:1}, byFlavor={existing:1}
dfy/delegated PASS admin rollup: 54 machines; 7 deal-driven (self_install:4, delegated:3) consumed onto the canonical lifecycle from real deals on this stack
self-install entry PASS deal dfy_ab38808967: pre-order paid via real Stripe TEST checkout (4242 card, scripts/dfy_stripe_checkout.py) → order=paid, stage pre_ordered
concierge entry PASS same deal: concierge ACTIVE ($99/mo) ⇒ flavor concierge at pin time
existing-zhzn PASS LCF-ZH-8657773248: ZHZN register → claim → LIVE, hardware tagged zhzn (2026-08-12 run 8657773248). KIOSKX_ZHZN_DEVICE_SECRET now CI-bootstrapped into kiosk-x-secrets + wired in the deployment (kube-infra b534dd65)

Deal flavors past the pre-order checkpoint (lease signature → pin → manufacturing consumption) run through real Documenso signatures by design; that stretch is proven end-to-end in tests/test_machine_lifecycle.py (deal + manufacturing sync tests) and by the 7 deal-driven machines already on the live rollup above.