Skip to main content

Service ticket lifecycle

A service ticket moves through a small set of statuses. Each transition is logged to service_ticket_status_history and audited.

v0.2 — Ready renamed to Completed; new Notified state inserted (2026-05-24, migration 056)

The workflow has a step the v1 status set didn't: after work is complete, the shop calls or texts the customer to tell them the bike is ready. That's a distinct state from "ready but customer doesn't know yet" — important operationally because the owner can see who hasn't been reached yet. Code-side, 'ready' keeps the same string so existing transition-map JS keeps compiling; only the display label is now 'Completed'. The new state is 'notified', slotted between ready (Completed) and picked_up.

v0.3 — "Awaiting parts" relabelled "Parts - Hold"; new invoice state inserted between Notified and Picked Up (2026-05-25)

Two refinements landed: migration 061 updates the awaiting_parts display name to "Parts - Hold" (operator request — the new label reads as the actionable state, not a passive observation). Code stays 'awaiting_parts' so transition maps keep compiling. Migration 067 inserts a new invoice status (display 'Invoice', display_order=55, amber #f59e0b) between notified and picked_up — the missing "customer is ringing it up but the bike is still in the shop" moment. The auto-park trigger moves from picked_up → invoice; clicking Invoice on the Slot switches to the Sales tab where the auto-created parked sale is waiting. Invoiced ≠ finished — the Slot goes read-only on invoice to enforce the doctrine (money flowing at the till; bike still on the floor). Picked Up stays the terminal "bike out the door" state.

States and transitions

State definitions

State (code)UI labelWhat it means
dropped_offDropped OffCustomer left the bike; ticket is awaiting work
quotedQuotedMechanic provided a quote; awaiting customer approval (optional state)
in_progressIn ProgressA mechanic is actively working on the bike
awaiting_partsParts - HoldWork is on hold waiting for parts to arrive (label updated in migration 061; code stays 'awaiting_parts')
readyCompletedWork is complete; customer has not yet been told
notifiedNotifiedCustomer has been called or texted; bike is awaiting pickup (teal #14b8a6)
invoiceInvoiceCustomer is at the till ringing it up; parked sale auto-created on the Sales ledger; Slot is read-only. Bike is still in the shop (amber #f59e0b) (migration 067)
picked_upPicked UpCustomer paid and took the bike; terminal
cancelledCancelledTicket was abandoned or refused; terminal

Why ready keeps its code name. Migration 056 only changed the display_name column — the status code is still 'ready' so every transition-map / state-machine / audit-event-name in src/index.js and public/index.html keeps working without a rename. Operators see Completed; code reads ready.

Transition rules

  • Every transition requires a staff id (no system-initiated transitions during normal flow; cron may flag abandoned tickets but doesn't move them)
  • dropped_offquoted is optional; many tickets skip directly to in_progress
  • awaiting_parts is a sub-state of "in flight"; counts as work-in-progress on the kanban
  • readyin_progress is the "reopen" path; common when a customer test-rides and reports an issue
  • picked_up and cancelled are terminal; only an audit-correction admin action can change them after the fact

Side effects of transitions

TransitionSide effects
* → in_progressIf assigned_staff_id is null, set it to the transitioning staff
* → ready (Completed)Trigger optional SMS to customer (if opted-in); write to service_ticket_messages regardless. Also upserts a Service parked sale on the Sales ledger via POST /api/parked-sales/upsert-by-ticket (deduped by ticket_id) so the cashier has a row to resume when the customer comes to pay — see multi-user realtime
notified → invoiceAuto-park trigger fires (parked sale upserted if not already there); operator is switched to the Sales tab; Slot goes read-only (the parked sale is the source of truth for the lines while it's at the till)
invoice → picked_upTriggered by apiTicketsInvoice server-side when the parked sale is consumed; transaction is linked via service_tickets.transaction_id
invoice → notifiedWhen the cashier cancels the parked sale on the Sales side, the ticket flips back from Invoice and the Slot's read-only lock clears
in_progress → cancelledRelease any committed parts inventory (qty_committed → 0); optional refund of pre-paid amounts
awaiting_parts → in_progressNo side effects; the inventory state already updates as the part arrives

Late-status detection

A daily cron flags tickets that have been in in_progress, awaiting_parts, or ready longer than the per-status SLA (configurable in shop_config). Surfaces on the Today dashboard as "Late tickets" card.

Default SLAs:

  • in_progress > 3 days: warning
  • awaiting_parts > 7 days: warning (parts on order — owner may need to follow up with vendor)
  • ready > 5 days: warning (customer hasn't picked up — send a reminder SMS)

Audit

Every transition writes:

  • service_ticket_status_history row: {ticket_id, from_status, to_status, staff_id, at, notes?}
  • audit_events row: action='ticket.status', entity_id=ticket_id
  • audit_mutations row: before/after of the service_tickets row

See also