Shop It Docs
Developer Resourcesorder

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

StateMeaning
receivedOrder confirmed; stock reserved/finalized; awaiting preparation
readyOrder packed and ready for pickup by delivery partner
picked_upDelivery partner has collected the package
in_transitPackage in transit to delivery address
deliveredPackage delivered to customer
key_sentDigital keys assigned and emailed to customer (digital orders only)
activatedCare package subscription activated; subscription is now live (care_package orders only)
cancelledOrder 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:

  1. Application layer (order.processor.ts) — rejects skip-ahead transitions (e.g., received → delivered)
  2. Database constraint (order_fulfillment_histories table) — CHECK constraint on from_status / to_status pair prevents invalid transitions even if application logic is bypassed

Valid Transitions

Current StateNext Valid StateActorNotes
receivedreadyadminOrder packed
readypicked_upadminDelivered to partner
picked_upin_transitadminPackage in transit
in_transitdeliveredadminCustomer received
receivedactivatedadminCare package activation via POST /:id/activate-care-package endpoint only

Invalid Transitions

FromAttempted ToResult
receivedpicked_up, in_transit, deliveredinvalid_transition — async request marked failed
readyin_transit, deliveredinvalid_transition
picked_updeliveredinvalid_transition
deliveredany prior stateinvalid_transition (no backward transitions)

API Reference

Update Fulfillment Status

POST /api/admin/orders/:id/fulfillment-status

Request:

{
  "status": "ready",
  "remarks": "Package sealed and labeled for Aramex pickup"
}
FieldTypeRequiredDescription
statusenumYesTarget state — must be exactly one step forward
remarksstringNoAdmin 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 error
  • 404 Not Found — order does not exist
  • 409 Conflict — order already in delivered or cancelled state

Poll Async Request

GET /api/admin/orders/requests/:requestId

Response 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

  1. Call the mutating endpoint → receive requestId
  2. Wait 1–2 seconds
  3. Poll GET /api/admin/orders/requests/:requestId
  4. If status === "pending" → wait 1–2s → poll again
  5. Stop after 30 seconds of pending — escalate to admin

Status Values

StatusMeaningAction
pendingOperation in progressContinue polling
completedOperation succeededRead result field for outcome
failedOperation failedRead error field for reason code

Error Codes in error Field

Error CodeMeaning
invalid_order_idOrder ID is null or malformed
order_not_foundNo order with that ID exists
already_at_target_statusIdempotent replay — no action taken, counts as success
invalid_transitionTransition not allowed (skip-ahead or backward)
constraint_violation_non_retryableDB constraint prevented update — non-retryable
business_errorApplication-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 StateNext Valid StateActorNotes
receivedkey_sentadminAdmin triggers digital delivery

Digital Delivery API

POST /api/admin/orders/:id/send-digital-delivery

Auth: 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"
}
FieldTypeDescription
orderIdnumberOrder ID
deliveredItemsarrayItems that received key assignments
emailSentTostring | nullEmail address where keys were sent
fulfillmentStatus"key_sent"New fulfillment status

Error responses:

  • 400 Bad Request — order not in paid status or not digital type
  • 404 Not Found — order does not exist
  • 409 Conflict — no available keys in pool for one or more items

View Digital Keys on an Order

GET /api/admin/orders/:id/digital-keys

Auth: 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 StateNext Valid StateActorNotes
receivedactivatedadminAdmin triggers care package activation

Care Package Activation API

POST /api/admin/orders/:id/activate-care-package

Auth: 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 not care_package (ORDER_ACTIVATE_CARE_PACKAGE_NOT_APPLICABLE)
  • 400 Bad Request — no pending subscriptions to activate
  • 404 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:

orderTypefulfillmentStatuseligibleFulfillmentActions
physical / mixedreceived, ready, picked_up, in_transit["advance"]
physical / mixeddelivered[]
digitalreceived["send-digital-delivery"]
digitalkey_sent[]
care_packagereceived (with pending subscription)["activate-care-package"]
care_packagereceived (no pending subscription)[]
care_packageactivated[]

Relationship to Order Lifecycle

  • Fulfillment begins after online orderStatus reaches paid, or after COD reaches paymentStatus = "cod_pending"
  • An order can be in any orderStatus while its fulfillmentStatus advances
  • If an order is cancelled, fulfillment transitions stop; no reversal of delivered state
  • fulfillmentStatus is independent of orderStatus — do not assume orderStatus === "paid" means fulfillmentStatus === "received"