# WP1 — Grid and search must carry the product's unit list

**Repo:** BE · **Branch:** `hazemdev2` · **Migration:** no · **[FIN]:** no

## Goal

A product reached at the till by **scanning a unit barcode** arrives with its full unit list. The same product reached by **the grid or the search box** arrives with none. Close that asymmetry — it is the whole of the owner's «سواء لها باركود أو ليس لها».

## The defect (verified, not inferred)

`Modules/POS/app/Http/Controllers/POSProductController.php`:

| Line | Method | Eager loads |
|---|---|---|
| **43** | `index()` | `['category', 'baseUnit']` ← **missing `productUnits.unit`** |
| **88** | `search()` | `['category', 'baseUnit']` ← **missing `productUnits.unit`** |
| 141 | barcode path | `['category', 'baseUnit', 'productUnits.unit']` ✅ |
| 163 | barcode path | `['category', 'baseUnit', 'productUnits.unit']` ✅ |

`Modules/POS/app/Http/Resources/POSProductResource.php:38` already emits `product_units` via `whenLoaded('productUnits', …)`. **So the resource needs no change** — it simply never receives the relation on two of the four paths.

## What to do

Add `productUnits.unit` to the eager-load on `index()` and `search()`, matching the barcode paths exactly.

Then look at the cost, because these two endpoints are the till's hot path:
- `index()` is paginated (page size capped ~50/100) and `search()` ~20/50 — an eager-load is one extra query per page, not N+1. Confirm that with the actual code rather than assuming.
- If you find the payload grows enough to matter on a slow counter connection, **say so with numbers** rather than silently trimming fields. Do not invent a `?with_units=` flag — an inconsistent payload between paths is exactly the bug being fixed.

## Interfaces this WP exposes to WP2

`GET /pos/products` and `GET /pos/products/search` return, per product, the same `product_units` array the barcode path already returns:

```
product_units: [ { id, unit_id, name, abbreviation, conversion_factor, barcode, sale_price, is_sale, is_purchase } ]
```
(Read `POSProductResource.php:38` and document the **exact** shape it emits in your report — WP2 binds to it, and a guessed field name is a silent failure.)

`base_unit` stays exactly as it is today. No field is renamed or removed.

## Acceptance criteria

1. `GET /pos/products` returns `product_units` for a product that has alternative units — asserted from a real HTTP call in a test, not from the query builder.
2. `GET /pos/products/search?q=…` does the same.
3. A product with **no** alternative units returns `product_units` as an empty array (or the same shape the barcode path returns for that case — match it, do not invent a different one).
4. The barcode paths are unchanged.
5. `base_unit`, `stock`, `sale_price`, `min_sale_price` and every other existing key are byte-for-byte unchanged — assert the full structure, so a future field drop is caught.
6. No N+1: assert the query count for a page of N products does not scale with N (use `DB::listen` or Laravel's query log in the test).

## Tests

Extend `Modules/POS/tests/Feature/` — find the existing POS product API test and add to it rather than creating a parallel file; if none exists, create `POSProductUnitsPayloadTest.php`.

⚠️ **Pest loads every test file into ONE process** — prefix every top-level helper with its file's subject. A generic name is a fatal redeclare that kills the whole suite: exit 255, zero output. **Five occurrences in this project**, the most recent caused by a merge bringing two same-named helpers together.

## Baseline — green = zero NEW failures

`pest Modules/POS` → the number recorded in `LEDGER.md` (Phase A). Do not fix anything outside this WP.

## Environment

- BE `/home/moonui2/moon-erp-be`, branch `hazemdev2`. **moonui2 ONLY — never touch `/home/moonui`.**
- Tests ONLY with `/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='…'` — the default `php` is php-cgi and dies with "Undefined constant STDOUT".
- `./vendor/bin/pint` on touched files only. `chown moonui2:moonui2` after every edit.
- Commit on `hazemdev2` with a conventional `fix(pos): …` message. **Do not push, do not merge, do not deploy.**
- There is **one unpushed commit per repo** from a previous package (D1). Do not touch it, do not amend it, do not reset.
- ⛔ No migrations. Never `migrate:fresh`/`refresh`/`db:wipe` against `moonui2_dev_be`.

## Out of scope

- Any frontend change — that is WP2.
- The offline cache — that is WP3.
- Changing `POSProductResource`'s shape, adding fields, or introducing a query flag.
