This is the canonical reference for operational status workflows in IMS. The
source of truth is the state-machine code in lib/domain/workflows; the tables
below are generated from those transition constants with npm run docs:workflows.
Workflow guards are used by mutation paths that change sales order, shipment, purchase order, refund-derived, and stock transfer statuses. UI labels and help articles should stay high level and link here instead of duplicating transition rules.
| Status | Allowed next statuses | Notes |
|---|---|---|
ALLOCATED |
PICKING, PROCESSING, SHIPPED, CANCELLED, ON_HOLD, PARTIALLY_REFUNDED, REFUNDED |
- |
CANCELLED |
None | Terminal. |
COMPLETED |
DELIVERED, PARTIALLY_REFUNDED, REFUNDED |
- |
DELIVERED |
PARTIALLY_REFUNDED, REFUNDED |
- |
DRAFT |
PROCESSING, PENDING_PAYMENT, ALLOCATED, CANCELLED, ON_HOLD, PARTIALLY_REFUNDED, REFUNDED |
- |
ON_HOLD |
DRAFT, PENDING_PAYMENT, PROCESSING, ALLOCATED, PICKING, PACKING, CANCELLED, PARTIALLY_REFUNDED, REFUNDED |
- |
PACKING |
SHIPPED, CANCELLED, ON_HOLD, PARTIALLY_REFUNDED, REFUNDED |
- |
PARTIALLY_REFUNDED |
REFUNDED |
Can move only to REFUNDED. |
PENDING_PAYMENT |
PROCESSING, DRAFT, ALLOCATED, CANCELLED, ON_HOLD, PARTIALLY_REFUNDED, REFUNDED |
- |
PICKING |
PACKING, SHIPPED, CANCELLED, ON_HOLD, PARTIALLY_REFUNDED, REFUNDED |
- |
PROCESSING |
ALLOCATED, CANCELLED, ON_HOLD, PARTIALLY_REFUNDED, REFUNDED |
- |
REFUNDED |
None | Terminal. |
SHIPPED |
COMPLETED, DELIVERED, PARTIALLY_REFUNDED, REFUNDED |
- |
The DELIVERED transition can be driven automatically by delivery-tracking
polling (TrackShip or the active shopping connector, via /api/cron/delivery-status).
That cron path runs the same transition guard and side effects as a manual
delivery — it does not write the status directly. Because the cron has no user
session it skips only the permission check (not the state-machine guard, unlike
the WooCommerce status-sync path), so if an order has moved out of a deliverable
state (e.g. cancelled or refunded after dispatch) between the poll's SHIPPED
query and the under-lock write, the guard rejects the change and the cron logs a
delivery_status_skipped warning instead of forcing it.
COMPLETED vs DELIVERED. Both are manual, operator-set terminal-ish
statuses reached from SHIPPED (and COMPLETED → DELIVERED). DELIVERED means
the carrier confirmed delivery and can be set automatically by the delivery-status
cron; COMPLETED is a manual "this order is done from our side" marker for
businesses that don't track delivery (no automatic trigger sets it). Neither is
forced by any sync; both still allow PARTIALLY_REFUNDED/REFUNDED afterwards.
Use DELIVERED when delivery tracking is in play, COMPLETED otherwise.
Manual status edits are rejected on archived orders (unarchive first);
automated pushes (WooCommerce force-sync, the delivery-status cron) still apply.
Deleting a payment that takes an already-paid order in an advanced status
(SHIPPED/COMPLETED/DELIVERED/PARTIALLY_REFUNDED) back below fully-paid
does not auto-revert the status but raises a payment_status_mismatch warning
activity log so the operator can decide (it fires only on a genuine paid→unpaid
transition, not for orders that were never fully paid, e.g. credit terms).
| Status | Allowed next statuses | Notes |
|---|---|---|
PACKED |
SHIPPED |
- |
PENDING |
PICKING |
- |
PICKING |
PACKED |
- |
SHIPPED |
None | Terminal; stock dispatch and cost-layer snapshots happen on this transition. |
| Status | Allowed next statuses | Notes |
|---|---|---|
CANCELLED |
None | Terminal. |
CLOSED |
None | Terminal. |
DRAFT |
RFQ_SENT, PO_SENT, CANCELLED |
- |
INVOICED |
PARTIALLY_RETURNED, RETURNED, CLOSED |
- |
PARTIALLY_RECEIVED |
RECEIVED, PARTIALLY_RETURNED, RETURNED, CLOSED, CANCELLED |
- |
PARTIALLY_RETURNED |
RETURNED |
- |
PO_SENT |
SHIPPED, PARTIALLY_RECEIVED, RECEIVED, PARTIALLY_RETURNED, RETURNED, CLOSED |
- |
QUOTE_RECEIVED |
PO_SENT, CLOSED |
- |
RECEIVED |
INVOICED, PARTIALLY_RETURNED, RETURNED, CLOSED |
- |
RETURNED |
None | Terminal. |
RFQ_SENT |
QUOTE_RECEIVED, PO_SENT, CLOSED |
- |
SHIPPED |
PARTIALLY_RECEIVED, RECEIVED, CLOSED |
- |
| Status | Allowed next statuses | Notes |
|---|---|---|
CREDIT_NOTE_SYNCED |
PAID |
Derived from accounting credit-note sync state. |
PAID |
None | Terminal; derived from linked refund payments. |
RECORDED |
CREDIT_NOTE_SYNCED, PAID |
Derived when a SalesOrderRefund row exists. |
| Status | Allowed next statuses | Notes |
|---|---|---|
CANCELLED |
None | Terminal. |
DRAFT |
IN_TRANSIT, CANCELLED |
- |
IN_TRANSIT |
RECEIVED |
- |
RECEIVED |
None | Terminal. |
Sales order status tracks the commercial order lifecycle. Shipment status tracks physical fulfilment for one warehouse shipment.
Order status and shipment status are intentionally separate. Shipment SHIPPED
performs stock dispatch and cost-layer snapshot work. Sales order SHIPPED is
an aggregate state that is reached only after shipment rows exist and all
shipments have reached shipment SHIPPED.
PARTIALLY_REFUNDED and REFUNDED are order statuses set by refund creation.
REFUNDED is terminal; PARTIALLY_REFUNDED can move to REFUNDED when later
refunds bring the total refunded amount up to the order total.
Purchase order status tracks supplier procurement, shipment, receipt, invoicing,
closure, and return state. Receipt actions move orders to PARTIALLY_RECEIVED
or RECEIVED; supplier return actions can move eligible orders to
PARTIALLY_RETURNED or RETURNED.
Returns vs. supplier bills. A return reverses stock and FIFO cost layers but
does not adjust supplier invoices already recorded against the PO. When a
return leaves a line billed for more than the quantity now kept
(received − returned), the PO detail page shows an amber over-billing alert and a
return_overbilled_bill WARNING activity-log row (naming the bills and the
over-billed amount). Reducing the AP liability is a manual step — raise a
supplier credit; IMS does not yet post a credit memo automatically (audit-C4).
Cancellation and already-consumed cost. Cancelling a PO reverses the
remaining (still-on-hand) receipt cost layers. Units already sold or used keep
their COGS booked against the cancelled receipt. When that consumed quantity is
non-zero, cancellation emits a cancelled_consumed_cogs_standing WARNING and the
UI surfaces the consumed units and value for finance review — IMS does not
reverse COGS for stock that has already left (audit-H8). Cancellation remains
blocked entirely once a supplier invoice exists.
Refunds do not currently have a persisted status column. The refund workflow is
derived from SalesOrderRefund, accounting credit-note sync state, and linked
refund payments.
Stock transfer status tracks inter-warehouse movement. IN_TRANSIT means stock
has left the source warehouse and is not yet available at the destination.
Stock / concurrency invariants (audit-M-stock). A few cross-cutting guards worth knowing:
- Transfers don't strand allocations. Dispatch availability is the source
warehouse's on-hand minus its
reservedQty(availableForTransfer), andStockLevel.reservedQtyis per-(product, warehouse) and kept in sync with order allocations — so a transfer can never drain stock an order is holding in that warehouse. - Manual receipt + WMS booked-in don't double-count.
reconcileBookedInQuantitiesnets the locally-received quantity (read under aFOR UPDATElock on the PO or the stock transfer) so a manual receipt already covering a line is not re-added when the Mintsoft booked-in webhook approves the same ASN. - Opening stock can't duplicate.
applyOpeningStocktakes aFOR UPDATElock on the stock level before checking for an existing opening cost layer, so concurrent calls serialise and the second is rejected. - Non-negative stock in the DB.
quantity >= 0andreservedQty >= 0are fully VALIDATEd CHECK constraints (every existing row checked).reservedQty <= quantityis aNOT VALIDconstraint — enforced on new writes but not yet validated against historical rows (that cleanup is deferred), so the transfer guard above (not just the constraint) is what protects allocations at dispatch. - FIFO ordering at the destination of a received transfer follows the dispatch-time cost-layer snapshot order. If transfers are received out of dispatch order the recreated layers can be marginally out of strict FIFO order at the destination — this is cosmetic for reporting and accepted (the cost basis per layer is preserved).