# WP2 — The unit control on the cart line (+ fix the wrong unit display)

**Repo:** FE · **Branch:** `hazemdev2` · **Migration:** no · **Depends:** WP1

## Goal

The cashier picks the unit of sale on the line — قرص / شريط / علبة — and the price follows. Today there is **no unit control anywhere in POS**: `grep 'product_units|matched_unit|setUnit|unit_id' src/app/features/pos --include=*.html` returns **zero hits**.

## Two things to build and one bug to fix

### 1. `setUnit(index, unitId)` on `PosCartService`

`src/app/features/pos/services/pos-cart.service.ts` already has everything you need — **reuse it, do not write a second path**:

- `resolveUnitPrice(product, variant)` (~line 671) — the price precedence: the unit's own `sale_price`, else base × `conversion_factor`. The server's min-price floor scales by the same factor, so a second price path here would disagree with the server and 422 at the counter.
- The line-add merge key at ~304-309: `(product, unit_id)`. A carton line and a piece line are deliberately separate lines.

`setUnit()` must:
- resolve the new unit price through `resolveUnitPrice()`,
- re-run the line recalculation the rest of the service already uses (discount %, tax bracket, totals) — do not recompute totals inline,
- **re-evaluate the merge key**: if the target unit is already on another line of the same product, the two lines must **merge** (quantities add) rather than leave two lines the server would happily accept as two.

That merge case is the one a naive implementation gets wrong. Cover it with a test.

### 2. The control on the line

`src/app/features/pos/components/invoice-table/invoice-table.component.html`.

- Show it **only when the product has more than one sellable unit**. A single-unit product's line must look exactly as it does today — that is the regression guard.
- The obvious home is the **edit bar** (the row of per-line fields that appears when a line is selected — quantity, price, discount, tax). It already has ~5 fields across ~940px and room for one more. Read it first; if a dropdown on the line itself reads better in RTL at 1366×768, say why and do that instead.
- The list comes from `product.product_units` (WP1 now delivers it on grid/search too). Filter to units flagged sellable — read WP1's report for the exact field name it emits; **do not guess `is_sale` vs `is_sellable`**.

### 3. BUG — the line displays the wrong unit

`invoice-table.component.html:92-93`:

```html
@if (showUnit() && item.product?.base_unit; as unit) {
  <span class="product-unit">{{ unit.abbreviation || unit.name }}</span>
```

It renders the **base** unit. A line sold as a carton still shows «قرص». Show the **line's** unit (`item.unit_id` → the matching entry in `product.product_units`, falling back to `base_unit` when the line is in the base unit).

Keep honouring the existing `showUnit()` terminal setting — do not remove that gate.

## Interfaces consumed from WP1

`GET /pos/products` and `GET /pos/products/search` now return `product_units` per product, the same array the barcode path already returned. **Read WP1's report for the exact emitted shape** before binding to it.

## Acceptance criteria

1. A product with 3 units (قرص base / شريط ×10 / علبة ×100) added from **search** shows a unit control; changing it re-prices the line correctly for each (unit's own price when set, base × factor when not).
2. Same product added by **scanning a carton barcode** arrives already on the carton unit, and the control shows it selected.
3. **The line displays the unit actually sold**, not the base unit — verify on a carton line specifically.
4. Switching line A to a unit that line B already uses **merges the two lines**; quantities add; no duplicate line is sent.
5. A product with only a base unit shows **no** control and its line is byte-for-byte as today.
6. The payload still sends `unit_id` per line and nothing new — `POST /pos/sales` uses `RejectsUnknownKeys`, so an extra key 422s **every** sale. Read `StorePOSSaleRequest` and confirm.
7. The min-price floor still passes for a non-base unit (the server scales it by the factor — a carton at its carton price must post).
8. RTL correct at 1366×768; the controls column is already at zero spare vertical space, so the control must live in the invoice panel, not the left column.
9. `npx tsc --noEmit` clean, `npx ng build --base-href /app/` green.

## Verification

Prefer a real check over assertion: the repo has a headless-Chromium + API-double pattern under `e2e/` (see `e2e/specs/d1-drug-entry-preset.spec.ts` for the shape — it captures every outgoing request). Criteria 1–5 are worth verifying that way; if you cannot, say exactly what you verified by reading instead. **Do not claim behaviour you did not exercise.**

## Environment

- FE `/home/moonui2/public_html/moon-erp`, branch `hazemdev2`. **moonui2 ONLY — never `/home/moonui`.**
- `npx ng build --base-href /app/` and `npx tsc --noEmit` — pre-authorized.
- **Do NOT deploy** — the orchestrator deploys once after WP3. Do not merge, do not push.
- Commit on `hazemdev2` with a conventional `feat(pos): …` message. There is **one unpushed commit per repo** from a previous package (D1) — do not touch, amend or reset it.
- `chown moonui2:moonui2` after every edit.
- **Never** run `git checkout`, `git restore`, or `git stash` on `src/assets/i18n/ar.json` or `en.json` — they were destroyed that way once. Additive edits at matching positions in both; verify both parse as valid JSON.

## Out of scope

- Any backend change. WP1 delivered the payload; if it is insufficient, **report it and stop** rather than patching the BE.
- The offline cache — WP3.
- Batch/expiry, the drug tab, the products screen.
