# WP1 — [FIN] BE: partition the material issue (free lines now, over-plan materials held)

## Goal
Today `IssueMaterials::checkOverIssueAllowance()` throws ONE 422 (`over_issue_requires_approval`) aborting the WHOLE issue if ANY material's excess is uncovered — so a mixed issue (some materials within plan, one over) blocks the within-plan materials too. Change it to a **per-material partition**: issue the FREE materials now (draft to keeper / inline), HOLD only the over-plan materials with uncovered excess, and return the held materials to the caller (200, not 422) so the FE opens the over-issue REQUEST dialog for them. This satisfies D4 (within-plan issues immediately) while keeping D3 (an over-plan material is held entirely; its whole qty ships later in one draft on approval).

## Exact files
- `/home/moonui2/moon-erp-be/Modules/Production/app/Actions/IssueMaterials.php`
  - `execute()` (~87-160): the over-issue block (`:130-136`) and how the draft is built.
  - `checkOverIssueAllowance()` (~505-574): change from "collect shortfalls → throw 422" to "return a partition: which material_ids are HELD (uncovered over-plan) vs FREE".
  - `createDraftInventoryIssue()` (private, ~739-766): must build the draft from FREE resolved lines only.
- `/home/moonui2/moon-erp-be/Modules/Production/app/Http/Controllers/ProductionOrderController.php`
  - `issueMaterials()` (~653-690): response becomes 200 `{ issued: <IssueResource|null>, held: [ { production_order_material_id, material_name, planned, consumed, requested, excess, allowed_free } ] }` — REUSE today's `over_issue.materials` element shape so the FE change is minimal.

## Interfaces (consumed by WP2 + WP3)
- `execute($order, array $lines, int $userId, ?int $warehouseId = null): ?InventoryIssue` — unchanged signature; now: partitions lines when `$enforceOverIssue`; creates the draft from FREE lines only; if NO free lines → creates NO draft and returns null; the HELD materials are exposed to the controller (return a small value object / attach to a property the controller reads, OR return a richer result — pick the least-invasive; document the shape you choose here).
- Controller `issueMaterials` returns 200 with `issued` (the created draft resource or null) + `held` (array; empty when nothing held). The FE (WP3) reads `held` to open the request dialog. **`held[]` element = `{ production_order_material_id, material_name, planned_quantity, consumed_quantity, requested_quantity, excess, allowed_free }`** (match the existing `over_issue.materials` keys the FE 422 handler reads at production-orders.component.ts:1227-1230 — verify those keys and mirror them EXACTLY).
- WP2 relies on: when a line's excess is fully covered by an APPROVED-unused allowance, the partition treats it as FREE (so approve→execute issues it). Preserve `checkOverIssueAllowance`'s existing coverage math (`:534-556`).

## Acceptance criteria
- Mixed issue (material A within plan + material B over plan, no allowance) → draft created for A only; B returned in `held`; A's stock/consume path proceeds exactly as a normal issue; response 200.
- All-held issue (single over-plan material, no allowance) → NO draft created, `issued=null`, `held=[B]`, 200 (no `issue_requires_lines` error).
- Fully-covered excess (material has an approved-unused allowance ≥ excess) → treated as FREE, draft includes it (this is the WP2 approve→execute path).
- **OFF-path invariant:** `over_issue_requires_approval` off → `$enforceOverIssue` false → partition never runs → whole issue proceeds via `createDraftInventoryIssue` + inline/keeper exactly as today. Byte-for-byte.
- Keeper-gate-on: the free-lines draft is still returned as a held Draft for the keeper (existing `:141-144` behavior) — WP1 does not change keeper routing.

## Tests (Pest, sqlite) — extend `Modules/Production/tests/Feature/ProductionOverIssueApprovalTest.php`
- NEW: mixed issue → free line issued (draft has A only), held returns B, 200 (was 422).
- NEW: all-over-plan issue → no draft, held=[B], 200.
- NEW: covered-excess line issues as free (approved allowance covers it).
- KEEP GREEN: existing (a) setting-ON-no-approval, (d) setting-OFF-immediate, (f) backflush-never-gated, (g) keeper-path-hold, (h) reject-no-consume — especially the OFF-path (d) and backflush (f) invariants. If an existing test asserted the OLD 422-whole-issue behavior for a MIXED case, update it to the new partition contract and note it in the LEDGER as a deliberate contract change.

## Flags
- **[FIN]** — touches the material-issue path (stock + WIP downstream). Architect re-consult on the diff.
- **migration:** none.

## Out of scope
- The approve→auto-draft seam (WP2). The FE deletion (WP3). Any `used_quantity` revert / new columns (WP4). Do NOT change `consumeOverIssueAllowances` or the keeper apply path — decision D5 is already satisfied there.
