# WP5 — Checkout **[FIN]** (addresses · calculate · place order · success · tracking)

**Repo:** FE only — `/home/moonui4/public_html/moon-erp` (branch `hazemdev4`)
**Depends on:** WP0–WP4 ✅ · WPS ✅ · WPS2 ✅ (`11e3ec2e8`, `15b4e3cf4`)
**Review:** **Codex** + an architecture/advisor pass · **[FIN]: YES** · **Migration:** no

## Goal

Close the loop: a logged-in customer with a cart picks an address, sees real shipping and a real total, places a **COD** order, and can then see and track it. When this WP is done the owner's acceptance test is satisfied — *a real order placed from the storefront appears in the ERP admin.*

**This is the only [FIN] package in the plan.** It moves money and creates real records. Correctness beats speed everywhere it conflicts. If something is ambiguous, stop and say so rather than guessing.

## Endpoints (verified in `Modules/WebStore/routes/customer.php`)

| Endpoint | Use |
|---|---|
| `apiResource addresses` | list / create / update / delete customer addresses |
| `GET /governorates`, `GET /cities` (public) | the cascade — **unpaginated** `{data:[...]}` |
| `POST /checkout/validate` | pre-flight check |
| `POST /checkout/calculate` | **the only server source of totals** |
| `POST /checkout/place-order` | creates the order |
| `GET /orders`, `GET /orders/{id}`, `GET /orders/{id}/track`, `POST /orders/{id}/cancel` | post-purchase |

## Money rules — non-negotiable

1. **Every figure the customer sees comes from `checkout/calculate`.** Never compute or "fix up" a total client-side. WP3 already routes the cart's totals through it; reuse that path, do not fork it.
2. **Re-calculate whenever an input changes** — address, coupon, quantity. A stale total shown next to a changed cart is a lie.
3. **The total shown at confirmation must be the total sent to `place-order`.** If the server's placed-order total differs from what was displayed, that is a **hard error the user must see**, not something to silently accept.
4. Money arrives as strings for decimals and numbers for computed fields; WP0 normalizes at the HTTP boundary. **Never `parseFloat` in a component.** Add missing keys to the normalize lists, not locally.
5. `tax_amount` is hard-coded **0** server-side. Do not invent a tax line.

## Seeded reality (verified live 2026-07-19)

- **Payment methods: exactly one — COD** (`id:1`, `type:"cod"`, «الدفع عند الاستلام»). Owner decision **D4**: show COD as the only selectable option and render card/online/wallet as **disabled «قريبًا»**. Do **not** offer a method the backend cannot process.
- **23 cities across 10 governorates, `shipping_cost` 30.000–65.000 EGP, none at 0.** So shipping is genuinely exercised — if you ever see 0, that is a bug or a null `city_id`, not "free shipping".
- Order statuses: 6 · payment statuses: 4 (company 1).
- **`OPEN-4`: the theme offers a paid «توصيل سريع +25 ج.م» option. The backend has NO express-shipping concept** — shipping is flat per city. **Hide it.** Same honesty rule as D4.

## Contract facts

| Fact | Consequence |
|---|---|
| **A5** `place-order` takes **`payment_method_id` (FK)**, not a string | send the id from `/payment-methods` |
| **A12** shipping = the selected address's `city.shipping_cost`, flat; `shipping_cost` is **0 when no `address_id`** is passed to `calculate` | never show a total before an address is chosen without labelling shipping as pending |
| **WPS2** foreign/soft-deleted `address_id` or `payment_method_id` now return **422** instead of being silently accepted | handle that 422 as a real, translated error |
| **A10** three 422 shapes; checkout uses the **flat-array** form `{valid:false, errors:[...]}` | the interceptor branches on `Array.isArray` — verify checkout errors surface readably |
| Order/payment **statuses are per-company DB rows** — read `status.name`/`status.color` from the order | ⛔ **never hard-code status names or colours in the UI** |
| **A7** no logout endpoint; **A6** no guest cart | checkout is auth-only — use WP4's guard with `returnUrl` |

## Pages

Theme (read-only): `/tmp/claude-0/-home-moonui4-public-html/714ac5d7-2ce4-45d7-bc22-9a935e3e9336/scratchpad/theme/theme/`
`checkout.html` · `order-success.html` · `order-tracking.html` · `account-addresses.html` + `account-address-add.html` (address CRUD only — the rest of the account area stays out of scope)

Reuse WP3's `OrderSummary` and `CouponInput` — they were built for this. **Fix in the shared layer** if they need changes.

## Address cascade

`governorate → city`, both from the unpaginated public endpoints. Changing the governorate must reset the city (a stale city from another governorate would produce a wrong shipping cost). Changing the city must trigger a re-calculate.

## Acceptance criteria

- [ ] `npx ng build storefront` green **and** `npx ng build` (admin) still green; nothing under `src/` or `angular.json`'s `moon-erp` block touched.
- [ ] **A real order is placed end to end against the live API** and appears in the ERP admin. Paste the order id/number and what you observed.
- [ ] Shipping shown equals the selected city's `shipping_cost` — prove it with **two different cities** at different costs.
- [ ] The displayed total equals the placed order's total. Show both numbers.
- [ ] Coupon apply/remove re-calculates and the discount is the server's figure.
- [ ] COD is the only selectable payment; the others render disabled as «قريبًا»; express shipping is absent.
- [ ] Address create/edit/delete works; the governorate→city cascade resets correctly.
- [ ] Order success + tracking render **server-provided** status names/colours, with nothing hard-coded.
- [ ] A 422 from checkout (e.g. an out-of-stock line) surfaces as readable translated text, not a raw dump or a silent failure.
- [ ] Zero hardcoded user-facing strings — grep your diff and paste it. `I18N_VERSION` bumped.

## Known live hazards (do NOT work around them, do NOT fix them)

These are **backend tickets already recorded**; the owner chose to proceed. Do not paper over them in the UI, and do not silently rely on them being fixed:

- `CartService::addItem` fetches products **unscoped** — cross-company product injection is possible at the API level.
- `ShippingCalculator` resolves the city **unscoped**, and a null/missing city yields **0 shipping (fail-open, i.e. free delivery)**.
- The backend does **not** block adding an out-of-stock product to a cart — the client is the only gate.
- `StoreCouponValidateController` leaks foreign coupon validity publicly.

If any of these produces behaviour you cannot handle honestly in the UI, **stop and report** rather than inventing a client-side workaround.

## Out of scope

No wishlist, prescriptions, loyalty, notifications, CMS pages, order rating/review, or the rest of the account area beyond address CRUD. Do not touch the backend, `src/`, or `angular.json`'s `moon-erp` block.

⛔ **Do not push, deploy, run `/fullpush`, or merge to `main`.**

## Commit

Conventional commit on `hazemdev4` + **one bilingual bullet** under `## [Unreleased]` in `/home/moonui4/moon-erp-be/docs/moonstack/CHANGELOG.md`, then verify the file is well-formed. **Be honest in the bullet** — COD only; do not imply card/online payment exists.

## Report back

This is [FIN]. Report **what you verified with pasted evidence** versus what you assumed, and be explicit about anything involving a number the customer would pay. Flag every contradiction you find with this brief — **every WP so far has found real errors in its own brief, and the last one had a factually wrong claim about a seeder.**
