Shop It Docs
Developer Resourcesorder

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:

  • OrderCoreModule
  • OrderCustomerModule
  • OrderAdminModule
  • OrderProcessingModule

Ownership model:

  • OrderCoreModule exports shared domain service (OrderService)
  • customer/admin modules own their HTTP surfaces
  • OrderProcessingModule owns queues, processors, maintenance scheduler, outbox dispatch, and cleanup

3. Data Model (Drizzle / PostgreSQL)

Primary tables:

  • order — includes order_type enum (physical, digital, care_package, mixed); payment_method enum (online, cod, pos) — "pos" identifies in-store POS terminal payment (no gateway redirect); order_source enum: 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 — includes care_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 lines
  • order_status_history
  • order_fulfillment_history — tracks fulfillment transitions (received, ready, picked_up, in_transit, delivered, key_sent, activated)
  • async_request
  • outbox_event
  • user_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.ts
  • packages/db/src/schema/order/orders.ts
  • packages/db/src/schema/order/order-items.ts
  • packages/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_id
  • actor_type = admin + created_by_admin_id
  • actor_type = system + both null

4. Runtime Rules and Domain Invariants

4.1 Synchronous initiation paths

  • POST /api/orders/checkout
  • POST /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 with ORDER_DIGITAL_NOT_REFUNDABLE

Domain invariant — care package buy-now is not supported:

  • createBuyNow() rejects productKind = "care_package" with ORDER_BUYNOW_CARE_PACKAGE_NOT_ALLOWED
  • Care packages must be purchased through cart checkout

Order type resolution (both checkout and buy-now):

  • productKind on the product determines order_type: physicalphysical, digitaldigital, care_packagecare_package
  • If cart items have mixed product kinds, order type resolves to mixed
  • Digital-only and care-package-only carts skip shipping: no shipping_district required, 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.reserve outbox events are enqueued during order creation. OrderProcessor.processReservationResult() sets reservationStatus = "reserved" asynchronously after the inventory worker processes them.
  • Digital-only, care-package-only orders: no stock.reserve events are enqueued (non-physical products have no inventory to reserve). OrderService detects this inside the creation transaction and sets reservationStatus = "reserved" synchronously — otherwise waitForReservationResolution() would time out waiting for an async job that will never run, producing a 503.
  • POS orders (createFromPosItems): POS does not call waitForReservationResolution, 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 anyPhysicalReservationEnqueued flag.

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); COD cod_pending orders 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/success
  • GET|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_confirmed job now resolves the linked cart via orders.cartId and calls CartCustomerService.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_failed with paymentStatus=failed
  • appends payment_failed status history
  • emits stock.release outbox events for each product in the order
  • cancels any pending_activation care-package subscriptions linked to the order
  • restores the linked cart (identified via orders.cartId) from "checkout" back to "active", using an atomic NOT 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

  • orders
  • orders_maintenance
  • cart (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:

  • OutboxDispatcherService claims pending rows only for owned target queues:
    • orders
    • orders_maintenance
    • cart

Cleanup flow:

  1. OrdersMaintenanceScheduler enqueues orders_maintenance.cleanup_outbox.
  2. OrdersMaintenanceProcessor runs OutboxCleanupService.cleanup().
  3. Completed rows older than OUTBOX_RETENTION_DAYS and failed rows older than OUTBOX_FAILED_RETENTION_DAYS are removed in batches.
  4. DLQ health check compares failed-job count against OUTBOX_CLEANUP_DLQ_THRESHOLD and 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:

  1. For each order_item row with care_package_subscription_id set:

    • The care_package_subscriptions row was created at checkout time with status = "pending_activation".
    • Processor enqueues care_package.activate_subscription to QueueName.CARE_PACKAGE: { subscriptionId: item.carePackageSubscriptionId, orderId: order.id }
  2. Activation is asynchronous. The subscription moves to status = "active" in a separate BullMQ job (CarePackageProcessor.processActivation()).

  3. If order is cancelled after payment, processor dispatches care_package.cancel_subscription for each subscription to transition them to cancelled.

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 legacy countries prefix)

Cross-module keys invalidated by order processing when applicable:

Configured TTLs:

  • ORDER_HISTORY_CACHE_TTL_SECONDS (default 120)
  • ORDER_ADMIN_CACHE_TTL_SECONDS (default 60)

6. Rate Limiting Strategy

Customer-side controls:

  • ORDER_CHECKOUT_RATE_LIMIT (default 10)
  • ORDER_RETRY_PAYMENT_RATE_LIMIT (default 5)
  • ORDER_CANCEL_RATE_LIMIT (default 10)
  • ORDER_REFUND_REQUEST_RATE_LIMIT (default 5)
  • ORDER_CHECKOUT_RATE_WINDOW_SECONDS (default 60)

Admin-side controls:

  • ORDER_ADMIN_OPERATION_RATE_LIMIT (default 120)
  • ORDER_ADMIN_RATE_WINDOW_SECONDS (default 60)

All rate limiting paths use Redis-backed counters/sliding windows and throw RateLimitExceededException on breach.

Additional order runtime knobs:

  • ORDER_AUTO_CANCEL_MINUTES (default 30)
  • ORDER_PAYMENT_RETRY_MAX_ATTEMPTS (default 3)
  • OUTBOX_RETENTION_DAYS (default 7)
  • OUTBOX_FAILED_RETENTION_DAYS (default 30)
  • OUTBOX_CLEANUP_ENABLED (default true)
  • OUTBOX_CLEANUP_CRON (default 0 3 * * *)
  • ORDERS_DISPATCH_CRON (default * * * * *)
  • ORDERS_FAILED_MONITOR_CRON (default */5 * * * *)
  • OUTBOX_CLEANUP_DLQ_THRESHOLD (default 100)
  • DLQ_WARNING_THRESHOLD (default 50)
  • OUTBOX_BATCH_SIZE (default 300)
  • OUTBOX_PENDING_PREVIEW_LIMIT (default 10)
  • RETRY_BASE_DELAY_MS (default 2000)
  • RETRY_MAX_DELAY_MS (default 120000)
  • PAYMENT_RETRY_DELAY_ATTEMPT_1_MS (default 60000)
  • PAYMENT_RETRY_DELAY_ATTEMPT_2_MS (default 300000)
  • PAYMENT_RETRY_DELAY_ATTEMPT_3_MS (default 900000)
  • WORKER_ORDERS_CONCURRENCY (default 20)
  • WORKER_ORDERS_MAINTENANCE_CONCURRENCY (default 8)

7. Error and Resilience Contracts

Representative API/domain errors:

HTTPerrorCodeMessage
400ORDER_CART_INACTIVEOnly active cart can be checked out.
400ORDER_CART_EMPTYCart has no items.
400ORDER_PAYMENT_AMOUNT_MISMATCHPayment amount mismatch detected.
400ORDER_PRODUCT_NOT_AVAILABLEProduct is not available for purchase.
400ORDER_PAYMENT_METHOD_NOT_SUPPORTEDOnly online payment is supported.
400ORDER_CANCEL_NOT_ALLOWEDOrder cannot be cancelled in current status.
400ORDER_DIGITAL_NOT_REFUNDABLEDigital product orders are non-refundable.
400ORDER_NOT_PENDING_REFUNDOrder is not pending refund processing.
400ORDER_NOTE_REQUIREDNote is required.
403ORDER_REFUND_ACCESS_DENIEDYou do not have permission to request refund for this order.
404ORDER_CART_NOT_FOUNDCart not found.
404ORDER_ASYNC_REQUEST_NOT_FOUNDAsync request not found.
404ORDER_NOT_FOUNDOrder not found.
409ORDER_IDEMPOTENCY_MISMATCHRequest with this idempotency key is already in progress.
400ORDER_FULFILLMENT_NOT_PHYSICAL_TYPEOrder type does not support fulfillment advance.
400ORDER_FULFILLMENT_NOT_DIGITAL_TYPEOrder type does not support digital delivery.
400ORDER_ACTIVATE_CARE_PACKAGE_NOT_APPLICABLEOrder is not a care package type.
400ORDER_NOT_PAIDOrder must be in paid status.
409ORDER_SERIAL_ALREADY_ASSIGNEDSerial number already assigned to this item.
409ORDER_SERIAL_DUPLICATESerial number already in use.
422ORDER_BUYNOW_CARE_PACKAGE_NOT_ALLOWEDCare package products do not support buy-now.
422ORDER_SERIAL_ITEM_NOT_PHYSICALSerial number can only be assigned to physical items.
422ORDER_SERIAL_AFTER_DELIVEREDCannot assign serial after order is delivered.
422CART_CARE_PACKAGE_ITEM_NOT_PHYSICALCare package attachment requires a physical cart item.
503ERR_CONFIG_INVALIDConfiguration value is missing/invalid.
503ORDER_ASYNC_REQUEST_CREATION_FAILEDFailed to create async order request.
429RATE_LIMIT_EXCEEDEDToo 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_serial table is present and enforces unique constraint on serial_number per product.
  • order_item.serial_number is set by admin PATCH endpoint, not automatically.
  • order_fulfillment_history CHECK constraint accepts activated in 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

VariableDefaultDescription
ORDER_CHECKOUT_RATE_LIMIT10Rate limit for checkout requests per window
ORDER_CHECKOUT_RATE_WINDOW_SECONDS60Rate limit window in seconds for checkout
ORDER_RETRY_PAYMENT_RATE_LIMIT5Rate limit for retry payment requests
ORDER_RETRY_PAYMENT_RATE_WINDOW_SECONDS60Rate limit window for retry payment
ORDER_CANCEL_RATE_LIMIT10Rate limit for order cancellation requests
ORDER_CANCEL_RATE_WINDOW_SECONDS60Rate limit window for cancellation
ORDER_REFUND_REQUEST_RATE_LIMIT5Rate limit for refund request submissions
ORDER_REFUND_REQUEST_RATE_WINDOW_SECONDS60Rate limit window for refund requests
ORDER_ADMIN_OPERATION_RATE_LIMIT120Rate limit for admin order operations
ORDER_ADMIN_RATE_WINDOW_SECONDS60Rate limit window for admin operations
ORDER_HISTORY_CACHE_TTL_SECONDS120Cache TTL for order history in seconds
ORDER_ADMIN_CACHE_TTL_SECONDS60Cache TTL for admin order views in seconds
ORDER_AUTO_CANCEL_MINUTES30Minutes before unpaid orders are auto-cancelled
ORDER_PAYMENT_RETRY_MAX_ATTEMPTS3Maximum payment retry attempts
OUTBOX_RETENTION_DAYS7Days to retain completed outbox events
OUTBOX_FAILED_RETENTION_DAYS30Days to retain failed outbox events
OUTBOX_CLEANUP_ENABLEDtrueEnable outbox cleanup job
OUTBOX_CLEANUP_CRON0 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_THRESHOLD100DLQ threshold for outbox cleanup alerts
DLQ_WARNING_THRESHOLD50Warning threshold for failed jobs
OUTBOX_BATCH_SIZE300Batch size for outbox processing
OUTBOX_PENDING_PREVIEW_LIMIT10Preview limit for pending outbox events
RETRY_BASE_DELAY_MS2000Base delay for retry backoff in milliseconds
RETRY_MAX_DELAY_MS120000Maximum retry delay in milliseconds
PAYMENT_RETRY_DELAY_ATTEMPT_1_MS60000Delay before first payment retry
PAYMENT_RETRY_DELAY_ATTEMPT_2_MS300000Delay before second payment retry
PAYMENT_RETRY_DELAY_ATTEMPT_3_MS900000Delay before third payment retry
WORKER_ORDERS_CONCURRENCY20Worker concurrency for orders queue
WORKER_ORDERS_MAINTENANCE_CONCURRENCY8Worker 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