# RESUME — POS unit-of-sale selector

**Status: ✅ COMPLETE (2026-08-03).** All 3 WPs done, built, **deployed to `/app` as `main-V3VKJUPJ.js`**. Committed on `hazemdev2`, **not pushed** — `/fullpush` is the owner's step.

## What the owner reported
«في نقاط البيع عند اختيار منتج بباركود أو بحث عنده أكتر من وحدة قياس لا يظهر إمكانية اختيار الوحدة — سواء لها باركود أو ليس لها.»

## What it actually was — three separate defects, not one

1. **The backend was inconsistent between paths.** Scanning a unit barcode sent the product's unit list; the grid and the search box did not (`POSProductController::index()`/`search()` eager-loaded `['category','baseUnit']` only, while the barcode paths also loaded `productUnits.unit`). That asymmetry *is* the owner's «سواء لها باركود أو لأ».
2. **No unit control existed in any POS template** — `grep` returned zero hits.
3. **The line printed the base unit even on a carton line** (`invoice-table.component.html:92`), so even the path that worked displayed the wrong thing.

Plus two more found while building:

4. 🔴 **The search box called a different endpoint entirely.** `PosProductService.searchProducts()` hit `GET /core/products?search=` and rebuilt each row through a hand-written whitelist that **dropped `product_units`** — so WP1's fix was landing on an endpoint the screen never called. Without WP2 exercising it in a browser, this plan would have shipped "fixed" and the owner would have seen no change. Switched to `GET /pos/products/search?q=`. Side benefit: `/core/products` sits behind `core.products.view`, which a cashier has no reason to hold.
5. **Units displayed their full name, never their symbol.** `POSProductResource` read `$unit->abbreviation`; the `units` table has **`symbol`** and no such column, so every unit in POS returned `abbreviation: null` and the screen fell back to the full name. Masked by the FE's own `|| name` fallback. Fixed in `1632b3bd4`.

## Commits (all on `hazemdev2`, unpushed)

| WP | Commit | What |
|---|---|---|
| WP1 | `afcc5679a` | eager-load `productUnits.unit` on `index()` + `search()`; 7 tests |
| — | `1632b3bd4` | orchestrator: resource reads `symbol`, not the non-existent `abbreviation` |
| WP2 | `b9b8c513f` | `setUnit()` + the unit control in the edit bar + the line-unit fix + the search-endpoint switch |
| WP3 | `cc7f414ad` | per-unit barcodes resolve offline |
| — | `e6c1087b4` | bilingual CHANGELOG bullet |

## Verification

- `pest Modules/POS` **243 passed / 0 failed** (baseline 236/0 — exactly +7, zero new failures).
- **12 headless-Chromium specs** with a full API double capturing every request: WP2 ×5, WP3 ×7.
- **Red-in-isolation proved for three separate defects**: reverting the one template line reproduces «tab» on a box line; disabling the merge branch leaves 2 rows; reverting `getByBarcodeCached()` turns exactly C1/C4/C6 red.
- **No N+1** — 6 queries for a page of 2 and 6 for a page of 12, measured on grid and search.
- **No new payload key** — `POST /pos/sales` uses `RejectsUnknownKeys`; verified on the captured wire.

## Behaviour now

A product with more than one sellable unit shows a **unit picker in the line's edit bar**. Price follows the choice: the unit's own `sale_price` when set, otherwise base × `conversion_factor`. The line shows **its own** unit with the short symbol. Purchase-only units are filtered out; the base unit is synthesised (it is not in `product_units`). Choosing a unit another line already uses **merges the lines**. Single-unit products are byte-for-byte unchanged. Unit barcodes resolve offline with no cache schema change.

## Next

- Owner runs `/fullpush`, then tests on `/app`.
- Open decision: should the picker be per-terminal (`pos.allow_unit_switch`)? Shipped ungated.
- See LEDGER "Deferrals" — chiefly that **the till cannot start offline at all** (no service worker; the shell bounces to Login without `/auth/me`). That is a real gap, far bigger than this plan.
