Happy House - Ecommerce Docs
Developer ResourcesCart

Cart Module Overview

What a customer intends to buy — intent stored, live data always, and the obligation checkout inherits.

Audience: Product owners, QA, frontend and backend developers Scope: The cart surface, its idempotency model, and the contract the checkout module inherits

Cart Module - Overview

1. What the module is

The cart stores what a customer intends to buy, before an order exists: which products, and how many. Ten routes — eight customer, two read-only admin.

It stores intent and nothing else: no inventory reservation, no stock deduction, no price snapshot for charging, no shipping, no promotions. Every figure it reports is derived from live rows read at the moment of the request — so a cart response is never stale.

2. The ten routes

SurfaceRouteNotes
CustomerGET /api/mobile/cartThe cart, never cached
CustomerGET /api/mobile/cart/summaryBadge counters, 30s cache
CustomerGET /api/mobile/cart/checkout-validationThe strict re-read pass
CustomerPOST /api/mobile/cart/itemsAdd (delta; Idempotency-Key optional)
CustomerPOST /api/mobile/cart/items/bulkBatch (version required)
CustomerPUT /api/mobile/cart/items/:variantPublicIdSet quantity (absolute, upsert)
CustomerDELETE /api/mobile/cart/items/:variantPublicIdRemove line
CustomerDELETE /api/mobile/cart/itemsEmpty the cart
AdminGET /api/admin/cartsRead-only list, always paginated
AdminGET /api/admin/carts/:cartIdRead-only detail with lines

Every mutation returns the whole cart, so a client never reconciles a partial response and always receives the current version for its next write.

3. The rules that matter

  1. One statement does four jobs. Every mutation opens with a single UPDATE that takes the cart row lock, enforces status = 'active', checks an optional client-supplied version and bumps it. No new endpoint can forget any of them — and because the line-count check runs inside that lock, the 50-product cap is exact, unlike the wishlist's approximate cap.
  2. PUT is an upsert and quantity: 0 removes — including for a product that has since been archived. The line a customer most wants gone is usually the one whose product was withdrawn.
  3. Adding an out-of-stock product succeeds. The gate is on lifecycle, never on stock — stock changes minute to minute, and the cart keeps a line that goes out of stock a second after it was added. It is reported on the line and blocks checkout instead.
  4. last_known_unit_price is NOT a price snapshot. No total is computed from it. It only answers "has this changed since you chose it", and is written only on deliberate mutation — writing it on read would make GET /api/mobile/cart a write and destroy the signal. Re-sending the current quantity to PUT is the acknowledgement gesture.
  5. checkout_locked is an absorbing state until checkout owns the return edge. It holds the customer's only live-cart slot and refuses every mutation. Nothing writes it today — but checkout must own the unlock on payment failure, cancellation and reservation expiry. This is the single most important thing the checkout module inherits.

4. The pricing block, and what it deliberately omits

subtotal (valid lines only) and savingsno tax (already inside MRP and selling price), no promotionDiscount (promotions apply at checkout), no shippingFee (the cart does not know the address), and no total (with none of those in scope it would equal subtotal, and a "total" the customer will not be charged is worse than an honest subtotal).

Page guide

PageRead it for
Features and flowsActor journeys, the debounce contract, line validation, edge cases
BackendThe one-statement UPDATE, schema, the inventory seam, the oversell trap
APIAll ten endpoints, DTOs and error codes