# WP5 — [FIN] fix keeper-approve owner-drop (single-source-of-truth) + gated test

## Problem (WP3 review HIGH, latent)
When `production.material_issue_requires_approval` is ON, a production material issue is HELD as a draft, and on keeper approval `ApplyMaterialIssueOnApproval` rebuilds the resolved lines from the approved items and calls `IssueMaterials::applyEffects`. Today `applyEffects` reads the **warehouse from the persisted `$item`** (survives the keeper path) but reads the **owner from the rebuilt `$line['owner_partner_id']`** (which the listener does NOT populate). So a held consignor-draft line (item row stamped `owner_partner_id = X`) is applied on approval with `owner=null` → the auto-split runs → the (normally supply_source='own') material becomes an OWN leg → **WIP leak + drawn from company lots + no consignment-ledger bump**. Currently unreachable (the controller strips the per-line fields), but it MUST be fixed before WP4/WP6 expose them.

## The fix (reviewer-recommended — single source of truth)
Make `applyEffects` read `owner_partner_id` from the PERSISTED `$item`, exactly like it already reads warehouse — so BOTH the inline path AND the keeper-approve path use the same authoritative value, and the listener rebuild does NOT need to carry owner.

## Exact changes
- `Modules/Production/app/Actions/IssueMaterials.php`:
  - In `applyEffects` (~:321,:331) — where each item is processed, derive `$lineOwner = $item->owner_partner_id ?? null` from the persisted `inventory_issue_items` row (WP1 column; WP3 stamps it in createDraftInventoryIssue). Pass `$lineOwner` into `ownershipLegs(...)` INSTEAD of `$line['owner_partner_id'] ?? null`. (Warehouse is already read from `$item` at :321 — mirror that.) This makes the inline path (where $item.owner_partner_id === the just-stamped value) and the keeper path (where $item is the persisted draft row) both correct.
  - Verify the inline path still stamps + reads consistently (createDraftInventoryIssue writes owner_partner_id; applyEffects reads it back from the reloaded items) — the OFF-path (owner NULL) stays byte-for-byte.
- `Modules/Production/app/Listeners/ApplyMaterialIssueOnApproval.php` (~:94-104): OPTIONAL/defensive — since applyEffects now reads owner from $item, the listener rebuild does not strictly need owner. But for robustness, if the rebuilt `$line` is used anywhere else, also carry `owner_partner_id` (and confirm `warehouse_id`) from the item into the rebuilt line. Keep minimal; the primary fix is the applyEffects read-from-item.
- **Held payload echo (for re-issue after over-issue approval):** in `IssueMaterials` the held-materials payload (~:618-629 from ISS-9105) — echo `warehouse_id` + `owner_partner_id` on each held element so the FE (WP6) can re-issue a held line with identical routing. Additive.

## Acceptance
- **Gated keeper consignor invariant (the WP3-HIGH):** with `material_issue_requires_approval` ON, issue a consignor line (owner_partner_id=X) → held draft → keeper approves → the CUSTOMER leg is applied: X's off-book lots drawn, X's ConsignmentMaterialLedger bumped, NO WIP for that line, own sibling still WIP. (Before this fix it would wrongly become an OWN/WIP leg.)
- OFF-path (no override) keeper-approve unchanged (own/toll auto-split as today).
- Inline path (gate OFF) unchanged (WP3 behavior preserved).
- Held payload now carries warehouse_id + owner_partner_id.

## Tests (Pest, sqlite) — extend PerLineConsignorIssueTest / ProductionOverIssueApprovalTest area
- NEW (critical): gated keeper-approve of a consignor line → CUSTOMER leg on approval (X lots + ledger + no WIP). This is the invariant that fails without the fix.
- Keep the inline PerLineConsignor tests + the OFF-path keeper tests green.
- Ignore pre-existing ConsignmentFoundationTest settle-buy red.

## Verify before reporting
- Run PerLineConsignorIssueTest + ProductionOverIssueApprovalTest + ProductionMaterialOwnershipTest + your new gated test → green (baseline 70 + new).
- `php -l` + `./vendor/bin/pint` clean.

## Flags
- **[FIN]** — keeper-path WIP/ledger correctness. Architect re-consult on the diff. migration: none. Conventional commit `fix(production): keeper-approved issue honors per-line consignor (ISS-2026-9166)` on hazemdev2. No FE, no main, no deploy. chown if root.

## Out of scope
- Controller validation / availability pre-check (WP4 — runs AFTER this). FE (WP6).
