# LEDGER — Production Partial-Issue Fix + Over-Issue Approval (ISS-2026-9105)

**Date:** 2026-07-15 · **Branch:** hazemdev2 (both repos) · **Dev DB:** moonui2_dev_be (forward-only)
**Source (approved + scope-frozen):** knowledge-base/plans/production-partial-issue-completed-investigation.html (§7 plan, §8 decisions) · Portal ISS-2026-9105 scope_freeze step 3629.

**Owner/client acceptance test:** A production order that is `completed` with a material still partially issued can be **reopened for issue** (Completed→InProcess), the remainder issued, then re-completed. Trying to **complete** an order that still has un-issued materials asks for explicit confirmation instead of silently completing. Issuing **up to** the planned qty is free (remainder + batches). Issuing **beyond** the planned qty (when the setting is ON) is **held pending a separate over-issue approval** with a mandatory reason; only after approval does the excess move stock and surface as cost variance. The warehouse-keeper approval (ISS-9103) and the over-issue approval are independent layers that compose correctly.

## Frozen decisions (do NOT re-litigate)
- **D1 — complete guard:** completing an order with `planned > consumed` materials requires explicit confirmation (`allow_under_issue`/`confirm` flag), NOT a hard block. Under-issue is legitimate.
- **D2 — reopen:** new action Completed→InProcess (`reopen`), permission-gated. Reopen only re-opens the issue door; it reverses NO journals.
- **D3 — over-issue approval:** setting `production.over_issue_requires_approval` (boolean, default **TRUE**, scope company). When ON and (consumed+qty > planned) for a material → the over-planned portion is held pending approval by the **separate** permission `production.orders.approve_over_issue` (grantable to any role) with a **mandatory reason**. Post-approval → excess issues, surfaces in cost variance (`production.cost_variance_account_id`).
- **D4 — layering:** over-issue approval is fully independent of ISS-9103 warehouse-keeper approval (`production.material_issue_requires_approval`). OFF-path for BOTH stays byte-for-byte today. No double WIP.

## Phases (WP table)
| WP | Scope (one dense line) | Repo | Depends | Review | Migration? | Status | Commits |
|----|------------------------|------|---------|--------|-----------|--------|---------|
| WP1 | Completion guard: `complete()` requires explicit confirm when any material has remaining>0 (BE 422 without flag + FE confirm dialog). | BE+FE | — | Codex | no | ✅ done | BE 7f89b82e2 + test 51cd9e1ab · FE 8efc19eca + fix f6c5d3a46 · review HIGH(produced_qty race) FIXED via completeLoading gate · tests 4 passed + regression 25 · ng build green |
| WP2 | Reopen-for-issue action Completed→InProcess (endpoint + permission + FE button). Reverses no journals. [FIN-adjacent] | BE+FE | WP1 | Codex+arch | no | ✅ done | BE 2482d13a5 · FE 6ac1c78bc · tests 5+50 green · review APPROVE (0 crit/high/med; double-post proven impossible — settlement only at close, idempotency key, reopen blocked from Closed) · 2 Low → deferrals |
| WP3 | FE issue UX: clear labeled issue button (not bare icon), visible remaining, clearer insufficient_stock message. No logic change. | FE | — | Codex | no | ✅ done | FE b726b7458 · ng build green · self-reviewed (presentation-only: tooltip+error-map+i18n) |
| WP4 | [FIN] Over-issue approval cycle: setting (default TRUE) + separate permission + hold over-planned portion pending approval w/ mandatory reason → cost variance; independent of 9103. | BE+FE | WP1,WP2 | Codex+arch | yes (production_over_issue_approvals) | ✅ done | BE 1e6472e3c + fix 57f297342 · FE 882e5c0d7 · migration ran on dev · arch design gate GO-WITH-CHANGES (all 6 folded) · [FIN] review WARNING→2 HIGH FIXED (per-material aggregate consume + keeper-path tests) · over-issue suite 9 passed + guard 29 green |

## DAG / serialization
- **One writer per repo at a time.** WP1, WP2, WP4 all touch BE `ProductionOrderController` + `ProductionOrderStatus` → **serialize BE: WP1 → WP2 → WP4**.
- WP3 is FE-only and independent, but to keep one-writer-per-repo simple, run it in slot order too (FE touched by WP1/WP2/WP4 as well). Execute strictly sequential: WP1 → WP2 → WP3 → WP4.

## Baseline (2026-07-15, sqlite)
Guard suites **29 passed / 0 failed**: ProductionMaterialOwnershipTest + ProductionIssueCancelTest + ProductionConfirmationsTest. These MUST stay green (esp. OFF-path invariant + ISS-9103 tests). FE `ng build` assumed green (verify before WP finish).

## Critical context
- Branch hazemdev2 already carries ISS-9103 (warehouse-keeper approval: IssueMaterials.php `$applyingInline`+`applyEffects` + `ApplyMaterialIssueOnApproval` listener on `InventoryIssueApproved`). WP4 must compose, not collide.
- Status enum today: `ProductionOrderStatus.php` transitions Planned→[Released,Cancelled], Released→[InProcess,Cancelled], InProcess→[Completed], Completed→[Closed]. `canIssue()` = Released||InProcess (:74-77). complete() at ProductionOrderController.php:314-329 checks canComplete() only.
- Reviewers: Codex per WP; architect agent as the [FIN] design gate (Fable is EMERGENCY-ONLY per owner). 
- ⛔ No merge/main/deploy — owner runs /fullpush. After all WPs: notify main session to publish portal verification (do NOT post to portal from within).

## Review findings (open)
- **WP1 HIGH (pending fix):** FE `performComplete()` chains `recordOutput → complete` as 2 HTTP calls; `recordOutput` ACCUMULATES `produced_quantity` (ProductionOrderController.php:582). If user submits before `getById` loads materials → recordOutput commits, complete 422s, retry re-adds produced_quantity (double-count). Fix: gate the complete submit until the full order (materials) is loaded so the un-issued check is authoritative before recordOutput; and/or don't re-run recordOutput on a 422 retry. Add a test proving recordOutput isn't double-applied. (WP1-fix agent must wait until WP3 releases the FE component.)
- WP1 MEDIUM: add zero-materials-completes test; WP1 LOW: assert `materials_not_fully_issued` body in case (a).

## WP4 review findings (open → fixing)
- **HIGH-1 (fix):** keeper path (9103 ON) has ZERO test coverage — add: (1) 9103+9105 ON → approve allowance → issue creates draft (used_quantity still 0) → keeper approves draft → used_quantity drawn EXACTLY once; (2) keeper REJECTS draft → used_quantity stays 0, no MfgMaterialIssue.
- **HIGH-2 (fix):** CHECK aggregates excess per-material but CONSUME (consumeOverIssueAllowance, per-line in applyEffects) reads stale per-line consumed_quantity → two lines same material can under-draw used_quantity (over-consume beyond allowance via direct API). Fix: aggregate CONSUME per material_id across the issue (mirror CHECK) / fresh-read consumed; consume allowance once per material. Do NOT reject duplicate lines (batch-split issuing is legitimate).
- **MEDIUM/pre-existing (deferral):** duplicate same-material lines also cause a PRE-EXISTING consumed_quantity lost-update (not introduced by 9105) — record as deferral, out of 9105 scope.
- **LOW (accept):** getBool evaluated on manual-OFF path (cache-backed, negligible); no segregation-of-duties on self-approve (not in contract).

## Phase C — FINISH (2026-07-16)
- **Whole-feature cross-WP review: APPROVE** (0 crit/high/med; 1 Low). Seams verified clean: reopen×over-issue (allowedFree=0 when over-issued → gated), completion-guard×over-issue (remainingQuantity clamps 0 → no false prompt), reopen×completion-guard (fresh re-eval), permissions coherent (both new perms in Core catalog + ProductionRoleSeeder + dependency map), FE state distinct (no signal/method/i18n collisions).
- **Full relevant suite: 54 passed (331 assertions)** — ProductionOverIssueApproval(9) + Reopen(5) + CompletionGuard(4) + MaterialOwnership + IssueCancel + Confirmations + TollMaterialCharge. Zero new failures vs baseline (29/0).
- **CHANGELOG [Unreleased]:** 3 well-formed bilingual bullets (WP1 completion confirm-gate, WP2 reopen, WP4 over-issue approval). WP3 is UX-only, folded in.
- **LOW (deferral):** `production.orders.reopen` depends only on `orders.view` → a hand-crafted role could get reopen without complete/consume (not reachable via seeded roles). Latent permission-design nuance, not a bug.

## Deferrals
- **WP4 pre-existing (out of 9105 scope):** duplicate same-material issue lines cause a consumed_quantity last-write-wins lost-update in applyEffects (each line updates its own stale ProductionOrderMaterial instance). Pre-dates 9105; fix needs a per-material aggregation of consumed writes across the whole issue. Surface to owner as a separate Production bug.
- **WP2 Low (Phase C):** add an e2e test driving reopen→re-complete→close and asserting a SINGLE WIP settlement JE (settlement count==1, WIP nets to zero) — locks the [FIN] invariant against future regression (proven safe by inspection now).
- **WP2 Low (Phase C):** strengthen ProductionReopenTest with direct StockBalance before/after + Event::fake()/assertNothingDispatched on reopen.
