Order Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Order module.
Use this page for the order domain: what it does for customers and admins, and how each flow behaves from start to finish.
| Source Type | Files or Docs | What Was Extracted |
|---|
| API | apps/api/src/modules/order/customer/order-customer.controller.ts, admin/{order,return,refund}/*.controller.ts | Routes, permissions |
| Backend | order-customer.service.ts, order-*.service.ts, return/refund services, workers | Lifecycle, transitions, money |
| Schema | packages/db/src/schema/order/*.ts | 12 tables, 9 statuses, CHECKs |
| Money | apps/api/src/utils/money/money.util.ts | allocateProportionally, valueOfUnits |
| Error registry | apps/api/src/common/types/error-codes.ts (// ORDER) | ORDER_* codes |
| Field | Value |
|---|
| Module | order |
| Submodule | dispatch, returns, refunds, receipts, emails |
| Primary user value | A permanent, truthful commercial record with a full post-purchase lifecycle — dispatch, delivery, returns and refunds |
| Actors | Customer, admin (orders/returns/refunds permissions), background jobs |
| Main entry points | /api/mobile/orders (8), /api/orders (11), /api/order-returns (5), /api/order-refunds (4) |
| Main outputs | Orders, shipments, returns, refunds, receipt PDFs, emails |
| Related docs | API, Backend |
| Actor | Can Do | Cannot Do | Auth Requirement | Notes |
|---|
| Customer | List orders, read one, poll status, download receipt, cancel before dispatch, request/cancel returns | Create orders (background job does), return after the window, cancel after dispatch (409 → offer return) | JWT | ORDER_NOT_FOUND for another customer's order |
| Admin (orders) | Confirm, processing, cancel, notes, COD collection, shipments, deliver/fail | Create refunds without Refunds_UPDATE, decide returns without Returns_UPDATE | Admin JWT + Orders_* | Permission modules are separate by design |
| Admin (returns) | Decide, receive, inspect | Approve refunds | Admin JWT + Returns_* | |
| Admin (refunds) | Create (via order), approve (with method), reject, settle (with reference) | Mark paid, edit amounts | Admin JWT + Refunds_* | Settlement is human, never gateway-integrated |
| Worker | CREATE_ORDER from the outbox row | — | BullMQ | One order per completed checkout (unique index) |
| Capability | Surface | Actor | Route/Trigger | State Read | State Written | Linked API Section |
|---|
| List/read/status/receipt | Customer | Customer | GET /api/mobile/orders* | orders | — | API §4 |
| Cancel before dispatch | Customer | Customer | POST /:id/cancel | order | status, restock | API |
| Request return | Customer | Customer | POST /:id/returns | order + items | return rows | API |
| Cancel return | Customer | Customer | POST /returns/:returnId/cancel | return | status | API |
| Confirm/processing/cancel/notes/COD | Admin | Admin | POST /api/orders/:id/* | order | order fields | API |
| Shipments + deliver/fail | Admin | Admin | POST /:id/shipments* | order + items | shipment, serials | API |
| Create refund | Admin | Admin | POST /api/orders/:id/refunds | order + items | refund rows | API |
| Return workflow | Admin | Admin | /api/order-returns/* | return | status | API |
| Refund workflow | Admin | Admin | /api/order-refunds/* | refund | status + settlement | API |
| Create order | Worker | System | outbox → CREATE_ORDER | checkout, payment | order rows | backend §6 |
Payment marks the attempt succeeded; the same transaction writes an order.eligible outbox row. The CREATE_ORDER worker builds the order from the frozen checkout — items, prices, promotions, address — and the order appears in the customer's list.
| Branch | Condition | Behavior | Error/Result |
|---|
| Redelivered outbox row | Duplicate | Unique index → no second order | one order |
| Short order | A hold lapsed between charge and completion | Order created with unavailableQuantity > 0 | Distinct email; operator decides |
| Reconciliation flag | Payment flagged | Order stays created until a human confirms | created = "a human has to look" |
A customer can pay for three items and receive two: the inventory hold on one lapsed between the charge landing and the checkout completing. Checkout completes anyway, because stranding a charged customer is worse. Those lines come back with unavailableQuantity > 0 and a per-line label of unavailable. The customer paid for those items and they are not coming — distinct email, and the order waits for an operator to decide between refunding, sourcing or substituting. Never render a short order as though it shipped complete.
A COD order is paid in the data model from the moment it is placed — that is what lets stock be deducted. The cash arrives days later. codCollectedAt says money actually changed hands. Until it is set, cancelling that order refunds nothing — correctly — and the cancellation email says so. The customer must not be shown a refund that does not exist.
NINE statuses, none of them returned or refunded: created · confirmed · processing · partially_shipped · shipped · partially_delivered · delivered · completed · cancelled. A return does not change the order's status — the order stays delivered forever, because that is what happened. Returns and refunds are separate objects with their own statuses, several per order allowed.
The aggregate status is recomputed from the line quantities rather than incremented — a line of three can have one delivered, one in transit and one cancelled at the same instant, and no single label describes that.
- Confirm — normally instant at creation; an order stays
created only when something needs a decision (short order, reconciliation flag). created means "a human has to look at this".
- Dispatch —
POST /:id/shipments with quantities and device serial numbers (unique per product; entered at dispatch). Deliver and fail close a shipment.
- COD collection — records when cash changed hands; only applicable for COD, delivered, not already counted.
- Returns — decide (approve/reject), receive (goods back), inspect (quantities + rejection reason). The budget:
return_pending_quantity + returned_quantity <= delivered_quantity.
- Refunds — create (lines and quantities — the server computes the money), approve (choose the method), reject, settle (record a settlement reference — required by constraint). An approved refund can never be rejected.
ORDER_REFUND_EXCEEDS_PAID is reachable with no operator fault — two people approving at once — so say the figure changed and refresh.
Covered in §5.4. Legal transitions are guarded — ORDER_TRANSITION_NOT_ALLOWED (409) on anything else.
| From | Event/Action | To | Guard | Side Effects |
|---|
| — | request | pending | delivered + within window | budget held |
pending | decide approve | approved | — | awaits receipt |
pending | decide reject | rejected | — | budget released |
approved | receive | received | goods back | — |
received | inspect | inspected / rejected | quantities + reason | refund eligibility |
| any | customer cancel | cancelled | still pending | budget released |
| From | Event/Action | To | Guard | Side Effects |
|---|
| — | create (from order) | pending | lines within net ceilings | — |
pending | approve (with method) | approved | — | cannot be rejected after |
pending | reject | rejected | — | — |
approved | settle (with reference) | settled | reference required (400) | money moved by a human |
| Flow | DB Writes | Cache Effects | Jobs | Realtime | Analytics | Notifications |
|---|
| CREATE_ORDER | order + 12 tables' rows | — | from outbox | — | — | confirmation email (or short-order email) |
| Cancel | status + restock | — | — | — | — | cancellation email (COD: no refund stated) |
| Shipment | shipment + serials | — | — | — | — | dispatch email |
| Return decide/receive/inspect | return rows | — | — | — | — | return email |
| Refund approve/settle | refund rows | — | — | — | — | — |
| Scenario | Trigger | User/System Experience | Recovery | Source |
|---|
| Transition blocked | Order moved | 409 | Refresh, show current state | ORDER_TRANSITION_NOT_ALLOWED |
| Cancel after dispatch | Too late | 409 | Offer a return — message says which | ORDER_NOT_CANCELLABLE |
| Restock refused | Inventory declined | 500, cancellation rolled back | Retry, escalate — nothing changed | ORDER_RESTOCK_REFUSED |
| Double-tap return | Same units twice | 409 | Refresh — not alarming | ORDER_RETURN_QUANTITY_UNAVAILABLE |
| Double-approve refund | Two admins | 409 | Figure changed — refresh | ORDER_REFUND_EXCEEDS_PAID |
| Settle without reference | Missing evidence | 400 | Require the field | ORDER_REFUND_SETTLEMENT_REFERENCE_REQUIRED |
- Actor capability diagram — §3/§4.
- Sequence diagram per major flow — §5.1.
- State machine diagram — §5.4/§7.
- Data side-effect diagram — §9.
- Error branch diagram — §10.
| Feature | Minor Behavior | Actor | Trigger | User/System Result | Backend Side Effect | Source |
|---|
| Status poll | pollAfterSeconds | Customer | Poll | Server-set interval | honoured | |
| Receipt | Generated on demand | Customer | Download | Always current | PDF stream | |
| Serial entry | Per-unit at dispatch | Admin | Shipment | Unique per product | ORDER_SERIAL_ALREADY_RECORDED on dup | |
| Short order | Distinct email | Customer | Hold lapsed | Told what is missing | operator decision | |
| COD cancel | No refund | Customer | Before collection | Cancellation email says so | correct | |
| Return cancel | Budget release | Customer | Cancel | Units available again | — | |
| Refund approve | Method chosen | Admin | Approve | Row gains method | not rejectable | |
| Refund settle | Reference required | Admin | Settle | Evidence recorded | 400 without | |
| Rule | Business Reason | Actor Impact | Enforced In | API Impact | Backend Impact | Tests |
|---|
No returned status | History is truth | Derived badge | enum | — | nine-status enum | probe |
| Quantities are truth | One line, three states | Labels for display | schema | — | no item status column | probe |
| Returns as budget | No over-return | — | CHECK | — | pending + returned <= delivered | probe |
| Refund ceilings | Never more than paid | — | two CHECKs | — | order + line | probe |
| COD committed ≠ collected | Cash days later | No refund pre-collection | field | — | codCollectedAt | spec |
| Settlement reference required | Human evidence | — | constraint | 400 | — | probe |
| One order per checkout | No duplicates | — | unique index | — | uq_order_checkout_session_id | spec |
| Outbox row in completion tx | No paid-no-order | — | checkout payment | — | order.eligible | spec |
| Product Decision | User Benefit | Engineering Benefit | Alternative | Tradeoff | Risk |
|---|
| Background order creation | Client never fakes orders | Outbox guarantee | Synchronous | Slight delay | Unique index |
| Returns/refunds as aggregates | Several in flight | No status explosion | Statuses | Derive badges | Documented |
| No item status | One truth | Quantities add up | Label column | Client derives | Label provided |
| Human-settled refunds | Money control | No gateway dep | Auto-refund | Manual queue | Reference required |
| Receipt not tax invoice | Legal honesty | — | Tax invoice | No VAT claims | Verbatim disclaimer |
| Flow | Edge Case | Trigger | Expected Behavior | User/System Feedback | Source |
|---|
| Create | Outbox redelivered | Duplicate | One order | unique index | |
| Create | Short line | Hold lapsed | Order + unavailable label | distinct email | |
| Cancel | COD uncollected | Pre-collection | No refund | email says so | |
| Cancel | Restock refused | Inventory down | Rollback, 500 | nothing changed | |
| Shipment | Serial duplicate | Typo/real | 409 | operator checks | |
| Shipment | Split dispatch | Two parcels | partially_shipped then shipped | — | |
| Deliver | Partial | First parcel | partially_delivered, second dispatchable | — | |
| Return | Past window | Late | 409 | date in message | |
| Return | Double-tap | Concurrent | 409 | refresh | |
| Refund | Over ceiling | Two approvers | 409 | figure changed | |
| Refund | Approved then reject | Mistake | Refused | transition guard | |
| Flow | Reads | Writes | Cache | Jobs/Events | Response Fields |
|---|
| Create | checkout, payment | order + items + promotions + address | — | outbox → CREATE_ORDER | order |
| Status | order, items | — | — | — | status, labels, timeline, pollAfterSeconds |
| Cancel | order, inventory | status, restock | — | — | message |
| Shipment | order, items | shipment, serials | — | dispatch email | shipment |
| Return | order, items | return rows | — | return email | return |
| Refund | order, items, return | refund rows | — | — | refund |