Skip to main content

Order Lifecycle

This is the most non-obvious part of the system, and the one most worth reading before touching orders.service.ts. Every transition below was verified end-to-end against a real running instance (real HTTP calls, not mocks) — not inferred from reading code alone.

Who is FromOrg, who is ReceiverOrg

For a Request order (a facility asking its parent store for stock): FromOrg is the parent store (the one that will approve and fulfil), ReceiverOrg is the requesting facility. For a Return order (a facility sending stock back), it's the other way round: FromOrg is the facility initiating the return, ReceiverOrg is the parent store receiving it.

The happy path (Request order, full receipt)

  1. Draft — the facility calls POST /orders/request with a product + quantity. This creates the order (if one doesn't already exist in Draft for that facility) and adds a line item with quantityApproved still unset.
  2. → Pending — the parent store assigns a batch and an approved quantity per line via PUT /orders/:id/assign-batch. (There's a second, more granular path — submitOrderRequest then matchBatchToOrderDetails then validateOrder — that goes through an explicit Submitted status first; assign-batch is the direct single-step version of the same transition, both landing in Pending.)
  3. → ApprovedPATCH /orders/approveOrder/:id. This is where the real stock movement happens: for each batch-tracked line, it debits the parent store's stock and credits the facility's stock (two StockMovement rows), then marks the line isPosted. The whole sequence runs inside a database transaction, so a failure partway through can't leave stock debited on one side without the matching credit on the other.
  4. Acknowledge each line — the facility calls PATCH /orders/acknowledgeProduct per line with quantityReceived.
  5. → CompletePATCH /orders/acknowledgeOrder/:id. If every line's quantityReceived equals its quantityApproved, the order goes straight to Complete.

The shortfall branch

If any line's quantityReceived is less than its quantityApproved, step 5 above takes a different path: acknowledgeOrder requires a shortfallReason (one of a fixed set of options, defined via an AttributeDefinition — e.g. "Damaged in transit", "Wrong quantity shipped", "Lost/missing") and the order moves to PendingShortfallApproval instead of Complete.

The parent store then resolves it via PATCH /orders/approveShortfall/:id, choosing a destination:

  • Inventory — the shortfall quantity is treated as returned stock; it's credited back to the parent store's balance.
  • Wastage — the shortfall quantity is recorded as lost, with no balance credited anywhere (a recordStockMovementWithoutBalanceChange call, not a real debit/credit).

Either way, the order ends in Complete.

The return-order branch

A separate flow, for a facility returning stock it already has (not tied to a prior request's shortfall): POST /orders/InitiateReturnOrderPATCH /orders/SubmitReturnOrder/:id → the parent store acknowledges each returned line (PATCH /orders/AcknowledgeReturnOrderProduct) → then acknowledges the whole order (PATCH /orders/AcknowledgeReturnOrder/:id), which posts the actual debit-facility/credit-parent-store stock movements and completes it.

Every write sequence above is transaction-wrapped

ApproveOrder, AcknowledgeOrder (both branches), ApproveShortfall, RejectShortfall, requestOrder, InitiateReturnOrder, AcknowledgeReturnOrder, and the batch-assignment step all wrap their multi-step writes in a database transaction — a crash or thrown error partway through rolls back cleanly rather than leaving stock genuinely inconsistent. This wasn't always true; it was a deliberate fix, closing a real bug class (a debit posted with no matching credit if the process died mid-sequence).