# WP4 — Over-issue approval cycle [FIN]

## Goal
Issuing materials **up to** the planned quantity stays free. Issuing **beyond** planned (consumed + qty > planned for a material), when `production.over_issue_requires_approval` is ON (default), must be **held pending a separate approval** with a **mandatory reason** — only after approval may the excess be issued. The excess then surfaces as material cost variance (existing mechanism). This over-issue approval is a **separate, independent layer** from the ISS-9103 warehouse-keeper approval.

## Core design decision (risk containment — VALIDATED by architect design gate, GO-WITH-CHANGES)
**The over-issue approval is a GATE that permits the EXISTING issue path to exceed planned — NOT new accounting code. Add ZERO new GL/WIP posting.** The gate only (a) blocks an over-planned issue unless an approved allowance covers it, and (b) captures the reason. Stock + WIP flow through the UNCHANGED IssueMaterials path once permitted.

**CORRECTED accounting rationale (the earlier draft was WRONG — do not repeat it):** Over-issue does NOT post to `production.cost_variance_account_id` under ACTUAL costing (the default/common path). It correctly **capitalises into Finished-Goods inventory value** (issue posts DR WIP/CR Inventory at full actual incl. excess → inflated WIP raises FG unit cost; close settles WIP residual to FG, `PostGoodsReceiptJournal.php:63-64`, `CloseProductionOrder.php:121-142`). It surfaces as the **reporting** figure `ProductionOrder::costVariance()` = `totalActualCost − totalPlannedCost` (`ProductionOrder.php:285-288`, on `ProductionOrderResource.php:60`) — ALWAYS, regardless of costing method — and additionally as a GL Material Usage Variance only under STANDARD costing (`PostGoodsReceiptJournal.php:99-114`). So "shows as cost variance" is satisfied by the reporting figure. **No GL code to add.**

## Architect must-do adjustments (follow EXACTLY)
1. **Split the gate into CHECK and CONSUME:**
   - **CHECK (permit-or-422):** in `IssueMaterials::execute()` AFTER `resolveLines()` and BEFORE `createDraftInventoryIssue()` (~IssueMaterials.php:119-121). Compute per material `allowedFree = max(0, planned − consumed)`; if `qty > allowedFree` and setting ON and `$requireApprovalGate === true` → look for an `approved` allowance with `requested_quantity − used_quantity >= (qty − allowedFree)`; if none → abort the WHOLE issue with 422 (before any draft/sequence number is burned). Do NOT increment used_quantity here.
   - **CONSUME (increment used_quantity):** inside `applyEffects()` (~IssueMaterials.php:212 loop), computing the actual excess from the LIVE `issued_quantity` vs `planned − consumed` at that moment (reflects any keeper trim). `applyEffects` runs exactly once on BOTH the inline path and the keeper-approval path → consume is exactly-once.
2. **`lockForUpdate()` the `production_over_issue_approvals` row at the CONSUME point**, re-assert `requested_quantity − used_quantity >= excess` under the lock, then increment — inside the effects transaction. Throw with a clear message if insufficient (race: `consumed` grew between check and approval) — mirrors the existing throw-inside-ApproveIssue-tx pattern (`ApplyMaterialIssueOnApproval.php:72-76`).
3. **Bypass the gate on backflush:** gate ONLY when `$requireApprovalGate === true` (same condition as the ISS-9103 gate at `IssueMaterials.php:126`). `ConfirmOperation` backflush (`$requireApprovalGate=false`) must always post immediately — never stall operation confirmation.
4. **Keep all-or-nothing per issue — do NOT physically split the InventoryIssue line.** `requested_quantity` = the excess is allowance SIZING only; the issue still moves the full requested qty in ONE document (consumed_quantity, reservation release, lot allocation all unchanged). If a user wants the free part now, they issue up to planned as a normal issue, then the excess separately after approval.
5. **OFF-path byte-for-byte:** when setting OFF, or `qty <= allowedFree`, the gate is a pure no-op — no new locks, no new queries on that path. Add a test asserting the inline OFF path is unchanged.
6. **The 422 payload must name the per-material excess AND the `production.orders.approve_over_issue` permission** so the keeper knows the next step (default-ON flips behaviour for in-flight orders — intended per client).

## Exact files
- BE setting: `/home/moonui2/moon-erp-be/Modules/Production/database/seeders/ProductionSettingDefinitionSeeder.php` — add `production.over_issue_requires_approval` (boolean, default `'true'`, scope company; mirror the `production.material_issue_requires_approval` definition added by ISS-9103). Seed it on moonui2_dev_be same-step.
- BE permission: `Modules/Production/database/seeders/RolePermissionSeeder.php` (or the production permission seeder) — add `production.orders.approve_over_issue`; grant to admin/owner role only by default (admin can re-assign to production manager/accountant — the client wants it separately assignable). Seed on dev.
- BE migration (NEW, timestamp after 2026_07_15): `production_over_issue_approvals` table — `id, company_id, production_order_id, production_order_material_id, requested_quantity(decimal 15,3 = the excess above planned being requested), reason(string), status(string: pending|approved|rejected, default pending), used_quantity(decimal 15,3 default 0), requested_by, approved_by(nullable), approved_at(nullable), timestamps, softDeletes`. FKs constrained. Run on dev same-step.
- BE model (NEW): `Modules/Production/app/Models/ProductionOverIssueApproval.php` — fillable + casts + relations (order, material, requester, approver). Company-scoped (use the project's tenant scope pattern like other Production models).
- BE action/gate: `Modules/Production/app/Actions/IssueMaterials.php` (read FULLY first — it carries ISS-9103 `$applyingInline`/`applyEffects`) + `ProductionOrderController.php::issueMaterials` (:583). Implement the gate as **CHECK + CONSUME split** exactly per the "Architect must-do adjustments" section above (CHECK before `createDraftInventoryIssue`, CONSUME with `lockForUpdate` inside `applyEffects`, gate only when `$requireApprovalGate === true`, OFF-path no-op). The 422 key is `over_issue_requires_approval` with payload listing per-material `{material_id, product, planned, consumed, requested, excess}` + the approver permission name.
- BE endpoints (controller + `routes/api.php`):
  - `POST production-orders/{order}/over-issue-requests` — body `{production_order_material_id, requested_quantity, reason(required)}` → creates a `pending` approval (requester = auth user). Permission: same as issue (`production.orders.issue_materials` or whatever gates issueMaterials — match it).
  - `GET production-orders/{order}/over-issue-requests` — list for the order (+ maybe a global pending list for approvers).
  - `POST production-orders/{order}/over-issue-requests/{req}/approve` and `.../reject` — permission `production.orders.approve_over_issue`; sets status + approver + approved_at. Reason immutable.
- BE i18n: `Modules/Production/lang/{en,ar}/production.php` — `over_issue_requires_approval`, reason labels.
- BE Resource: `ProductionOverIssueApprovalResource.php`.
- FE: `src/app/features/production/orders/production-orders.component.ts`+`.html` — on issue 422 `over_issue_requires_approval`, show a dialog to submit an over-issue request (pre-fill excess per material + **mandatory reason** select/textarea: هدر/سقط/إعادة تشغيل). A list/section of pending over-issue requests with Approve/Reject for users holding `production.orders.approve_over_issue`. After approval, the requester re-runs issue (now permitted).
- FE models/service + i18n keys `PRODUCTION.ORDERS.OVER_ISSUE.*`.

## Behaviour / interface
- Setting OFF → over-issue works exactly as today (no gate). Setting ON → over-planned portion blocked until an approved allowance covers it.
- Approval is per-material, quantity-bounded (`requested_quantity`), consumed via `used_quantity` so one approval can't be reused beyond its amount.
- Reason mandatory on the request.
- Independent of `production.material_issue_requires_approval` (ISS-9103): both can be ON; an over-issue on an approval-gated issue still first needs the over-issue allowance, then follows the warehouse-keeper draft/approve path. No double WIP (reuse existing single path).

## Acceptance criteria
- Setting ON: issuing qty ≤ (planned−consumed) → succeeds immediately. Issuing beyond → 422 `over_issue_requires_approval` with per-material excess. After creating + approving a request for the excess, re-issuing that excess → succeeds and `consumed_quantity` exceeds `planned_quantity`. Assert the variance via the REPORTING figure: `actual_material_cost` (or `ProductionOrder::costVariance()`) rises with the excess while planned stays fixed — do NOT assert a variance JE (there is none under actual costing; the excess capitalises into FG, per the architect finding).
- Setting OFF: over-issue succeeds with no gate (byte-for-byte today).
- `production.orders.approve_over_issue` enforced on approve/reject (403 without).
- ISS-9103 guard suites stay green; OFF-path invariant intact; no double WIP.

## Tests
- New `ProductionOverIssueApprovalTest.php` (Pest/sqlite): (a) setting ON + over-issue with no approval → 422; (b) create request → approve → re-issue → consumed>planned + variance surfaces; (c) approve without permission → 403; (d) setting OFF → over-issue succeeds directly; (e) approval quantity bound: excess > approved allowance → still 422.
- Guard suites (ProductionMaterialOwnershipTest, ProductionIssueCancelTest, ProductionConfirmationsTest) stay green.

## Flags
- **[FIN]** — architect-agent design review BEFORE coding the gate (confirm the gate-only approach produces correct variance via the existing path and never double-posts WIP), and Codex review after. Fable is EMERGENCY-ONLY — use architect + Codex.
- **migration: yes** — new table; run on dev same-step; timestamp sorts after existing.

## Out of scope
- No new GL/WIP posting code (reuse existing issue path). No change to ISS-9103 warehouse approval. No reopen logic (WP2).
