---
id: STATE-COM-ORD-013
title: Purchase order lifecycle
object: DOM-COM-ORD-006
product_area: commerce
module: orders
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-commerce: c8b66a98e2490617aec38ba5c8165dd9bd0a8f9b
states:
  - name: draft
    meaning: The order and lines remain replaceable at the version read.
  - name: issued
    meaning: The issued order is eligible for receipt.
  - name: partially_received
    meaning: Receipt has covered some units while an outstanding remainder remains.
  - name: received
    meaning: 'The order has no outstanding quantity, including quantities cancelled rather than delivered.'
  - name: closed
    meaning: Staff concluded the order and cancelled its outstanding remainder.
  - name: cancelled
    meaning: The order ended before any stock was received.
transitions:
  - from: draft
    to: issued
    trigger: Staff move the stock order into issue.
    guard: Current status must be draft.
    side_effects:
      - record the issue timestamp
    who: Buyer with the purchasing update permission
    provenance: test
    source: >-
      services-commerce:modules/purchasing/tests/e2e/purchasing.e2e.spec.js#drafts the order numbered with its
      line and issues it
  - from: issued
    to: partially_received
    trigger: Goods arrival covers part of the outstanding remainder.
    guard: Each reported quantity must fit its line's open quantity.
    side_effects:
      - 'commit stock, acquisition cost, posting facts and waiting allocation with receipt'
    who: Receiver with the purchasing create permission
    provenance: test
    source: >-
      services-commerce:modules/purchasing/tests/integration/services/receiving/purchasing-flow.integration.spec.js#records
      the first receipt numbered, against the order, with one line of 60 at the order line’s cost, and the
      order partially received
  - from: partially_received
    to: received
    trigger: Receipt or quantity cancellation removes the final remainder.
    guard: Progress is derived from the resulting quantities.
    side_effects:
      - record the fully accounted receipt timestamp
    who: Receiver or buyer
    provenance: test
    source: >-
      services-commerce:modules/purchasing/tests/integration/services/receiving/purchasing-flow.integration.spec.js#receives
      the remaining 40 and the order is received with its instant; the two layers stand oldest first
  - from: issued
    to: received
    trigger: Full receipt or line cancellation removes all outstanding units.
    guard: Quantity derivation also admits an untouched order whose remainder was cancelled.
    side_effects: []
    who: Receiver or buyer
    provenance: observed
    source: >-
      services-commerce:modules/purchasing/services/persistence/purchase-orders.ts:cancelPurchaseOrderLineQuantity
  - from: partially_received
    to: closed
    trigger: Staff conclude a partly received order.
    guard: 'Closing accepts issued, partially received or received status.'
    side_effects:
      - cancel all outstanding line quantities before closing
    who: Buyer with the purchasing update permission
    provenance: test
    source: >-
      services-commerce:modules/purchasing/tests/integration/services/receiving/purchasing-flow.integration.spec.js#closes
      an issued order after 90 of 100: cancelled_quantity 10, status closed; cancelling it instead is refused
  - from: issued
    to: closed
    trigger: Staff conclude an issued order.
    guard: The issued status is eligible for closing.
    side_effects:
      - cancel quantities still outstanding
    who: Buyer
    provenance: observed
    source: 'services-commerce:modules/purchasing/services/persistence/purchase-orders.ts:closePurchaseOrder'
  - from: received
    to: closed
    trigger: Staff conclude a fully received order.
    guard: The received status is eligible for closing.
    side_effects: []
    who: Buyer
    provenance: observed
    source: 'services-commerce:modules/purchasing/services/persistence/purchase-orders.ts:closePurchaseOrder'
  - from: draft
    to: cancelled
    trigger: Staff end a draft stock order.
    guard: The draft has no received quantities.
    side_effects:
      - record the cancellation timestamp
    who: Buyer
    provenance: observed
    source: 'services-commerce:modules/purchasing/services/persistence/purchase-orders.ts:cancelPurchaseOrder'
  - from: issued
    to: cancelled
    trigger: Staff end an issued stock order without delivery.
    guard: Every line has zero received units.
    side_effects: []
    who: Buyer
    provenance: observed
    source: 'services-commerce:modules/purchasing/services/persistence/purchase-orders.ts:cancelPurchaseOrder'
invalid:
  - from: partially_received
    to: cancelled
    why: Received stock requires closure of the outstanding remainder rather than whole-order cancellation.
  - from: received
    to: partially_received
    why: Corrections create later movements instead of revising completed receipts.
  - from: issued
    to: draft
    why: Only drafts can be replaced; issued purchases must progress or end.
concurrency: >-
  Order locking serializes receipt and lifecycle changes. Cancelling an untouched issued order's entire
  remainder through line operations still labels it received, even though no delivery occurred.
invariants:
  - INV-DOM-COM-ORD-006-02
  - INV-DOM-COM-ORD-006-09
  - INV-DOM-COM-ORD-006-10
evidence:
  - >-
    services-commerce at c8b66a98: modules/purchasing/db/purchase-orders.table.ts;
    services/persistence/purchase-orders.ts; services/receiving/receive-stock.ts;
    tests/e2e/purchasing.e2e.spec.js.
---

# Purchase order lifecycle

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> draft
  draft --> issued
  draft --> cancelled
  issued --> partially_received: some received
  issued --> received: all received or all cancelled
  issued --> closed
  issued --> cancelled: nothing received
  partially_received --> received
  partially_received --> closed
  received --> closed
```

## Transitions Explained

Staff create a draft, issue it and record deliveries. Partial receipts leave an outstanding remainder; receipts or quantity cancellation can eliminate it. Closing cancels all remaining units, while cancellation is reserved for orders with no received stock.

## What Cannot Happen

Only drafts are replaceable. Received stock prevents whole-order cancellation, and fully received orders cannot take another receipt.

## Concurrency and Timing

Order locking serializes receipt and lifecycle changes. Cancelling an untouched issued order's entire remainder through line operations still labels it received, even though no delivery occurred.
