# Phase 4 execution tracker (per-lot balances × ownership + FEFO)

## ✅✅ PHASE 4 COMPLETE (2026-07-11) — all sub-phases 4.0→4.5 shipped to hazemdev2 + deployed, code-reviewed + Fable-UX-reviewed. NOT merged to main.
Backlog follow-ups (low priority, documented below): P3 Fable polish #2/#3/#4 · per-owner count variance · Stage/Backflush WIP-staging lot-awareness · borrow-aging full FE threading.


> Durable progress ledger for the autonomous multi-sub-phase build. Design/architecture: [phase4-per-lot-fefo.html](phase4-per-lot-fefo.html). Branch **hazemdev2** (push periodically; **do NOT merge to main** — the release merge was a one-time owner-requested action). Consult Fable on fateful decisions / deep analysis. code-reviewer after each substantial chunk. Owner is away — organized autonomous execution.

Repos: BE `/home/moonui2/moon-erp-be` · FE `/home/moonui2/public_html/moon-erp`. Root artisan → `chown -R moonui2:moonui2 storage bootstrap/cache` after.

## Sub-phase status
- **4.0 — ledger + seed + read view + backfill**  ✅ DONE (BE `631904537`, FE `58eb5555e`, deployed). Code-review CRITICAL (backfill vs live collision) + 2 IMPORTANT (upsert race, unassigned clamp) + MINOR (variant_id) all fixed. 6 tests green. Reconciliation verified on real data (17159: 101==101, only the consumption-touched lot flagged estimated). CHANGELOG bullet added. Consignment live-hook + tracking_type guard deferred (see below).
  - [x] migration `inventory_lot_balances` (lot × owner + on_book) — `2026_07_11_400000`
  - [x] model `InventoryLotBalance` + ownership scopes
  - [x] `LotBalanceService.seedFromReceipt` + `SeedLotBalancesOnReceiptApproved` listener (registered) — **committed `a34e6eb7d`**, 3 tests green
  - [x] `addConsignmentLot` (off-book owner-tagged)
  - [x] Backfill command `inventory:backfill-lot-balances` (company retroactive-FEFO reconcile to on-hand, `is_estimated`) — **committed `cf4e0e78d`**, verified on real data (product 17159/wh13: 4 lots sum 101 == stock 101, reconciled). Consignment backfill N/A on this host (consignment_material_ledgers=0, MfgBatch has no owner col) → consignment lots seed live via CRN hook in 4.4.
  - [x] Read endpoint `GET stock-balances/product/{id}/lots?warehouse_id=` (owner-grouped + reconciliation block) — committed `cf4e0e78d`, verified.
  - [~] FE Surface 1 — **dispatched to Sonnet subagent** (agent a7dba0f16a3720a3d): per-lot expansion, owner groups, reconciliation footer, Unassigned row, violet 🤝.
  - [~] BE code-review — **dispatched** (agent afe8a3e5d55449533): reconciliation invariant, FEFO backfill, double-count, variant-aggregation mismatch, concurrency.
  - [ ] Live consignment hook (`CreateConsignmentReceipt` → `addConsignmentLot`) — moved to 4.4 (no legacy consignment data here)
  - [ ] CHANGELOG bullet + deploy FE + push (after FE + review land)
  - **DEFERRED fast-follow:** `tracking_type`-edit guard (Core UpdateProductRequest — block when InventoryMovement history exists; Phase 3.5 MINOR finding, edge-case; needs core lang keys).
- **4.1 — lot-aware issuing (auto-FEFO + reverse)** ✅ CORE DONE (BE `f2251b07f`, deployed). `inventory_issue_lot_allocations` + `LotAllocationService` (FEFO within owner, manual-validated, shortfall→unassigned, exact reverse); threaded into `decreaseStock` opt-in; `ApproveIssue` opts in for batch products (company owner); `CancelIssue` reverses. Bilingual CHANGELOG bullet + stale-3.5-note amended. 5 allocation + 67 issue/stock tests green. **Review dispatched** (agent a35d1759e5ad82589).
  - **Manual-picker DIALOG → moved to 4.2:** Fable designed ONE allocation dialog reused for company-FEFO + owner-scoped consignment; building it once in 4.2 (with owner awareness) is cleaner + it needs a new `lot_allocations` persistence path on `inventory_issue_items` (ApproveIssue already reads `item->lot_allocations`). Auto-FEFO already covers the common case with zero UI.
- **4.1 review** ✅ CLEAN (0 CRITICAL — reconciliation/cancel/partial/serial all confirmed correct). 2 IMPORTANT (both manual-branch): variant-scope fixed in pickManual + forward-hook comment clarified. Committed `dcc10af51`. 6 allocation tests green.
- **4.2 — sales/POS owner=own + toll owner=customer + borrow/Due-back + manual dialog** ⬜  Break into testable pieces:
  - **4.2a** ✅ DONE (Sonnet-built, reviewed-by-me, committed `253aad743`, deployed). `ConfirmDeliveryNote` + `PostSalesInvoice` (POS routes through it) + `PostPurchaseReturn` opt into FEFO allocation (owner=0); cancels reverse with matching reference_type (delivery_note/sales_invoice/purchase_return). E2E delivery-note test + delivery/invoice/cogs/return suites green. NOTE: **production consumption** (`StageMaterialsBulk`, `BackflushFromStaging`, borrow settle) deferred to 4.2b (they carry owner/consignment nuance). Transfer lot pass-through = 4.3.
  - **4.2b** (complex, Opus) 🟡 STARTED:
    - [x] consignment live-hook `CreateConsignmentReceipt` → `addConsignmentLot` (owner=customer, off-book) — **committed `f41b9e91a`**, deployed, 1 test. Consignment goods now seed customer-owned lots (surface in the 4.0 violet 🤝 owner group).
    - [x] toll consumption: `IssueMaterials` per-leg owner-scoped allocation (customer legs → customer lots) + `recordIssue`/`reverseIssue` wired (**closes the ledger-drift bug**). ApproveIssue skips auto-alloc for production issues. **Committed `fadab75ea`**, deployed, 3 tests + regression green.
    - [x] borrow lot-awareness (`RecordBorrow` draws customer lots, `is_borrow=true`) + [x] consignment RETURN action (`ReturnConsignmentMaterial` + `ConsignmentService::recordReturn`) — **committed `89e900441`**, deployed, 4 tests. **4.2b COMPLETE.** (Due-back UI surface + HTTP route for return = 4.4 FE.)
    - DEFERRED to backlog: `StageMaterialsBulk`/`BackflushFromStaging` WIP-staging lot-awareness (analogous customer-lot gap on the staging path; noted by the toll agent — lower frequency).
  - **4.2c** (Opus FE): the allocation dialog (company FEFO preview + manual) on stock-issues — needs a `lot_allocations` JSON persistence path on `inventory_issue_items` (ApproveIssue already reads `item->lot_allocations`). Reused for owner-scoped consignment issues.
- **4.3 — transfer lot+owner pass-through + direct-bill** ✅ DONE (`2a7c97d15`, deployed). Ship records shipped lots → Receive recreates at dest (batch/expiry/owner) → Cancel reverses. Direct-bill = no gap (draft→keeper approval seeds lots). 5 tests + regression green.
- **4.4-BE** ✅ DONE (`897d8f9eb`, deployed). Valuation `on_book` barrier (owned vs consignment memo — LEAK FIXED) + expiring-lots owner column/filter + consignment-return HTTP route + permission (both seeders). 7 tests + regression green. (Per-owner count variance → deferred to 4.4-BE-2 / backlog.)
- **Fable 4.4-FE build spec** ✅ `plans/phase4.4-fe-build-spec.md` (ship order P1→P5, contracts pinned, i18n table). Owner-flagged §10 decisions: adopting Fable's recommended defaults (memo on exports=yes · expired-customer CTA=link-ceremony · borrow aging=14d · add consignment nav entry).
- **4.4-FE**: P1+P2 ✅ built+deployed+pushed (`bbeb9edb0`) — valuation owned-hero + consignment memo · expiring-lots owner-lens + split KPIs. **Fable UX review 🔄** (a7b55fc6d41853ba8). Issue-item `lot_allocations` persistence ✅ (`2f4645b8a`). **P3 allocation dialog 🔄** (a0e272ead2468c784, Sonnet). Remaining: P4 due-back tray · P5 CRT return dialog.
  - P1+P2 ✅ built+Fable-reviewed+fixed (`bbeb9edb0` + `7657e6a3e`). P3 allocation dialog ✅ (`1a22ae30c`) — **Fable review 🔄** (a7b55fc6→done for P1P2; a7e9c95be1d671ea6 for P3). Permission `inventory.issues.expired-override` registered (`626d8a118`, register-only on update — elevated). **P4 due-back + P5 CRT return dialog 🔄** (ae1e26b47b45ad344, Sonnet).
- **P4+P5 ✅** (`78432c8c8`, deployed): due-back badge + borrows age/reorder + consignment return dialog + print slip + nav entry. P3 Fable fix#1 (auto→emit[]) + «أرجِعها» route→`/factory/consignment` done.
- **P3 Fable polish (deferred follow-up, low):** #2 manual-chip tooltip (show batch×qty split) · #3 fetch-error state (don't show "no lots" on a failed request) · #4 expired-lot reopen: zeroing an expired take must not need override (only increasing does). Files: `stock-issues/lot-allocation-dialog/*` + `stock-issues.component.*`.
- **4.5** 🔄 (last phase) settings + memo reconcile. CONCRETE:
  - `inventory.issuing_order` (fefo default / fifo) → wire into `LotAllocationService::pickFefo` ordering (fefo=expiry asc; fifo=received_date asc). Add to `InventorySettingDefinitionSeeder`.
  - `inventory.expired_issue_policy` (block/warn/allow) → controls whether expired lots are auto-picked / blocked. `InventorySettingDefinitionSeeder`.
  - `production.consignment.borrow_aging_days` (default 14) → the due-back amber threshold (FE reads it; P4 currently hardcodes 14). `ProductionSettingDefinitionSeeder`.
  - Memo «فرق دفتر-فعلي» column on the customer consignment statement (ledger balance − Σ customer lot remaining) — early-warning.
  - **SEED DISCIPLINE:** every setting above → its module `*SettingDefinitionSeeder` (idempotent updateOrCreate, already in moonstack updater.seeders → auto-reaches clients).
- **4.5 — settings (issuing order, expired policy, borrow-aging) + memo «فرق دفتر-فعلي» reconcile column** ⬜ (Sonnet)

## 4.1 design (locked)
- Thread allocation into `StockService::decreaseStock` as an **opt-in** (`allocate_lots` bool + `owner_partner_id` default 0 + optional manual `lot_allocations` + `reference_line_id`). Only `ApproveIssue` opts in for 4.1; sales/POS/production opt in during 4.2 (single impl, minimal blast radius vs 15 callers).
- `LotAllocationService`: `allocate(...)` FEFO within owner (expiry asc nulls-last, received asc) — decrements `remaining_quantity`, writes `inventory_issue_lot_allocations`. If Σremaining < qty, the remainder is untracked "unassigned own" consumption (NOT blocked at BE; the FE dialog is where a true shortage blocks). `reverse(refType,refId)` restores remaining + deletes allocations (for CancelIssue). Manual allocations validated ≤ each lot's remaining.

## 🚚 RELEASE-READINESS (client seed/update) — verified 2026-07-11
MoonStack update runs `migrate --force` (tables) + idempotent **updater.seeders** in `config/moonstack.php` (Core `SettingDefinitionSeeder`, `InventorySettingDefinitionSeeder`, `ProductionSettingDefinitionSeeder`, `LabSettingDefinitionSeeder` — `updateOrCreate`; + `GrantRolePermissionsSeeder` for perms). So a release auto-delivers **tables + settings + permission grants** to existing clients. **DISCIPLINE (must-do for 4.4/4.5):** every NEW setting → add to its module `*SettingDefinitionSeeder`; every NEW permission → `RolePermissionSeeder` (fresh install) **and** `GrantRolePermissionsSeeder` (existing-client update). Lot DATA for existing stock: closed via data-migration `420000` (guarded backfill runs on update). So far 4.0–4.3 added **no new settings/permissions** (reused `inventory.near_expiry_days` + `inventory.stock.view`).

## 🎨 FABLE DISCIPLINE (owner-required, standing)
Consult **Fable** on every PIVOTAL/design-critical point («النقط المحورية») AND have Fable REVIEW UX deliverables — not just code-reviewer. Pivotal points remaining: valuation "owned vs consignment" presentation (4.4), the allocation dialog + owner-lens reports UI + Due-back tray (4.4-FE), settings UX (4.5). Code-reviewer verifies correctness; **Fable verifies the الشكل/UX**.

## Key decisions locked
- Dedicated lot ledger (NOT cost-layer extension — WAC never consumes layers). `stock_balances` stays owner-blind physical. owner_partner_id NOT NULL (0=company). Serial products derive lot view from `product_serials` (no duplicate rows).
- Consignment = lot with owner=customer, on_book=false; consignment_material_ledgers demoted to financial memo.

## Ledger
- 2026-07-11 — release merged to main (BE cb39d62 / FE 94561ef). 4.0 seed foundation committed a34e6eb7d + deployed. Continuing 4.0 backfill.
