Shop It Docs
Developer Resourcescart

Cart Module Backend Documentation

Backend architecture and data contracts for cart, quantity operations, manual address shipping projection, caching, and admin support reads.

Cart Module - Backend Documentation

1. Domain Scope

Cart manages:

  • active cart creation/fetch
  • item add/remove
  • quantity operations (increase/decrease/set)
  • applied coupon persistence + post-mutation revalidation
  • shipping location projection (district + address + fee)
  • public guest quote (read-only pricing + shipping projection)
  • cache invalidation

Cart does not initiate payment directly. Payment initiation is owned by order endpoints.

2. Data Model

Source of truth:

  • packages/db/src/schema/cart/cart.ts
  • packages/db/src/schema/config/shipping-fee.schema.ts

2.1 cart table

Important fields:

  • id
  • user_id
  • status
  • applied_promotion_id
  • applied_promotion_code
  • applied_discount
  • shipping_district
  • shipping_mobile_num
  • shipping_instructions
  • shipping_fee
  • shipping_address_line_1
  • shipping_address_line_2
  • shipping_city
  • shipping_state
  • shipping_postal_code
  • shipping_landmark
  • shipping_latitude
  • shipping_longitude
  • reservation_expired_at — set when cart stock reservation expires due to inactivity. Items are preserved; checkout re-reserves stock.
  • created_at
  • updated_at

Important constraints/indexes:

  • one active cart per user (cart_one_active_per_user partial unique)
  • non-negative applied_discount
  • non-negative shipping_fee

2.2 cart_item table

Important fields:

  • id
  • cart_id
  • product_id
  • unit_mrp
  • unit_sp
  • quantity
  • created_at
  • updated_at

Important constraints/indexes:

  • unique cart-product pair (cart_item_cart_product_idx)
  • non-negative unit_mrp and unit_sp

2.3 cart_care_package_items table

Important fields:

  • id
  • cart_id (FK → cart.id, CASCADE DELETE)
  • cart_item_id (FK → cart_item.id, CASCADE DELETE)
  • care_package_id (FK → care_packages.id, RESTRICT)
  • pricing_tier_id (FK → care_package_pricing_tiers.id, RESTRICT)
  • unit_price (integer, in paisa) — effective price at add time
  • created_at, updated_at

Important constraints/indexes:

  • Unique (cart_item_id) — enforces 1 care package per cart item
  • FK index on care_package_id
  • FK index on pricing_tier_id

3. Service Contracts

Primary service:

  • CartCustomerService

Key methods:

  • getCart(userId)
  • addItem(userId, dto) — guards via isSellableProductKind(productKind); only physical, digital, and care_package product kinds are addable
  • removeItem(userId, itemId)
  • increaseItemQuantity(userId, itemId)
  • decreaseItemQuantity(userId, itemId)
  • setItemQuantity(userId, itemId, quantity)
  • getLocation(userId)
  • listAvailableShippingDistricts()
  • quoteGuestCart(dto)
  • setLocationManual(userId, dto)
  • setLocationAuto(userId, geocodeDto)
  • clearLocation(userId)
  • clearCart(userId) — removes all items from the active cart
  • completeCart(cartId, userId) — clears items from a specific "checkout"-status cart, releases inventory reservations, resets promotion fields, and marks the cart "completed". Called by the cart.clear_after_order_confirmed job on payment success. Idempotent: no-ops if the cart is not in "checkout" status

4. Response Mapping Notes

buildCartResponse(...) composes mobile/customer cart response:

  • includes per-line quantity
  • includes per-line productKind (physical, digital, care_package) resolved via JOIN with products
  • maps each item discount as per-unit discount: unitMrp - effectiveUnitSp
  • computes totals using quantity-aware aggregation
  • can include appliedCoupon when an active cart-level promotion is present
  • can include promoNotice when a previously applied coupon was removed during the current cart mutation
  • includes shipping projection fields when present; shipping fields are absent when the cart has no shipping location set (e.g. digital-only carts)

When cart.reservation_expired_at is set, the response includes:

  • reservationExpired: true — signals the frontend to show a banner that the stock reservation expired and will be re-attempted at checkout.

Post-mutation coupon flow:

  • addItem, removeItem, increaseItemQuantity, decreaseItemQuantity, setItemQuantity, and syncCart mutate the cart first
  • then finalizeCartResponse() re-evaluates the current appliedCoupon against the updated cart
  • if still valid, the stored discount is refreshed when needed
  • if invalid, cart promo fields are cleared and the rebuilt response includes:
    • promoNotice.status = "promo_removed"
    • promoNotice.reason (promotion_inactive, promotion_expired, promotion_not_started, usage_limit_reached, per_user_limit_reached, min_cart_value_not_met, scope_mismatch, cart_type_mismatch)
    • promoNotice.message = "This promo is no longer valid for your current cart."

Money guardrails:

  • cart/order calculations use shared minor-unit validation before aggregation
  • effective sell price is resolved with fallback: effectiveUnitSp = unitSp > 0 ? unitSp : unitMrp
  • invalid monetary inputs (NaN, float, negative) fail fast before persistence/payment paths

Final payable logic:

finalPayable = totalSp - appliedDiscount + shippingFee

where null values fallback to 0.

Guest quote uses the same shared pricing helper as authenticated cart mapping, but with:

finalPayable = totalSp + shippingFee

because coupon application is not part of guest quote flow.

Checkout boundary:

  • cart responses own coupon maintenance after cart mutations
  • checkout/order creation still performs final validation as a safety guard
  • payment-method mismatch remains a checkout concern, not a cart promoNotice reason in the current flow

5. Location + Shipping Behavior

  • location is cart-level, not line-level
  • available district list reads active rows from shipping_fee ordered by district name
  • manual location flow resolves fee by district name from shipping_fee
  • guest quote resolves fee by district name from shipping_fee and combines it with backend-priced product lines plus optional items[].carePackage add-ons
  • manual flow persists full address snapshot on cart
  • auto location endpoint is disabled by default and returns 410
  • if shipping fee does not exist for district: throws CART_LOCATION_DISTRICT_NOT_AVAILABLE

5.1 Digital Product Handling

  • Digital products (productKind = "digital") and care packages (productKind = "care_package") do not require shipping.
  • When a cart contains only non-physical items, shipping fields (shippingDistrict, shippingFee, address) are omitted from the cart response.
  • The checkout flow reads hasPhysicalItem from cart items and skips shippingDistrict validation when all items are digital/care-package.

6. Caching

Cache prefix:

  • cart:user:

Behavior:

  • reads use cache-aside
  • cart/location/item mutations invalidate user cart key pattern
  • Redis failures degrade gracefully to DB behavior with warning logs

7. Rate Limiting

Controller-level Redis sliding window for mutating endpoints.

Defaults:

  • CART_CUSTOMER_OPERATION_RATE_LIMIT=60
  • CART_CUSTOMER_RATE_WINDOW_SECONDS=60

Exceeded threshold throws RateLimitExceededException with standard envelope.

8. API Surfaces

Customer/mobile

  • /api/cart/*
  • /api/mobile/cart/*
  • includes public POST /cart/guest/quote for unauthenticated pricing preview, including optional care package add-ons

Admin

  • /api/admin/cart/users/:userId

Admin read uses CartAdminService and returns active cart for support/debug.

9. Cart Lifecycle & Async Integration

9.1 Cart Status Lifecycle

A cart transitions through these statuses during its lifecycle:

  • "active" — default state for an in-progress cart. Only one active cart per user (cart_one_active_per_user partial unique index).
  • "checkout" — set during order creation (createFromCart). The cart is locked but still visible/queryable. Used to link the order to its cart via orders.cartId. See fix-cart-clear-timing feature.
  • "completed" — set by CartCustomerService.completeCart() on payment success. The cart's items have been cleared, inventory reservations released, and promotion fields reset.
  • "abandoned" — set by payment-failure restore when the user already has a different active cart (collision case).

9.2 Async Queue Jobs

Cart queue jobs:

  • cart.clear_after_order_confirmed — on payment success, resolves orders.cartId from the order, calls CartCustomerService.completeCart(cartId, userId) to clear items and mark the cart "completed". Skips cleanly when the order has no linked cart (buy-now/POS/pre-migration orders).
  • cart.expire — releases stock reservation for idle carts after configurable inactivity TTL (CART_EXPIRE_IDLE_MS, default 30 min). Sets reservation_expired_at on the cart and clears promotion fields. Does not delete items or abandon the cart — items remain visible so the user sees what they added. On checkout, stock is re-reserved; if unavailable, the user gets per-item errors. Cache is invalidated so the frontend reflects the new state.

9.3 Payment-Failure Cart Restore

On payment failure, OrderProcessor.processPaymentFailed() restores the linked cart:

  • If the cart is still "checkout" and the user has no other active cart: restored to "active".
  • If the cart is still "checkout" but the user already has a different active cart: marked "abandoned" (collision case).
  • If the order has no linked cart (cartId = null): skipped.
  • The restore uses an atomic NOT EXISTS-guarded conditional update — no exception-based handling, safe to replay.

9.4 clearCart vs completeCart

  • clearCart(userId) — legacy method, clears the user's active cart (resolved via getActiveCart). Used by manual cart-clear flows.
  • completeCart(cartId, userId) — new method, clears a specific cart identified by cartId, conditional on it being in "checkout" status. Used by the payment-success job. Does not replace clearCart (both coexist).

10. Care-Package Cart Module

Service: CarePackageCartService

Key methods:

  • addCarePackage(cartItemId, dto, customerId) — validates cart item belongs to user, product is physical, care package is active, eligibility rules match the product, tier is active, and no existing attachment; writes cartCarePackageItems row
  • removeCarePackage(cartItemId, customerId) — validates cart item belongs to user; deletes cartCarePackageItems row; returns confirmation

Response integration:

  • Cart response from buildCartResponse() includes carePackageItems field — array of CartCarePackageDto with { id, cartItemId, carePackageId, carePackageName, pricingTierId, durationMonths, unitPrice }
  • finalPayable aggregation includes care package unitPrice contributions via calculateCartPricing

11. Error Codes (Cart-Relevant)

Cart Codes

  • CART_NOT_FOUND
  • CART_ITEM_NOT_FOUND
  • CART_PRODUCT_NOT_FOUND
  • CART_PRODUCT_NOT_ELIGIBLE
  • CART_INVALID_ITEM
  • CART_LOCATION_SHIPPING_NOT_AVAILABLE
  • CART_LOCATION_DISTRICT_NOT_AVAILABLE
  • CART_LOCATION_AUTO_DISABLED
  • RATE_LIMIT_EXCEEDED

Care-Package Cart Codes

  • CARE_PACKAGE_CART_ITEM_NOT_FOUND — cart item not found or does not belong to user
  • CARE_PACKAGE_CART_ITEM_ALREADY_HAS_PACKAGE — unique constraint violation
  • CARE_PACKAGE_NOT_ELIGIBLE_FOR_PRODUCT — selected package eligibility rules do not match the cart product
  • CARE_PACKAGE_CART_PACKAGE_NO_LONGER_AVAILABLE — package deactivated after initial add
  • CARE_PACKAGE_CART_PRICING_TIER_NO_LONGER_ACTIVE — tier deactivated after initial add