# WP3 — Cart (guest cart · merge-on-login · cart page · coupon)

**Repo:** FE only — `/home/moonui4/public_html/moon-erp` (branch `hazemdev4`)
**Depends on:** WP0 ✅ · WP1 ✅ `05744239` · WP2 ✅ `64742cfa` · WPS ✅ (seeded catalog)
**Review:** code-reviewer · **[FIN]:** no (money is *displayed*, not computed — totals come from the server) · **Migration:** no

## Goal

Make the cart real: a guest can add products without an account, the cart survives a reload, and the moment they log in their local cart is replayed into the server cart without losing or duplicating anything. Plus the cart page itself — lines, quantity steppers, coupon, totals, empty state.

## 🔴 The one hard architectural fact

**A6: there is NO guest cart on the backend. Every `/cart/*` route requires auth.**

So there are two cart implementations and you must keep them cleanly separated:

| | Logged out | Logged in |
|---|---|---|
| Storage | `localStorage` (WP0's `cart.state.ts`) | server, via `/api/store/cart` |
| Source of truth for totals | **client-side estimate only** | **the server response — always** |

Never show a client-computed total as if it were authoritative once the user is logged in. The server owns pricing, discounts and coupon math.

## The merge-on-login replay — the risky part

When a guest with a local cart logs in, replay each local line via `POST /cart/items`, then adopt the server cart as the source of truth and clear the local one.

Get this wrong and the user loses items or gets doubles. Requirements:

- **Idempotent-ish and crash-safe:** if the replay dies halfway (network drop, 401, tab closed), the user must not end up with a half-merged cart *and* an emptied local cart. Clear local **only after** the server cart is confirmed.
- **Handle the "already has a server cart" case** — a returning customer may already have lines. Decide and document: merge quantities, or keep both. Recommended: **add the guest quantities on top**, then re-read the server cart.
- **Per-item failure must not abort the whole merge.** A product that went out of stock or was deleted since it was added will 422. Skip it, keep the rest, and tell the user which items dropped — via the toast service, in translated copy, never a silent loss.
- **Sequence guard:** a slow replay response must not clobber a newer cart state (same class of bug WP1 and WP2 both hit).

WP4 (auth screens) does **not** exist yet, so you cannot log in through the UI. Test the replay by obtaining a real token directly from the API (`POST /api/store/auth/register` or `/login` — registration needs `branch_id`, `name`, `mobile`, `password`, `password_confirmation`; the token comes back as a **top-level `token` key, sibling of `data`**, per A3) and injecting it into the auth state's localStorage key. Say plainly in your report that this is how you tested it.

## Pages / components

Theme (read-only): `/tmp/claude-0/-home-moonui4-public-html/714ac5d7-2ce4-45d7-bc22-9a935e3e9336/scratchpad/theme/theme/`

| Route | Theme page |
|---|---|
| `/cart` | `cart.html` — lines, steppers, coupon box, order summary |
| `/cart` (empty) | `cart-empty.html` — the empty state |

Extract `OrderSummary` and `CouponInput` as shared components — **WP5 (checkout) will reuse both**, so design their inputs for that now rather than making WP5 refactor them.

Reuse WP2's `QuantityStepper` and `PriceDisplay`. **Fix in the shared layer:** if the stepper needs a change for the cart, change the shared stepper — do not fork it.

## Contract facts

| Fact | Consequence |
|---|---|
| Cart endpoints: `GET /cart`, `POST /cart/items`, `PATCH|PUT /cart/items/{id}`, `DELETE /cart/items/{id}`, `DELETE /cart` (clear), plus coupon apply/remove — **verify the exact verbs and paths in `Modules/WebStore/routes/customer.php` yourself; do not trust this row** | — |
| **A8** money: decimals arrive as strings, computed as numbers — WP0 normalizes at the HTTP boundary. `CART_MONEY_KEYS`, `CART_ITEM_MONEY_KEYS`, `GUEST_CART_ITEM_MONEY_KEYS` already exist there | **never `parseFloat` in a component**; if a money field is missing, add it to those lists |
| **A10** three different 422 shapes: `{message, errors:{field:[...]}}` · `{valid:false, errors:[...]}` (flat array) · `{message, ...}` | the error interceptor already branches on `Array.isArray(body.errors)` — coupon errors in particular may use the flat-array shape |
| **A9 (corrected)** the API is *mostly* null-free, but **offer `custom_price` is genuinely null** and the nested `category` is a raw model | tolerate null in exactly those places |
| **A7** no logout endpoint — logout drops the token client-side | on logout, decide what happens to the cart and document it |
| Coupons: `StoreCouponController::index` has a **known cross-company leak** (backend bug, deferred) | do not build anything that depends on that endpoint being correct; validate coupons through the apply endpoint |

## Also fix here (small, pre-existing, in the shell you already touch)

**WP1's 9px horizontal overflow on mobile** — `layout/top-bar.component.html:37`: the language dropdown uses `invisible` (`visibility:hidden`) on an `absolute` element, which still occupies layout, giving `scrollWidth 399` vs `clientWidth 390` on a 390px viewport across every page. Fix it (e.g. `pointer-events-none` plus a `hidden` / `group-hover:block` swap, or clip the container) and **verify by measuring `scrollWidth === clientWidth` at 390px**, not by eye.

## Acceptance criteria

- [ ] `npx ng build storefront` green **and** `npx ng build` (admin) still green. Nothing under `src/` or in `angular.json`'s `moon-erp` block touched.
- [ ] Add-to-cart works from the product card **and** the product page; both header badges update live; the badge disappears at 0.
- [ ] Guest cart survives a full page reload.
- [ ] Cart page renders lines, quantity changes, line removal, clear-cart, and the empty state — verified against the **live** API with real seeded products.
- [ ] **Merge-on-login replay verified end to end** with a real token: guest adds N items → token injected → replay runs → server cart contains them → local cart cleared. Paste the observed before/after.
- [ ] A replay that fails on one item keeps the others and surfaces a translated message.
- [ ] Coupon apply + remove exercised against the real API. If no coupon exists in the seed, **say so** and state what you could and could not verify — do not fabricate a passing result.
- [ ] Out-of-stock product (`DEMO-P012`) cannot be added.
- [ ] Mobile overflow fixed: `scrollWidth === clientWidth` at 390px on the cart page and at least two other pages — paste the measurements.
- [ ] Zero hardcoded user-facing strings — grep your diff and paste the result. `I18N_VERSION` bumped.

## Out of scope

No checkout, address selection, shipping calculation, or order placement (WP5). No auth screens or guards (WP4) — you only *consume* the token. No account pages, wishlist, prescriptions, loyalty. Do not touch the backend or the admin app.

⛔ **Do not push, deploy, run `/fullpush`, or merge to `main`.** The orchestrator handles the preview deploy.

## Commit

Conventional commit on `hazemdev4`. This is a user-visible capability → add **one bilingual bullet** under `## [Unreleased]` in `/home/moonui4/moon-erp-be/docs/moonstack/CHANGELOG.md` (`- **Headline.** English {{ar}} **العنوان.** عربي`), then verify the file is still well-formed.

## Report back

Report what you **verified with evidence** versus what you assumed, and flag anything in this brief that contradicts what you find — **every WP so far has found real errors in its own brief, including facts I asserted confidently.** Treat the tables above as claims to check, not truth.
