# Cart Management in `apps/user-ui` — Detailed Documentation > How the customer cart works end-to-end: local storage, React Query hooks, the two cart types (regular + flash), pricing, slots, coupons, checkout handoff, and all edge cases. --- ## 1. Architecture Overview The cart is **100% client-side / local-first**. There is **no server cart** — items live in local storage (SecureStore on native, localStorage on web via `StorageServiceCasual`), and are resolved against the cached product catalog + slot/availability data on the device. ``` AddToCartDialog / ProductCard / FloatingCartBar │ useCartStore.setAddedToCartProduct() (trigger dialog) ▼ useAddToCart / useUpdateCartItem / useRemoveFromCart (cart-query-hooks.tsx) │ mutations write to local storage ▼ StorageServiceCasual (key: "cart_items" | "flash_cart_items") ▲ useGetCart() (query reads local storage + merges product/availability data) │ cart-page.tsx / floating-cart-bar.tsx / checkout-page.tsx ``` ### Key files | File | Role | |---|---| | `hooks/cart-query-hooks.tsx` | The core: local-storage CRUD + React Query hooks (`useGetCart`, `useAddToCart`, `useUpdateCartItem`, `useRemoveFromCart`, `clearLocalCart`). | | `src/store/cartStore.ts` | Zustand: `addedToCartProduct` — which product opened the AddToCart dialog. | | `src/store/flashCartStore.ts` | Zustand: `addedFlashProduct` — which product was routed to the flash flow. | | `src/store/centralProductStore.ts` | Zustand: cached products + `productsById` map (from `useAllProducts`). | | `src/store/centralSlotStore.ts` | Zustand: slots + `productSlotsMap` (per-product availability/out-of-stock/flash). | | `components/cart-page.tsx` | The full cart screen (regular + flash via prop). | | `components/floating-cart-bar.tsx` | Collapsible cart summary bar (count, total, free-delivery progress). | | `src/components/AddToCartDialog.tsx` | Slot/quantity/flash chooser when adding/updating an item. | | `components/checkout-page.tsx` | Reads cart via URL params (`slots=`, `coupons=`, `deliveryPrice=`) — the cart is passed by **reference**, not re-read. | --- ## 2. Data Model ### Local storage item (`LocalCartItem`) ```ts { id: number, // local incrementing id (max existing id + 1) skuId: number, // the product/SKU id quantity: number, slotId: number, // delivery slot id; 0 = flash/unspecified addedAt: string, // ISO timestamp } ``` Stored under `cart_items` (regular) or `flash_cart_items` (flash) via `StorageServiceCasual` (SecureStore native / localStorage web). ### Computed cart (`CartData`) ```ts { items: CartItem[], totalItems: number, // item count (not quantity) totalAmount: number, // Σ(price × quantity) } ``` `CartItem` = local item + `subtotal` (computed from `productBasic.price`). ### Product resolution `useGetCart` builds `items` by looking up each `skuId` in `productsById` (central product store) **and** requiring `productSlotsMap[skuId]` to exist — **if either is missing, the item is silently dropped** from the cart view (not deleted from storage). --- ## 3. Flow: Add to Cart 1. **Trigger**: `ProductCard` / home / store / search call `useCartStore.setAddedToCartProduct({ productId, product })` → the `AddToCartDialog` opens (regular) OR `useFlashCartStore.setAddedFlashProduct` (flash). 2. **Dialog** (`AddToCartDialog.tsx`): - Lists **available slots** for the product (from `useSlots()` → `productSlotIdsMap`, filtered to future `freezeTime`). - Shows **"1 hr Delivery"** option if `productSlotsMap[id].isFlashAvailable` AND flash is enabled. - Quantity via `Quantifier` (step 1 — not the product's `incrementStep`!). - **Pre-fills** from existing cart item: if the product is already in the cart, it loads the existing quantity + slotId. - "Add to Cart" / "Update Item" (if already in cart) / "Remove" (if updating). 3. **Mutation** (`useAddToCart` / `useUpdateCartItem`): - `addToLocalCart`: if `skuId` exists → **merge quantity** (add to existing) + optionally update slotId; else push new item. - Requires `slotId` (throws if null). - `updateLocalCartItem`: set `quantity` on the item (by local `id`). - `removeFromLocalCart`: filter out by local `id`. 4. **Flash special-case**: choosing "1 hr Delivery" sets `useFlashCartStore.setAddedFlashProduct(...)` and navigates to the flash products page — it does **NOT** add to the regular cart via the dialog; the flash cart is a separate local list. --- ## 4. Flow: Cart Page `cart-page.tsx` (`isFlashDelivery` prop selects `cartType`): - **Loading**: blocks on `useGetCart` loading + (regular only) `getCartSlots` loading. - **Per-item**: - Image, name, unit, `Quantifier` (step = product `incrementStep`). - Quantity to **0** → confirmation alert → remove. - **Slot selection** (regular only): `BottomDropdown` from `trpc.user.cart.getCartSlots({ productIds })`, filtered to future slots. Flash forces `slotId = 0`. - Price: regular uses `product.price`; flash uses `product.flashPrice ?? product.price`. - **Availability** logic per item: - `productSlotInfo.isOutOfStock` → unavailable "Out of Stock". - Flash: not flash-eligible → "Not available for flash delivery. Please remove". - Regular: no slots → "No delivery slots available". - **Auto-slot effect**: on `slotsData`/`cartItems` change, auto-selects each item's slot (keeps existing if still valid, else first upcoming; flash = 0). - **Coupons**: `trpc.user.coupon.getEligible` → eligible list; min-order + usage-limit filtering client-side; discount = percent (capped at `maxValue`) or flat (capped at total); applied coupon id passed to checkout via URL. - **Bill**: Item Total → Product Discount → Coupon Applied → Delivery Fee (0 if `finalTotal >= threshold`) → To Pay. Free-delivery nudge + "You saved ₹X" banner. - **Checkout handoff**: builds `slots=:;...` + `coupons=` + `deliveryPrice=` URL params → navigates to `/home/checkout` or `/flash-delivery/checkout`. - **Guard**: if no available items → alert (all OOS / none flash-eligible / missing slots) and blocks checkout. --- ## 5. Flow: Floating Cart Bar - Reads `useGetCart` for the active cart type; shows count + `totalCartValue` + free-delivery progress (regular vs flash thresholds). - **Expanded dialog**: list with `MiniQuantifier` (0 = remove), per-item slot dropdown (regular), subtotal, "Go to cart" → cart page. - **Slot-reassignment effect** (runs once on mount): for regular items whose `slotId` is no longer in `productSlotsMap[skuId].slots`, it **removes + re-adds** the item with the nearest slot (`removeFromCart.mutate` + `addToCartHook.addToCart`). (Note: `addToCart(item.skuId, item.quantity, nearestSlotId)` — passing `quantity` as the *quantity* arg.) --- ## 6. Checkout integration - Checkout does **not** re-read local storage directly — it receives the cart items **by URL params** (`slots=`, `coupons=`, `deliveryPrice=`) from the cart page. - This means the checkout's item set/quantities/slots are **frozen at navigation time**; if the cart changes after, checkout uses the snapshot. --- ## 7. Edge Cases & Observations ### Data / storage 1. **Missing product or slot data → item dropped from view** (`useGetCart` returns `null` for items lacking `productBasic` or `productAvailability`). The item stays in storage but is invisible — potential "phantom cart" if product is later removed from catalog. 2. **Duplicate add merges quantity** (same `skuId` → `quantity +=`). No separate line items. 3. **`slotId: 0` sentinel** for flash — 0 is not a real slot; code must treat 0 as "flash/unspecified". 4. **Local `id` generation** = `max(existing ids) + 1`. Deleting the highest-id item then adding reuses ids (fine) — but concurrent adds (two rapid taps) could race since id is computed from the read-then-write sequence (no lock). 5. **Storage failures** (`StorageServiceCasual.setItem` error) are caught by the service and return false — the mutation still resolves, but the write may not have persisted (silent data loss risk). ### Slot / availability 6. **Auto-slot reassignment**: if a cart item's saved slot becomes invalid, the cart page picks the first upcoming slot silently; the floating bar instead does remove+re-add. Two different behaviors for the same situation. 7. **Slot changes in cart page are local-only** (`selectedSlots` state) — they're NOT written back to `LocalCartItem.slotId` until checkout builds the URL. (The `useEffect` reads `cartData` slotId but the dropdown only updates `selectedSlots`.) → **If the user leaves the cart without checking out, the new slot choice is lost.** 8. **`AddToCartDialog` requires a slot** — if a product has no future slots, the dialog can't add it (button disabled). Good, but the flash path can bypass via "1 hr Delivery". 9. **Flash eligibility** is from `productSlotsMap[id].isFlashAvailable` — but the flash cart page also checks `flashEligibleProductIds` (derived from `products` + `productSlotsMap`). Two sources that should agree. 10. **Out-of-stock mid-cart**: if a product becomes OOS after adding, cart page shows "Out of Stock" and excludes it from totals/checkout, but does NOT auto-remove it. ### Quantity / pricing 11. **Dialog step = 1** vs **cart page step = product.incrementStep** — inconsistent stepping between the two add/update UIs. (A product with 500g increment could be set to any integer in the dialog.) 12. **`baseTotalPrice` (coupon eligibility) uses `product.price`** (not flash price) even in flash cart — coupon min-order check may be wrong for flash. 13. **`totalPrice` filters OOS items** — good — but the `Quantifier` still shows them; quantity edits on OOS items are allowed. 14. **Flash pricing**: `flashPrice ?? price` — if `flashPrice` is `0` or `null`, falls back to base price; if `flashPrice` is a valid string, used. (Floating bar + cart page consistent.) 15. **Discount cap for flat coupons**: `Math.min(flatDiscount, maxValue || totalPrice)` — reasonable; percent capped at `maxValue || Infinity`. ### Coupons 16. **Usage-limit coupons are filtered out entirely** from the dropdown (`.filter(coupon => coupon.ineligibilityReason !== "Usage limit exceeded")`) — a used-up coupon disappears rather than showing "used up". 17. **Min-order coupons show disabled** with reason — good. 18. **Selected coupon is only applied to available items' total** — matches backend intent (coupon applies to non-OOS items). 19. **Coupon passed by id to checkout** — checkout re-validates on the backend; client discount is a preview. ### Checkout / navigation 20. **Cart is snapshotted into URL params** — long carts → long URLs; quantities/slots frozen at navigation. 21. **`availableItems.length === 0` guard** produces specific alerts (all OOS / none flash-eligible / missing slots) — good UX. 22. **Partial availability**: if only some items are available, checkout proceeds with only the available ones — the unavailable ones stay in the cart (not removed). 23. **Cart type switcher** navigates between regular/flash flows, but does NOT move items — each cart type is independent. ### Flash-specific 24. **Flash cart items have `slotId: 0`** — the cart page forces `selectedSlots[item.id] = 0` for flash. 25. **Adding via "1 hr Delivery" in the dialog does NOT add to the regular cart** — it sets `addedFlashProduct` and navigates to flash products; the actual add happens on the flash page. (If the user abandons the flash page, nothing was added.) 26. **`isFlashDeliveryEnabled` gate** — flash option only shows if the const is true. ### Concurrency / timing 27. **`useGetCart` enabled only when `productsById` is populated** — before the central product store loads, the cart query is disabled (no stale empty cart flash). Good. 28. **`refetchOnWindowFocus`** default true → returning to the app refreshes cart totals (from local storage) but prices come from cached products — price updates in the catalog reflect on next product refetch. 29. **Floating bar slot-reassignment runs once (`[]` deps)** — on mount only; slot validity isn't re-checked on later slot-data changes. 30. **Remove-then-add in floating bar** for invalid slots changes the local item `id` (new id) — any UI holding the old id could mismatch. --- ## 8. Potential improvements (suggestions, not required) - **Persist slot changes**: write `selectedSlots` back to `LocalCartItem.slotId` so slot choices survive leaving the cart. - **Unify quantity step** (dialog step=1 vs cart incrementStep). - **Fix flash coupon min-order** to use flash price. - **Handle "phantom items"**: purge local cart entries whose product no longer exists (or show them as unavailable). - **Serialize add-to-cart writes** (mutex/queue) to avoid id races on rapid taps. - **Surface storage write failures** instead of silently succeeding. --- *Documentation based on code reading of `cart-query-hooks.tsx`, `cartStore.ts`, `flashCartStore.ts`, `centralProductStore.ts`, `centralSlotStore.ts`, `cart-page.tsx`, `floating-cart-bar.tsx`, `AddToCartDialog.tsx`, and `checkout-page.tsx`.*