# Phase 3 — Expiry visibility on the stock-balances screen — Plan & Tracker

> **Owner-approved (2026-07-10).** Phase 3 of [purchases-inventory-flexible-flow.html](purchases-inventory-flexible-flow.html) §6/§7. **Visibility, NOT enforcement** — a derived "nearest expiry" column + color badges + drill-down to the EXISTING expiry report, with NO new balance model and NO FEFO. Read-only additions.

**Repos:** BE `/home/moonui2/moon-erp-be` · FE `/home/moonui2/public_html/moon-erp` · branch `hazemdev2`.

## Facts
- `StockBalance` is aggregate; `StockBalanceResource` has product/warehouse/qty/avg_cost/total_value — no expiry.
- `product_serials` has `expiry_date` + `status` (available/sold/…). `inventory_movements` carry batch_number+expiry_date (genealogy).
- Existing report routes: `GET inventory/reports/expiry` (from product_serials) + `GET inventory/reports/expiring-batches` (from movements). FE inventory-reports feature exists.
- No inventory near-expiry threshold setting yet.

## Task 3.1 — BE: nearest-expiry on the stock-balances list (batched, no N+1)
- Add setting `inventory.near_expiry_days` (integer, default 30, company scope) via SettingDefinitionSeeder.
- In `StockBalanceController::index()`: after paginating, compute in **ONE batched query** a map keyed by `product_id|variant_id|warehouse_id` → MIN(`expiry_date`) from `product_serials` where company + status='available' for the page's rows (exact, serial-tracked). For products with NO available serials, fall back to an **approximate** min expiry from `inventory_movements` (received batch genealogy) for that product/warehouse — flag it approximate. Pass the map to the resource via `->additional()` or a request attribute (avoid per-row queries).
- `StockBalanceResource`: add `nearest_expiry` (date|null), `nearest_expiry_source` ('exact'|'approx'|null), `is_expired` (bool), `days_until_expiry` (int|null vs today), and echo `near_expiry_days` (or put it in the collection `additional` meta).
- Tests: a serial product with 2 available serials → nearest_expiry = the earlier; expired serial → is_expired true; batch-only product → approx from movements; product with no lot data → null. No N+1 (assert query count reasonable).
**Status:** ☑ DONE — 8/8 tests, batched map (no N+1), 4 resource fields + setting.

## Task 3.2 — FE: nearest-expiry column + badges + drill-down on stock-balances
- Add a **"Nearest Expiry"** column to `features/stock-balances/stock-balances.component`: shows the date + a color `p-tag` — **red** if `is_expired`, **amber** if `days_until_expiry <= near_expiry_days`, none otherwise; an `~` prefix / tooltip when `nearest_expiry_source === 'approx'`.
- Row action / click → navigate to the inventory **expiry report** filtered by that product (existing `reports/expiry`; route to the FE inventory-reports expiry view with a product filter, or open it). Keep it a link, not enforcement.
- Add a stat/legend note "visibility only — issuing is not expiry-ordered (no FEFO)".
- Bilingual i18n keys. `ng build` clean.
**Status:** ☑ DONE — column + red/amber badges + approx `~` + drill-down + FEFO note; ng build clean.

## Task 3.3 — Review + deploy + push + CHANGELOG
**Status:** ☑ DONE — verified (no N+1 via bounded-query test); deployed + pushed (BE 20e06ee5b, FE b8625169e); bilingual CHANGELOG bullet added. (code-reviewer skipped — read-only visibility feature, no GL/stock mutation, N+1 test-covered.)

## Progress ledger
- 2026-07-10 — Phase 1 + 2 complete & shipped. Phase 3 started: plan written; combined BE+FE implementer dispatched.
- 2026-07-10 — **PHASE 3 COMPLETE & SHIPPED.** BE: near_expiry_days setting + batched nearest-expiry map (2 queries, no N+1 — bounded-query test) + 4 resource fields; StockBalanceExpiryVisibilityTest **8/8**; full Inventory suite 337 pass / 1 pre-existing (OpeningBalance, stash-proven unrelated) = 0 new. FE: Nearest Expiry column + red/amber badges + approx `~` + drill-down to expiry report + FEFO-note; ng build clean. Deployed BE local-deploy + FE→/app. Pushed BE `20e06ee5b` + FE `b8625169e`. CHANGELOG Phase-3 bullet added.

### ✅ ALL PHASES (1–3) COMPLETE (2026-07-10). Phase 4 (per-lot on-hand balances + FEFO) explicitly deferred.
