Order Module Backend Documentation
Order Module - Backend Documentation
1. Backend Scope and Boundaries
Order backend owns:
- checkout and buy-now initiation
- order state transitions and status history
- async order commands (cancel/process-refund)
- payment callback-driven finalization handoff
- outbox dispatch and retention cleanup for order-owned maintenance runtime
Order domain supports:
payment_method:online,cod,pos—"cod"identifies cash on delivery pending collection, and"pos"identifies in-store POS terminal payment (no gateway redirect)
order_payment_method enum supports "online" (gateway redirect), "cod" (Cash on Delivery, no redirect, pending collection), and "pos" (in-store POS terminal payment, no redirect). COD orders are available for physical carts only. POS orders are created by the POS module (PosModule) via OrderService.createFromPosItems().
2. Module Composition (Aggregate + Leaf)
OrderModule composes:
OrderCoreModuleOrderCustomerModuleOrderAdminModuleOrderProcessingModule
Ownership model:
OrderCoreModuleexports shared domain service (OrderService)- customer/admin modules own their HTTP surfaces
OrderProcessingModuleowns queues, processors, maintenance scheduler, outbox dispatch, and cleanup
3. Data Model (Drizzle / PostgreSQL)
Primary tables:
order— includesorder_typeenum (physical,digital,care_package,mixed);payment_methodenum (online,cod,pos) —"pos"identifies in-store POS terminal payment (no gateway redirect);order_sourceenum: includes"pos_sale"— set when order is created via the POS terminal (PosModule). Standard orders have a different source value;cod_cash_collected_at(timestamp, nullable);cod_collection_remarks(varchar 500, nullable)order_item— includescare_package_subscription_id(nullable FK →care_package_subscriptions.id);serial_number(nullable varchar, assigned by admin at packing time for physical items); populated for care-package order linesorder_status_historyorder_fulfillment_history— tracks fulfillment transitions (received,ready,picked_up,in_transit,delivered,key_sent,activated)async_requestoutbox_eventuser_product_serial— serial number registry (publicId uuid7, serial_number unique per product, FK to order/orderItem/product/customer; assigned at packing transition)
Schema source of truth:
packages/db/src/schema/order/enums.tspackages/db/src/schema/order/orders.tspackages/db/src/schema/order/order-items.tspackages/db/src/schema/order/order-status-history.ts
Monetary integrity constraints (integer minor units; Stripe uses USD cents):
- order totals:
total_mrp,total_sp,discount,coupon_discount,final_payable,refund_amount - order line values:
unit_mrp,unit_sp,discount,final_value - DB check constraints enforce non-negative values.
Actor provenance model (order_status_history):
actor_type = customer+created_by_customer_idactor_type = admin+created_by_admin_idactor_type = system+ both null
4. Runtime Rules and Domain Invariants
4.1 Synchronous initiation paths
POST /api/orders/checkoutPOST /api/orders/buy-now
These create order/payment intent synchronously and return payment initiation payload immediately.
Domain invariant — digital orders are non-refundable:
requestRefund()rejects digital-only orders withORDER_DIGITAL_NOT_REFUNDABLE
Domain invariant — care package buy-now is not supported:
createBuyNow()rejectsproductKind = "care_package"withORDER_BUYNOW_CARE_PACKAGE_NOT_ALLOWED- Care packages must be purchased through cart checkout
Order type resolution (both checkout and buy-now):
productKindon the product determinesorder_type:physical→physical,digital→digital,care_package→care_package- If cart items have mixed product kinds, order type resolves to
mixed - Digital-only and care-package-only carts skip shipping: no
shipping_districtrequired,shipping_fee= 0
4.2 Reservation status lifecycle
orders.reservationStatus tracks whether stock reservation has been resolved for an order. Default: "pending".
Reservation flows:
- Physical or mixed orders:
stock.reserveoutbox events are enqueued during order creation.OrderProcessor.processReservationResult()setsreservationStatus = "reserved"asynchronously after the inventory worker processes them. - Digital-only, care-package-only orders: no
stock.reserveevents are enqueued (non-physical products have no inventory to reserve).OrderServicedetects this inside the creation transaction and setsreservationStatus = "reserved"synchronously — otherwisewaitForReservationResolution()would time out waiting for an async job that will never run, producing a 503. - POS orders (
createFromPosItems): POS does not callwaitForReservationResolution, so no short-circuit is needed. - Physical orders that skip reservation (all lines skipped by the non-physical guard, very rare for mixed carts): the same synchronous short-circuit applies via the
anyPhysicalReservationEnqueuedflag.
reservationStatus is only ever set to "reserved" or "failed" by processReservationResult() for physical reservations, or to "reserved" synchronously by the order-creation transaction for non-physical orders. No other code path touches this field.
4.3 Asynchronous finalization and commands
- payment success/failure jobs from payment redirect events
- retry reconciliation (
order.payment_retry) - auto-cancel for unpaid online orders (
order.auto_cancel); CODcod_pendingorders are skipped - async customer/admin cancellation and refund processing (
order.cancel,order.process_refund)
4.4 Payment redirect contract
Gateway verification uses backend-owned redirect handlers:
GET|POST /api/payments/redirect/:paymentId/successGET|POST /api/payments/redirect/:paymentId/failure
Return URL and callback URL resolution use runtime config (API_PUBLIC_BASE_URL, FRONTEND_BASE_URL, PAYMENT_RESULT_PAGE_URL).
4.5 Idempotency and atomicity
- idempotency-key replay for checkout/buy-now is deterministic
- async request dedupe via
async_requests - outbox dedupe via
(aggregate_type, aggregate_id, job_name, dedupe_key) - queue jobs use deterministic
jobId - multi-table mutations execute in DB transactions
4.6 Payment success artifact creation
On successful online payment, processor finalizes order state and emits inventory-finalization outbox events:
- physical products transition through reserve -> finalize inventory lifecycle
- cart-clear intent is emitted only after payment success confirmation; the
cart.clear_after_order_confirmedjob now resolves the linked cart viaorders.cartIdand callsCartCustomerService.completeCart(cartId, userId)— this clears cart items, resets promotion fields, and marks the cart"completed". Checkout-order creation keeps the cart in"checkout"status (visible but locked) until payment resolves. - checkout order creation keeps cart active/visible while order remains
payment_pending
COD uses a separate pending-collection confirmation path after stock reservation succeeds:
- order remains
orderStatus=payment_pending - order changes to
paymentStatus=cod_pending - the COD payment row remains
status=pending - cart-clear and stock-finalize outbox intents are emitted so fulfillment can proceed
- admin collection later completes the pending COD payment row and sets
paymentStatus=cod_collected
4.7 Payment initiation failure release behavior
If payment initiation fails after order creation (payment_pending), backend now immediately:
- marks order as
payment_failedwithpaymentStatus=failed - appends
payment_failedstatus history - emits
stock.releaseoutbox events for each product in the order - cancels any
pending_activationcare-package subscriptions linked to the order - restores the linked cart (identified via
orders.cartId) from"checkout"back to"active", using an atomicNOT EXISTS-guarded conditional update — if the user already has another"active"cart (collision case), the checked-out cart is marked"abandoned"instead. If the order has no linked cart (buy-now, POS, or pre-migration orders), this step is skipped.
This aligns initiation-failure handling with other failure paths and prevents reserved stock leakage. The cart-restore logic is idempotent: replaying the job after the cart is already restored results in no-ops because the WHERE status = 'checkout' condition no longer matches.
4.8 Queues used by order runtime
ordersorders_maintenancecart(outbox dispatch target for cart-clear intent)care_package(enqueue target: care package activation jobs on payment success)
4.9 Outbox lifecycle and cleanup behavior
outbox_events is shared infrastructure. Order runtime writes outbox events while maintenance runtime handles dispatch and retention cleanup.
Dispatch ownership:
OutboxDispatcherServiceclaimspendingrows only for owned target queues:ordersorders_maintenancecart
Cleanup flow:
OrdersMaintenanceSchedulerenqueuesorders_maintenance.cleanup_outbox.OrdersMaintenanceProcessorrunsOutboxCleanupService.cleanup().- Completed rows older than
OUTBOX_RETENTION_DAYSand failed rows older thanOUTBOX_FAILED_RETENTION_DAYSare removed in batches. - DLQ health check compares failed-job count against
OUTBOX_CLEANUP_DLQ_THRESHOLDand emits alert-level logs on breach.
4.10 Care-Package Subscription Creation on Payment Success
When an order contains care package items, OrderProcessor.processPaymentSuccess() performs additional work after standard order finalization:
-
For each
order_itemrow withcare_package_subscription_idset:- The
care_package_subscriptionsrow was created at checkout time withstatus = "pending_activation". - Processor enqueues
care_package.activate_subscriptiontoQueueName.CARE_PACKAGE:{ subscriptionId: item.carePackageSubscriptionId, orderId: order.id }
- The
-
Activation is asynchronous. The subscription moves to
status = "active"in a separate BullMQ job (CarePackageProcessor.processActivation()). -
If order is cancelled after payment, processor dispatches
care_package.cancel_subscriptionfor each subscription to transition them tocancelled.
Care-package activation is best-effort from the order processor's perspective — activation failures do not roll back the order. Admin can manually activate via POST /api/admin/care-package-subscriptions/:id/activate.
5. Caching Strategy
Read-side keyspaces:
order:*order:customer:list:*order:admin:list:*order:admin:detail:*order:admin:analytics:*— analytics summary (includes district filter in key)order:admin:analytics:districts:*— district aggregation (renamed from legacycountriesprefix)
Cross-module keys invalidated by order processing when applicable:
Configured TTLs:
ORDER_HISTORY_CACHE_TTL_SECONDS(default120)ORDER_ADMIN_CACHE_TTL_SECONDS(default60)
6. Rate Limiting Strategy
Customer-side controls:
ORDER_CHECKOUT_RATE_LIMIT(default10)ORDER_RETRY_PAYMENT_RATE_LIMIT(default5)ORDER_CANCEL_RATE_LIMIT(default10)ORDER_REFUND_REQUEST_RATE_LIMIT(default5)ORDER_CHECKOUT_RATE_WINDOW_SECONDS(default60)
Admin-side controls:
ORDER_ADMIN_OPERATION_RATE_LIMIT(default120)ORDER_ADMIN_RATE_WINDOW_SECONDS(default60)
All rate limiting paths use Redis-backed counters/sliding windows and throw RateLimitExceededException on breach.
Additional order runtime knobs:
ORDER_AUTO_CANCEL_MINUTES(default30)ORDER_PAYMENT_RETRY_MAX_ATTEMPTS(default3)OUTBOX_RETENTION_DAYS(default7)OUTBOX_FAILED_RETENTION_DAYS(default30)OUTBOX_CLEANUP_ENABLED(defaulttrue)OUTBOX_CLEANUP_CRON(default0 3 * * *)ORDERS_DISPATCH_CRON(default* * * * *)ORDERS_FAILED_MONITOR_CRON(default*/5 * * * *)OUTBOX_CLEANUP_DLQ_THRESHOLD(default100)DLQ_WARNING_THRESHOLD(default50)OUTBOX_BATCH_SIZE(default300)OUTBOX_PENDING_PREVIEW_LIMIT(default10)RETRY_BASE_DELAY_MS(default2000)RETRY_MAX_DELAY_MS(default120000)PAYMENT_RETRY_DELAY_ATTEMPT_1_MS(default60000)PAYMENT_RETRY_DELAY_ATTEMPT_2_MS(default300000)PAYMENT_RETRY_DELAY_ATTEMPT_3_MS(default900000)WORKER_ORDERS_CONCURRENCY(default20)WORKER_ORDERS_MAINTENANCE_CONCURRENCY(default8)
7. Error and Resilience Contracts
Representative API/domain errors:
| HTTP | errorCode | Message |
|---|---|---|
| 400 | ORDER_CART_INACTIVE | Only active cart can be checked out. |
| 400 | ORDER_CART_EMPTY | Cart has no items. |
| 400 | ORDER_PAYMENT_AMOUNT_MISMATCH | Payment amount mismatch detected. |
| 400 | ORDER_PRODUCT_NOT_AVAILABLE | Product is not available for purchase. |
| 400 | ORDER_PAYMENT_METHOD_NOT_SUPPORTED | Only online payment is supported. |
| 400 | ORDER_CANCEL_NOT_ALLOWED | Order cannot be cancelled in current status. |
| 400 | ORDER_DIGITAL_NOT_REFUNDABLE | Digital product orders are non-refundable. |
| 400 | ORDER_NOT_PENDING_REFUND | Order is not pending refund processing. |
| 400 | ORDER_NOTE_REQUIRED | Note is required. |
| 403 | ORDER_REFUND_ACCESS_DENIED | You do not have permission to request refund for this order. |
| 404 | ORDER_CART_NOT_FOUND | Cart not found. |
| 404 | ORDER_ASYNC_REQUEST_NOT_FOUND | Async request not found. |
| 404 | ORDER_NOT_FOUND | Order not found. |
| 409 | ORDER_IDEMPOTENCY_MISMATCH | Request with this idempotency key is already in progress. |
| 400 | ORDER_FULFILLMENT_NOT_PHYSICAL_TYPE | Order type does not support fulfillment advance. |
| 400 | ORDER_FULFILLMENT_NOT_DIGITAL_TYPE | Order type does not support digital delivery. |
| 400 | ORDER_ACTIVATE_CARE_PACKAGE_NOT_APPLICABLE | Order is not a care package type. |
| 400 | ORDER_NOT_PAID | Order must be in paid status. |
| 409 | ORDER_SERIAL_ALREADY_ASSIGNED | Serial number already assigned to this item. |
| 409 | ORDER_SERIAL_DUPLICATE | Serial number already in use. |
| 422 | ORDER_BUYNOW_CARE_PACKAGE_NOT_ALLOWED | Care package products do not support buy-now. |
| 422 | ORDER_SERIAL_ITEM_NOT_PHYSICAL | Serial number can only be assigned to physical items. |
| 422 | ORDER_SERIAL_AFTER_DELIVERED | Cannot assign serial after order is delivered. |
| 422 | CART_CARE_PACKAGE_ITEM_NOT_PHYSICAL | Care package attachment requires a physical cart item. |
| 503 | ERR_CONFIG_INVALID | Configuration value is missing/invalid. |
| 503 | ORDER_ASYNC_REQUEST_CREATION_FAILED | Failed to create async order request. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. Please try again later. |
Resilience controls in processing runtime:
- retry + backoff for queued failures
- queue failed-set threshold monitoring (
DLQ_WARNING_THRESHOLD,QUEUE_FAILED_THRESHOLD_*) - outbox retention cleanup with separate thresholds (
OUTBOX_CLEANUP_DLQ_THRESHOLD)
8. Performance Notes
- Services use select projections instead of broad row selection for list/detail views.
- Pagination is normalized and translated via
PaginationUtil, with count queries only when pagination is enabled. - Independent operations use
Promise.all(...)in critical paths (for example, outbox dispatcher scan and cleanup stats). - Hot order list/detail reads are cached in Redis with deterministic cache keys.
- Processor cache invalidation is prefix-based (
invalidatePattern(prefix*)) to avoid stale reads after async state transitions.
9. Backend Diagram
10. Release/QA Checklist
- Checkout/buy-now initiation remains synchronous and returns gateway payload.
- Payment redirect callbacks enqueue correct order jobs for success/failure.
- Cancel/refund async commands create pollable async requests and deterministic job ids.
- Status history entries preserve actor provenance constraints.
- Payment success replay does not duplicate entitlement artifacts.
- Outbox dispatcher only claims owned target queues (
orders,orders_maintenance,cart). - Outbox cleanup retention and DLQ threshold behavior is validated in non-prod run.
- Runtime env defaults are present and pass startup validation.
-
user_product_serialtable is present and enforces unique constraint on serial_number per product. -
order_item.serial_numberis set by admin PATCH endpoint, not automatically. -
order_fulfillment_historyCHECK constraint acceptsactivatedin the correct WHEN branches.
11. File Map
apps/api/src/modules/order/admin/*apps/api/src/modules/order/customer/*apps/api/src/modules/order/processing/*packages/db/src/schema/order/*
12. Environment Variables
| Variable | Default | Description |
|---|---|---|
ORDER_CHECKOUT_RATE_LIMIT | 10 | Rate limit for checkout requests per window |
ORDER_CHECKOUT_RATE_WINDOW_SECONDS | 60 | Rate limit window in seconds for checkout |
ORDER_RETRY_PAYMENT_RATE_LIMIT | 5 | Rate limit for retry payment requests |
ORDER_RETRY_PAYMENT_RATE_WINDOW_SECONDS | 60 | Rate limit window for retry payment |
ORDER_CANCEL_RATE_LIMIT | 10 | Rate limit for order cancellation requests |
ORDER_CANCEL_RATE_WINDOW_SECONDS | 60 | Rate limit window for cancellation |
ORDER_REFUND_REQUEST_RATE_LIMIT | 5 | Rate limit for refund request submissions |
ORDER_REFUND_REQUEST_RATE_WINDOW_SECONDS | 60 | Rate limit window for refund requests |
ORDER_ADMIN_OPERATION_RATE_LIMIT | 120 | Rate limit for admin order operations |
ORDER_ADMIN_RATE_WINDOW_SECONDS | 60 | Rate limit window for admin operations |
ORDER_HISTORY_CACHE_TTL_SECONDS | 120 | Cache TTL for order history in seconds |
ORDER_ADMIN_CACHE_TTL_SECONDS | 60 | Cache TTL for admin order views in seconds |
ORDER_AUTO_CANCEL_MINUTES | 30 | Minutes before unpaid orders are auto-cancelled |
ORDER_PAYMENT_RETRY_MAX_ATTEMPTS | 3 | Maximum payment retry attempts |
OUTBOX_RETENTION_DAYS | 7 | Days to retain completed outbox events |
OUTBOX_FAILED_RETENTION_DAYS | 30 | Days to retain failed outbox events |
OUTBOX_CLEANUP_ENABLED | true | Enable outbox cleanup job |
OUTBOX_CLEANUP_CRON | 0 3 * * * | Cron schedule for outbox cleanup |
ORDERS_DISPATCH_CRON | * * * * * | Cron schedule for orders dispatch |
ORDERS_FAILED_MONITOR_CRON | */5 * * * * | Cron schedule for failed order monitoring |
OUTBOX_CLEANUP_DLQ_THRESHOLD | 100 | DLQ threshold for outbox cleanup alerts |
DLQ_WARNING_THRESHOLD | 50 | Warning threshold for failed jobs |
OUTBOX_BATCH_SIZE | 300 | Batch size for outbox processing |
OUTBOX_PENDING_PREVIEW_LIMIT | 10 | Preview limit for pending outbox events |
RETRY_BASE_DELAY_MS | 2000 | Base delay for retry backoff in milliseconds |
RETRY_MAX_DELAY_MS | 120000 | Maximum retry delay in milliseconds |
PAYMENT_RETRY_DELAY_ATTEMPT_1_MS | 60000 | Delay before first payment retry |
PAYMENT_RETRY_DELAY_ATTEMPT_2_MS | 300000 | Delay before second payment retry |
PAYMENT_RETRY_DELAY_ATTEMPT_3_MS | 900000 | Delay before third payment retry |
WORKER_ORDERS_CONCURRENCY | 20 | Worker concurrency for orders queue |
WORKER_ORDERS_MAINTENANCE_CONCURRENCY | 8 | Worker concurrency for maintenance queue |
Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.
See Also
- Feature Guide: See Order - Feature List Section 6 (State Models) for order lifecycle diagram.
- API Reference: See Order - API & Integration Guide Section 7 (Endpoint Reference + Payload Cheatsheet) for API contracts.