# LEDGER — ISS-2026-9105 over-issue auto-draft redesign

**Date:** 2026-07-18 · Branch hazemdev2 (both repos) · Dev DB moonui2_dev_be (forward-only)
**Source (approved):** Portal ISS-2026-9105 step 3999 (redesign plan) + client step 4042 «تمام نفّذ كل الاتفاق عليه» (all recommendations accepted). Architect design gate (2026-07-18) refined it into WPs below.

**Owner/client acceptance test:** From the production order, issuing materials where some are within plan and one exceeds plan → the within-plan materials issue immediately (draft to keeper), the over-plan material is HELD and its over-issue request opens (mandatory reason). The over-issue REQUEST/APPROVE UI no longer appears inside the issue dialog — approval happens only on the central «موافقات الصرف الزائد» screen. **Approving** an over-issue there AUTO-GENERATES a draft إذن صرف for that material's WHOLE qty (planned+excess) routed to the warehouse keeper (no manual re-issue). Keeper **rejects** the draft → no stock/WIP, allowance not consumed, order unchanged. Keeper **partial** → consumes actual approved qty, allowance drawn by actual excess only.

## Frozen decisions (do NOT re-litigate)
- **D1** — Remove the over-issue approve/reject SECTION from the production issue dialog (FE); keep the REQUEST-with-reason dialog. Central screen unchanged.
- **D2** — Approving an over-issue AUTO-GENERATES a draft InventoryIssue (ref_type=ProductionOrder, Draft) routed to the keeper — replaces today's bare status flip.
- **D3 (Q1)** — The generated draft covers the material's WHOLE qty (planned+excess) in ONE issue.
- **D4 (Q2)** — Materials WITHIN plan issue immediately (per-MATERIAL split, not per-quantity). An over-plan material is HELD ENTIRELY; its planned portion ships together with the excess in the single approval-time draft (Q1 precedence). ← interpretation confirmed by architect as the only reading consistent with D3.
- **D5** — Keeper reject → allowance NOT consumed (stays available), order unchanged. Keeper partial → consume actual excess only. **ALREADY SATISFIED by current code (consume only at keeper-approve) — verify with tests, do NOT rebuild.**
- **Minor decisions (main-session calls, not client questions):** (a) WP1 success response is 200 with `issued`+`held` (BE authoritative), replacing the all-or-nothing 422. (b) `canIssue()` false at approval → still flip status to approved but SKIP the auto-draft + soft notice (don't hard-fail the approval). (c) auto-draft `created_by` = the approver.
- **OFF-path invariant:** both gates off (`over_issue_requires_approval` off) → whole issue proceeds exactly as today. Byte-for-byte. Tests must assert.

## WP table
| WP | Scope (one dense line) | Repo | Depends | Review | Migration? | Status | Commits |
|----|------------------------|------|---------|--------|-----------|--------|---------|
| WP1 | [FIN] `checkOverIssueAllowance` → PARTITION (free lines vs held over-plan materials) instead of aborting the whole issue with 422; `execute()` builds the draft from FREE lines only, skips draft when none free, returns held materials; `issueMaterials` controller responds 200 `{issued, held}`. OFF-path byte-for-byte. | BE | — | Codex+arch | no | ✅ done | `3b757f0c9` · tests 38→40 pass · code-review(opus)=APPROVE 0 crit/high, 0 [FIN] risk, OFF-path byte-for-byte · held[] keys: material_id,product_id,product,planned,consumed,requested,excess,approved_remaining · mechanism: public props `$heldMaterials`+`$issuedDraft` |

**WP1 → WP3 coordination (from review, MEDIUM — WP3 must handle):**
- **M1:** success response shape changed for ALL issues — order fields moved `response.data` → `response.issued` (JsonResource wrapper dropped when embedded in array). WP3 FE issue-submit success handler MUST read `res.issued` (not `res.data`) — affects the normal/OFF-path issue too, not just over-issue.
- **M2:** `over_issue.permission` key was dropped from the payload. If the FE used it to decide dialog-vs-notice, WP3 sources it FE-side (hard-code `production.orders.approve_over_issue`).
- **L1 (cosmetic):** stale `WP4:`/"...422" test titles in ProductionOverIssueApprovalTest — rename opportunistically.
| WP2 | [FIN] `decideOverIssueRequest` approve branch: auto-generate whole-qty draft on approve. | BE | WP1 | Codex+arch | no | ✅ done | `77aa0aa20` · tests 40→47 pass · code-review(opus)=**APPROVE (with notes)**: 0 crit/high, **[FIN] invariants HOLD on every path** (no double stock/WIP/over-draw — bounded at consume time under order row-lock) · wholeQty fresh, atomic (flip+execute one tx), idempotent (pending-guard), canIssue-false→notice, D5 untouched |
| WP3 | FE: delete the approve/reject SECTION from the issue dialog + adapt issue-submit to 200 `{issued,held}` (read res.issued, open request dialog from held); keep REQUEST dialog + shared service methods. | FE | WP1 | Codex | — | ✅ done | `1366dca88` (delete section + 200 handling, ng build green) + `0e0655478` (review MEDIUM: retype service to IssueMaterialsResponse, drop cast) · code-review(opus)=APPROVE 0 crit/high · central screen untouched · M2 no-op (FE never used permission key) |
| WP4 | (OPTIONAL, OUT of frozen scope — deferral unless client asks) cancel-revert of consumed allowance: add `inventory_issue_id` link + revert `used_quantity` on CancelIssue/CancelMaterialIssue. | BE | — | Codex+arch | yes | ⬜ deferred | |

## DAG / serialization
- BE WPs (WP1→WP2) touch `IssueMaterials.php` + `ProductionOrderController.php` → **serialize WP1 → WP2** (one writer per repo).
- WP3 (FE) depends on WP1's response shape; run after WP1 lands (BE done). One writer per repo — WP3 is FE-only, can run after WP1 while WP2 is BE.
- Execute order: **WP1 → WP2 → WP3**.

## Baseline (2026-07-18, sqlite CLI php)
- `ProductionOverIssueApprovalTest` + `ProductionConfirmationsTest` + `ProductionMaterialOwnershipTest` = **38 passed / 0 failed** (229 assertions).
- `PartialIssueApprovalTest` + `PartialIssueCancelTest` = **11 passed / 0 failed** (34 assertions).
- **Zero pre-existing failures** in the touched suites. These MUST stay green (esp. OFF-path invariant + ISS-9103 keeper tests + reject-no-consume (h)).

## Deferrals
- **WP4** cancel-revert of consumed allowance (architect §4) — net-new, out of «نفّذ كل الاتفاق»; do only if client asks.
- Pre-existing WP4-review open findings from the earlier over-issue LEDGER (keeper-path coverage) — folded into WP2 tests where they overlap.
- **WP2 review M1 (MEDIUM, non-financial UX):** duplicate held draft — after approve auto-creates a held draft (used_quantity still 0), a user who ALSO manually re-issues the same material builds a SECOND held draft; whichever keeper-approves second hard-fails 422 `over_issue_allowance_exhausted` (stranded draft to cancel). **Financial invariant HOLDS** (no over-consume). Fix needs a dedup guard (block manual re-issue of a material with an approved allowance + open draft, or link the draft to the allowance) — new surface beyond frozen decisions. **Deferred; flagged to client in the verification** (after approving, don't manually re-issue — the approval already generated the draft). Candidate follow-up.
- **WP2 review L1 (LOW):** if a backflush advances `consumed` between the unlocked wholeQty read and execute's locked read, approve holds the whole qty and returns `issue_created:false` with no `notice`/`held` (silent). Non-financial (under-issues; allowance survives for later manual issue). Consider surfacing `heldMaterials` on that branch.
- **WP2 review L2 (LOW):** keeper-reject test simulates rejection by cancelling the draft rather than driving the real reject endpoint — asserts the D5 effect, not the real reject path. Consider a follow-up test.
- **L (cosmetic):** stale `WP4:`/"...422" test titles + FE "(WP4)" labels — rename opportunistically.

## Notes
- [FIN]-adjacent state machine — architect design gate DONE (this LEDGER derives from it). Codex/code-reviewer per WP; architect re-consult on WP1/WP2 diffs. Fable EMERGENCY-ONLY (owner).
- No merge/main/deploy-to-client (owner /fullpush). Deploy /app for client testing. Update ISS-9105 verification when done. Decisions/comms via the ticket.
- Interpretation note (D4): if the client on testing wants an over-plan material's within-plan portion to ALSO stream immediately (literal per-quantity Q2), that reopens D3 — surface as a follow-up, don't pre-build it.
