Order Fulfillment Lifecycle
Order Fulfillment Lifecycle — state machine, transition rules, polling contract, and edge cases
Order Fulfillment Lifecycle
Overview
Order fulfillment tracks physical handling progress after the order is confirmed: online orders after payment success, and COD orders after reservation succeeds with paymentStatus = "cod_pending". It runs in parallel with the order status lifecycle but is independent of payment collection state.
Fulfillment is admin-driven and fully asynchronous: the POST /api/admin/orders/:id/fulfillment-status endpoint accepts a transition request and returns an async request ID. Poll GET /api/admin/orders/requests/:requestId to learn when the operation completes.
State Machine
States
| State | Meaning |
|---|---|
received | Order confirmed; stock reserved/finalized; awaiting preparation |
ready | Order packed and ready for pickup by delivery partner |
picked_up | Delivery partner has collected the package |
in_transit | Package in transit to delivery address |
delivered | Package delivered to customer |
key_sent | Digital keys assigned and emailed to customer (digital orders only) |
activated | Care package subscription activated; subscription is now live (care_package orders only) |
cancelled | Order cancelled before delivery (handled by order cancellation flow) |
Transition Rules
One-Step Constraint
Exactly one step per request. The system enforces this at two layers:
- Application layer (
order.processor.ts) — rejects skip-ahead transitions (e.g.,received → delivered) - Database constraint (
order_fulfillment_historiestable) — CHECK constraint onfrom_status/to_statuspair prevents invalid transitions even if application logic is bypassed
Valid Transitions
| Current State | Next Valid State | Actor | Notes |
|---|---|---|---|
received | ready | admin | Order packed |
ready | picked_up | admin | Delivered to partner |
picked_up | in_transit | admin | Package in transit |
in_transit | delivered | admin | Customer received |
received | activated | admin | Care package activation via POST /:id/activate-care-package endpoint only |
Invalid Transitions
| From | Attempted To | Result |
|---|---|---|
received | picked_up, in_transit, delivered | invalid_transition — async request marked failed |
ready | in_transit, delivered | invalid_transition |
picked_up | delivered | invalid_transition |
delivered | any prior state | invalid_transition (no backward transitions) |
API Reference
Update Fulfillment Status
POST /api/admin/orders/:id/fulfillment-statusRequest:
{
"status": "ready",
"remarks": "Package sealed and labeled for Aramex pickup"
}| Field | Type | Required | Description |
|---|---|---|---|
status | enum | Yes | Target state — must be exactly one step forward |
remarks | string | No | Admin note stored in history record |
Response 202 Accepted:
{
"requestId": "req_01JXYZ...",
"status": "pending",
"createdAt": "2026-04-24T10:00:00.000Z"
}Error responses:
400 Bad Request— invalid transition or validation error404 Not Found— order does not exist409 Conflict— order already indeliveredorcancelledstate
Poll Async Request
GET /api/admin/orders/requests/:requestIdResponse 200 OK:
{
"requestId": "req_01JXYZ...",
"status": "completed",
"result": {
"orderId": 1234,
"fulfillmentStatus": "ready"
},
"createdAt": "2026-04-24T10:00:00.000Z",
"completedAt": "2026-04-24T10:00:05.000Z"
}Or on failure:
{
"requestId": "req_01JXYZ...",
"status": "failed",
"error": "invalid_transition",
"createdAt": "2026-04-24T10:00:00.000Z"
}Polling Contract
Polling Cadence
- Call the mutating endpoint → receive
requestId - Wait 1–2 seconds
- Poll
GET /api/admin/orders/requests/:requestId - If
status === "pending"→ wait 1–2s → poll again - Stop after 30 seconds of pending — escalate to admin
Status Values
| Status | Meaning | Action |
|---|---|---|
pending | Operation in progress | Continue polling |
completed | Operation succeeded | Read result field for outcome |
failed | Operation failed | Read error field for reason code |
Error Codes in error Field
| Error Code | Meaning |
|---|---|
invalid_order_id | Order ID is null or malformed |
order_not_found | No order with that ID exists |
already_at_target_status | Idempotent replay — no action taken, counts as success |
invalid_transition | Transition not allowed (skip-ahead or backward) |
constraint_violation_non_retryable | DB constraint prevented update — non-retryable |
business_error | Application-level error (BadRequestException / NotFoundException) |
Fulfillment History
Every transition creates a record in order_fulfillment_histories:
{
fromStatus: "received",
toStatus: "ready",
remarks: "Package sealed and labeled for Aramex pickup",
actorType: "admin",
createdAt: "2026-04-24T10:00:00.000Z"
}The actorType field records who initiated the transition. Currently only "admin" is supported; future extensions (driver, system) are reserved.
Edge Cases
Idempotent Replay
Sending the same transition twice (e.g., received → ready twice in quick succession) returns { success: true, skipped: true, reason: "already_at_target_status" }. The second request is treated as a no-op. No duplicate history row is created.
Constraint Violations
If the database CHECK constraint rejects a transition (code 23514), the async request is marked failed with error constraint_violation_non_retryable. This is non-retryable — fix the application logic before retrying.
Order Cancellation During Fulfillment
If an order is cancelled while a fulfillment transition is in-flight, the cancellation takes precedence. The fulfillment operation may fail with order_not_found or business_error.
Digital Order Fulfillment
Digital orders (orderType === "digital") follow a simplified fulfillment path compared to physical orders. Instead of shipping logistics, fulfillment consists of key assignment and digital delivery.
Digital Transition Rules
| Current State | Next Valid State | Actor | Notes |
|---|---|---|---|
received | key_sent | admin | Admin triggers digital delivery |
Digital Delivery API
POST /api/admin/orders/:id/send-digital-deliveryAuth: JwtAuthGuard + RoleGuard + @Permissions(DigitalKeys_SEND_DELIVERY)
Response 200 OK:
{
"orderId": 101,
"deliveredItems": [
{
"orderItemId": 12,
"productId": 7,
"productTitle": "Microsoft Office 365 Home (1 Year)",
"keyId": 3,
"assignedAt": "2026-06-11T10:00:00.000Z"
}
],
"emailSentTo": "customer@example.com",
"fulfillmentStatus": "key_sent"
}| Field | Type | Description |
|---|---|---|
orderId | number | Order ID |
deliveredItems | array | Items that received key assignments |
emailSentTo | string | null | Email address where keys were sent |
fulfillmentStatus | "key_sent" | New fulfillment status |
Error responses:
400 Bad Request— order not inpaidstatus or not digital type404 Not Found— order does not exist409 Conflict— no available keys in pool for one or more items
View Digital Keys on an Order
GET /api/admin/orders/:id/digital-keysAuth: JwtAuthGuard + RoleGuard + @Permissions(Orders_READ)
Returns the digital keys assigned to each order item.
Care Package Order Fulfillment
Care package orders (orderType === "care_package") follow a simplified two-step fulfillment path. Instead of shipping, fulfillment consists of subscription activation.
Care Package Transition Rules
| Current State | Next Valid State | Actor | Notes |
|---|---|---|---|
received | activated | admin | Admin triggers care package activation |
Care Package Activation API
POST /api/admin/orders/:id/activate-care-packageAuth: JwtAuthGuard + RoleGuard + @Permissions(CarePackageSubscriptions_UPDATE)
This endpoint manually activates all pending_activation subscriptions on the order. It is the manual override path — activation normally runs automatically via BullMQ after payment success.
Response 200 OK:
{
"orderId": 101,
"activatedCount": 1,
"fulfillmentStatus": "activated"
}Error responses:
400 Bad Request— order type is notcare_package(ORDER_ACTIVATE_CARE_PACKAGE_NOT_APPLICABLE)400 Bad Request— no pending subscriptions to activate404 Not Found— order not found
Care Package Fulfillment Timeline
The customer-facing fulfillment timeline for care_package orders:
[
{ "status": "received", "label": "Received", "completed": true, "current": false },
{ "status": "activated", "label": "Activated", "completed": false, "current": false }
]Admin Detail: Eligible Fulfillment Actions
The admin order detail response (GET /api/admin/orders/:id) includes an eligibleFulfillmentActions array that indicates what fulfillment-related actions are currently available for the order based on orderType and fulfillmentStatus:
| orderType | fulfillmentStatus | eligibleFulfillmentActions |
|---|---|---|
physical / mixed | received, ready, picked_up, in_transit | ["advance"] |
physical / mixed | delivered | [] |
digital | received | ["send-digital-delivery"] |
digital | key_sent | [] |
care_package | received (with pending subscription) | ["activate-care-package"] |
care_package | received (no pending subscription) | [] |
care_package | activated | [] |
Relationship to Order Lifecycle
- Fulfillment begins after online
orderStatusreachespaid, or after COD reachespaymentStatus = "cod_pending" - An order can be in any
orderStatuswhile itsfulfillmentStatusadvances - If an order is cancelled, fulfillment transitions stop; no reversal of delivered state
fulfillmentStatusis independent oforderStatus— do not assumeorderStatus === "paid"meansfulfillmentStatus === "received"
Related Documentation
- Order API — full API reference including customer order detail
- Order Backend — module composition and data model
- Order Feature — order lifecycle and state models