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
| Surface | Route | Notes |
|---|---|---|
| Customer | GET /api/mobile/cart | The cart, never cached |
| Customer | GET /api/mobile/cart/summary | Badge counters, 30s cache |
| Customer | GET /api/mobile/cart/checkout-validation | The strict re-read pass |
| Customer | POST /api/mobile/cart/items | Add (delta; Idempotency-Key optional) |
| Customer | POST /api/mobile/cart/items/bulk | Batch (version required) |
| Customer | PUT /api/mobile/cart/items/:variantPublicId | Set quantity (absolute, upsert) |
| Customer | DELETE /api/mobile/cart/items/:variantPublicId | Remove line |
| Customer | DELETE /api/mobile/cart/items | Empty the cart |
| Admin | GET /api/admin/carts | Read-only list, always paginated |
| Admin | GET /api/admin/carts/:cartId | Read-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
- One statement does four jobs. Every mutation opens with a single
UPDATEthat takes the cart row lock, enforcesstatus = 'active', checks an optional client-suppliedversionand 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. PUTis an upsert andquantity: 0removes — including for a product that has since been archived. The line a customer most wants gone is usually the one whose product was withdrawn.- 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.
last_known_unit_priceis 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 makeGET /api/mobile/carta write and destroy the signal. Re-sending the current quantity toPUTis the acknowledgement gesture.checkout_lockedis 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 savings — no 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
| Page | Read it for |
|---|---|
| Features and flows | Actor journeys, the debounce contract, line validation, edge cases |
| Backend | The one-statement UPDATE, schema, the inventory seam, the oversell trap |
| API | All ten endpoints, DTOs and error codes |