Core
Orders
Order Lifecycle

Order lifecycle

The unified state machine of an Order across every ingestion channel (manual, public store, WhatsApp, Instagram). Audience: new dev with Node/React experience, no Eziseller context.

1. Overview

An Order is the canonical record of a customer purchase — the hand-off point between sales channels (messaging, storefront, dashboard) and fulfillment (invoice, shipping label, payment reconciliation). Orders can be created directly (manual dashboard entry, public checkout) or indirectly via a DraftOrder that is parsed from a WhatsApp/Instagram message and later approved by the seller. Once persisted, every Order follows a single 8-state OrderStatus machine, with two orthogonal sub-states for financialStatus and fulfillmentStatus. This doc is the single source of truth for "what state can an order be in, and how does it get there?"

2. Architecture

Manual and public checkout write directly to orders; messaging channels first produce a DraftOrder with a confidence score, which the seller reviews before it is converted. Fulfillment artefacts (invoice, label, tracking) hang off the Order and gate further state transitions.

3. Data model

See schema.prisma:L401-L517 for field-level detail. Order.customer is a denormalised JSON blob (not a FK to Customer) — the Customer model is a separate CRM-style index populated from order snapshots.

4. Key flows

4.1 Draft approval (WhatsApp/Instagram)

Implemented in draft-order-service.ts:L211-L318. The draft fetch, claim, order insert, and back-link all run inside a single prisma.$transaction — see gotchas.

4.2 Status transition (generic)

See routes/orders.ts:L933-L1069. Legal values are enforced by an allow-list, not the Prisma enum alone.

5. Lifecycle / state machine

Two entry states exist for historical reasons: direct-created orders default to pending (schema.prisma:L408), while orders converted from an approved draft are created as new (draft-order-service.ts:L257). The "can this order progress?" logic in routes/orders.ts:L19-L81 gates confirmed -> packed on the presence of invoiceUrl and labelUrl/fulfillment, but other transitions are currently open (any valid enum value is accepted by PUT /:id/status). Terminal states: delivered, cancelled, refunded.

Parallel sub-state machines:

  • financialStatus: pending | paid | partially_paid | refunded | partially_refunded — driven by PaymentTransaction rows, updated via the same status endpoint.
  • fulfillmentStatus: unfulfilled | partial | fulfilled — driven by Fulfillment rows.

Edits to order items/customer are blocked once status is in a non-editable terminal set (see routes/orders.ts:L690-L697).

6. Key files

7. Env vars & config

No env vars are specific to the order state machine itself. Related subsystems pull their own config:

VarRequiredPurposeWhat breaks
DATABASE_URLyesPostgres connectionAll order reads/writes
FRONTEND_URLyesCORS allow-listDashboard cannot call status endpoint

Draft expiry is hard-coded to 24h (draft-order-service.ts:L72-L74).

8. Gotchas & troubleshooting

  • Concurrent draft approvals would create two orders. Cause: the draft fetch and order insert used to run outside a transaction. Fix (commit 7e74ee6): everything including the initial findFirst now runs inside prisma.$transaction, and the draft is immediately updated to approved to claim the row (draft-order-service.ts:L214-L245). Do not re-introduce a pre-transaction fetch.
  • Two entry states (new vs pending). Draft-sourced orders start at new, all others at pending. UI filters that assume a single "just created" status will miss half the orders.
  • Status enum is validated twice. Prisma enforces the enum, but the route also keeps its own allow-list (routes/orders.ts:L941-L943). Adding a value to the enum without updating this list causes a 400 at runtime.
  • Inventory is only restored on cancelled, never on refunded. See routes/orders.ts:L1020-L1037. If a shipped order is refunded, stock is not automatically returned.
  • Order.customer is denormalised JSON. Updating the Customer CRM record does not retroactively change past orders — snapshots are intentional.
  • Legacy createDirectOrderFromMessage still exists (order-creation-service.ts:L53) but the default path routes through drafts. Do not call it from new code.

9. Extension points

  • To add a new ingestion source: add a value to OrderSource in schema.prisma, create a channel-specific service that calls order-helpers / DraftOrderService, and wire it into any dashboards filtering by source.
  • To add a new status: extend OrderStatus, update the allow-list in routes/orders.ts, add a case in calculateCanProgress, and update the frontend status badge map. Consider whether it is terminal (blocks edits).
  • To add transition guards: extend calculateCanProgress and reject disallowed transitions in PUT /:id/status — the endpoint currently only validates enum membership, not state-machine legality.

10. Related docs