# Phase 3 implementation report

**Status:** Phase 3 implemented, tested, and **closed after the mobile-shell and grant-STOP closeout.** **Phase 4 was not started.**  
**Stack (unchanged):** CodeIgniter 4, PHP 8.2+, MySQL 8 InnoDB, server-rendered Views, progressive JS only.  
**Suite:** `vendor/bin/phpunit --no-coverage` → **190 tests, 4807 assertions, 0 failures.**

---

## 1. Files changed

### Checkpoint A (already on `main` before this pass)

- `app/Database/Migrations/2026-09-12-300001_AddPhase3OperationalSchema.php`
- `app/Modules/IAM/Domain/PermissionCatalog.php` (`INWARD.REVERSE` only)
- `app/Modules/IAM/Domain/SampleIdentities.php` (Store Keeper grant; Plant Manager not granted)
- `app/Modules/Ui/Phase3Frames.php` (exact 110 CSV Frame Names)
- `app/Modules/Ui/FrameSimulation.php` (Phase 3 overlay under development/testing guard)
- `tests/unit/Phase3FramesTest.php`
- `tests/integration/Phase3SchemaTest.php`
- `docs/PHASE3_PLAN.md` (approval record; implementation not in that commit)

### Checkpoint A-fix (`9763ea9`)

- `app/Database/Migrations/2026-09-12-300001_AddPhase3OperationalSchema.php` — drop Phase 3 tables first; clear CI4 field-name cache after ALTER
- `app/Database/Seeds/TransactionSeeder.php` — additive `product_id` / `batch_lot_id` / `unit_id` on `WST-2026-000067`; rejection/reassignment timestamps. **Does not change** 067 status, 3+7+2, or 116.000 MT
- `app/Modules/Inventory/Services/ExpiryProcessor.php` — persist `row_version` on SYSTEM_EXPIRY insert

### Checkpoint B (`d88d495`) — coupled application code

Routes reference Return/Rejection/Wastage controllers, so those PHP files ship with INW-002/003 to keep the app bootable.

- Routes, sidebar, README Phase 3 table
- `InwardController`, `InwardService` (`postPlantProduction`, `postOpeningStock`, `recipePreview`, `writePostedInward`)
- `ReturnController`, `ReturnRecoveryService`, `InwardReversalService`, `InwardQuery`
- `RejectionController`, `DispositionService`, `RejectionService`
- `WastageController`, `WastageService`
- `IdempotentCommandStore`, `EvidenceStorage`
- Views: production, opening, return, register, rejection form/reassign, disposition resolve, wastage list/create/review/reverse, OV-06, OV-15
- `public/css/app.css`, `public/js/app.js`
- `tests/integration/Phase3InwardProductionOpeningTest.php`

### Checkpoints C–F (tests only; behaviour already in B)

| Checkpoint | Hash | File |
|---|---|---|
| C | `0f50d3b` | `tests/integration/Phase3ReturnAndReversalTest.php` |
| D | `91a7cae` | `tests/integration/Phase3DispositionTest.php` |
| E | `50b5f6b` | `tests/integration/Phase3WastageTest.php` |
| F | `065d2fd` | `tests/feature/Phase3FrameHttpTest.php` |

### Checkpoint G (`a80ac6a`)

- `tests/feature/Phase3AuthorizationHttpTest.php`
- `tests/feature/ProductionRouteAbsenceTest.php`
- `docs/PHASE3_IMPLEMENTATION_REPORT.md`

### G-fix (idempotent grants + live QA)

- `app/Database/Seeds/PermissionSeeder.php` — insert missing catalogue rows only
- `app/Database/Seeds/RoleSeeder.php` — insert missing roles/grants only
- `tests/integration/Phase3SchemaTest.php` — Store Keeper + Super Admin have `INWARD.REVERSE`; Plant Manager does not
- `README.md` — existing-database seed note

No React/Next/Nest/PostgreSQL/JWT/Shield. No IRN job. No production `/seed`, `/fixture`, `/demo`, `/probe`, or `/test` routes.

---

## 2. Migrations and rollback behaviour

**Forward:** `2026-09-12-300001_AddPhase3OperationalSchema` is additive InnoDB only.

| Object | Change |
|---|---|
| `inward_entry` | DB-CHG-006 columns: `original_challan_id`, `original_challan_line_id`, `original_invoice_id`, `original_credit_note_id` (**nullable, no FK** — credit-note tables do not exist), `client_id`, `return_reason`, `reusability_confirmed`, `disposition_record_id`, `replaces_inward_entry_id`. Pairing CHECK. Return-reusability CHECK. **No** `return_status` / `approved_by` / `approved_at_utc` |
| `material_movement` | Generated unique `reversal_of_key` so one `REVERSAL` per original movement |
| `rejected_dispatch` | `rejection_reason`, `remarks`, nullable `rejected_at_utc`, `row_version`. Status CHECK remains `PENDING` / `PARTIALLY_DISPOSED` / `FULLY_DISPOSED`. **No** `REVERSED` |
| `reassignment` | `client_company_id`, `client_site_id`, `reassigned_at_utc`, `row_version` |
| `wastage_record` | nullable `product_id`, `batch_lot_id`, `unit_id`, `row_version`. C13: `source_bucket` may be NULL |
| new tables | `evidence_attachment`, `idempotent_command` (types: `INWARD_REVERSE`, `INWARD_CORRECT`, `REJECTION_SAVE`, `REASSIGN_SAVE`, `DISPOSITION_SAVE`, `WASTAGE_APPROVE`, `WASTAGE_REVERSE` — **no** `RETURN_APPROVE`), `inward_reversal_history` |

**Rollback (`down()`):**

1. `DROP TABLE IF EXISTS` `inward_reversal_history`, `evidence_attachment`, `idempotent_command` **first** so `fk_irh_mv` cannot block later Phase 0–2 `DROP TABLE material_movement`.
2. Drop Phase 3 FKs and columns from existing tables.
3. Restore `chk_wst_bucket` to `AVAILABLE` / `REJECTED_PENDING` / `RESERVED_REASSIGNMENT` after coercing NULL buckets.
4. Clear CI4 `dataCache` field/table lists so PHPUnit `refresh=true` does not serve a pre-ALTER column list.

`down()` does not delete Phase 0–2 tables or posted sample documents. It reverses only this additive migration. PHPUnit `DatabaseTestTrait` refresh downs every migration in reverse; this `down()` is what makes that refresh succeed.

---

## 3. Route / controller / service / permission matrix

| Method | Route | Controller | Service | Permission |
|---|---|---|---|---|
| GET/POST | `/inward/production` | `InwardController::productionCreate/Store` | `InwardService::postPlantProduction` | `INWARD.VIEW` / `INWARD.ADD` |
| GET | `/inward/production/{id}` | `productionShow` | `InwardService::find` | `INWARD.VIEW` / `INWARD.ADD` |
| GET/POST | `/inward/opening-stock` | `openingCreate/Store` | `InwardService::postOpeningStock` | `INWARD.VIEW` / `OPENING_STOCK.ADD` |
| GET | `/inward/opening-stock/{id}` | `openingShow` | `InwardService::find` | `INWARD.VIEW` / `OPENING_STOCK.ADD` |
| GET/POST | `/inward/returns` | `ReturnController` | `ReturnRecoveryService::save` | `INWARD.VIEW` / `INWARD.ADD` |
| GET | `/inward/returns/{id}` | `ReturnController::show` | `InwardService::find` | `INWARD.VIEW` / `INWARD.ADD` |
| GET | `/inward` | `InwardController::index` | `InwardQuery::listForUser` | `INWARD.LIST` / `INWARD.VIEW` |
| POST | `/inward/{id}/reverse` | `InwardController::reverse` | `InwardReversalService::reverse` | `INWARD.REVERSE` |
| POST | `/inward/{id}/correct` | `InwardController::correct` | `InwardReversalService::reverse` (+ replacement) | `INWARD.REVERSE` |
| GET/POST | `/rejections/create` | `RejectionController` | `RejectionService::save` | `DISPATCH.VIEW` / `DISPATCH.ADD` |
| GET | `/rejections/{id}` | `show` | `RejectionService::find` | `DISPATCH.VIEW` / `DISPATCH.ADD` |
| GET/POST | `/rejections/{id}/reassign` | `reassign*` | `DispositionService::reassign` | `DISPATCH.REASSIGN` |
| GET/POST | `/rejections/{id}/disposition` | `disposition*` | `DispositionService::save` | `DISPATCH.DISPOSE` and/or `DISPATCH.REASSIGN` |
| GET | `/wastage` | `WastageController::index` | `WastageService::listForUser` | `WASTAGE.LIST` / `WASTAGE.VIEW` |
| GET/POST | `/wastage/create` | `create/store` | `WastageService::save` | `WASTAGE.VIEW` / `WASTAGE.ADD` |
| GET | `/wastage/{id}` | `show` | `WastageService::find` | `WASTAGE.VIEW` |
| POST | `/wastage/{id}/approve` | `approve` | `WastageService::approve` | `WASTAGE.APPROVE` |
| POST | `/wastage/{id}/return` | `returnForCorrection` | `WastageService::returnForCorrection` | `WASTAGE.APPROVE` |
| GET/POST | `/wastage/{id}/reverse` | `reverseForm/reverse` | `WastageService::reverse` | `WASTAGE.VIEW` / `WASTAGE.CANCEL_REVERSE` |

**Not added:** `INWARD.APPROVE`, `DISPATCH.REVERSE`, `WASTAGE.REVERSE`, any `/inward/*/approve`, any `/rejections/*/reverse`.

**Grants (C6):** Store Keeper + Super Admin (catalogue) receive `INWARD.REVERSE`. Plant Manager does **not**. Plant Manager keeps `INWARD.ADD` + `OPENING_STOCK.ADD`.

Vendor inward (`/inward/vendor`) is unchanged from Phase 1.

---

## 4. Exact 110-frame render inventory

**Total 110. Desktop 91. Mobile 19. Tablet 0.** Direct CSV Frame Names. No aliases.

Rendered by `Phase3FrameHttpTest::testAllOneHundredTenFramesRender` (HTTP 200, Frame Name in `data-frame`, testing environment, no “Development visual preview” banner).

**INW-SCR-002 (11)**  
`D-INW-SCR-002-Create-Default`, `D-INW-SCR-002-Create-Draft`, `D-INW-SCR-002-Create-Error`, `D-INW-SCR-002-Create-Loading`, `D-INW-SCR-002-Review-PendingApproval`, `D-INW-SCR-002-Review-Approved`, `D-INW-SCR-002-Review-ReturnedForCorrection`, `D-INW-SCR-002-Review-ConcurrentUpdate`, `D-INW-SCR-002-Review-PermissionRestricted`, `M-INW-SCR-002-Create-Default`, `M-INW-SCR-002-Create-Error`.

**INW-SCR-003 (11)**  
`D-INW-SCR-003-Create-Default`, `D-INW-SCR-003-Create-Draft`, `D-INW-SCR-003-Create-Error`, `D-INW-SCR-003-Create-Loading`, `D-INW-SCR-003-Review-PendingApproval`, `D-INW-SCR-003-Review-Approved`, `D-INW-SCR-003-Review-ReturnedForCorrection`, `D-INW-SCR-003-Review-ConcurrentUpdate`, `D-INW-SCR-003-Review-PermissionRestricted`, `M-INW-SCR-003-Create-Default`, `M-INW-SCR-003-Create-Error`.

**INW-SCR-004 (15)**  
`D-INW-SCR-004-Create-Default`, `D-INW-SCR-004-Create-Draft`, `D-INW-SCR-004-Create-PendingApproval`, `D-INW-SCR-004-Create-Approved`, `D-INW-SCR-004-Create-ReturnedForCorrection`, `D-INW-SCR-004-Create-Error`, `D-INW-SCR-004-Create-PermissionRestricted`, `M-INW-SCR-004-Create-Default`, `D-INW-SCR-004-Create-Loading`, `D-INW-SCR-004-Review-PendingApproval`, `D-INW-SCR-004-Review-Approved`, `D-INW-SCR-004-Review-ReturnedForCorrection`, `D-INW-SCR-004-Review-ConcurrentUpdate`, `D-INW-SCR-004-Review-PermissionRestricted`, `M-INW-SCR-004-Create-Error`.

**INW-SCR-005 (9)**  
`D-INW-SCR-005-List-Default`, `D-INW-SCR-005-List-Filtered`, `D-INW-SCR-005-List-Empty`, `D-INW-SCR-005-List-NoResults`, `D-INW-SCR-005-List-Loading`, `D-INW-SCR-005-List-Error`, `D-INW-SCR-005-List-PermissionRestricted`, `M-INW-SCR-005-List-Default`, `M-INW-SCR-005-List-NoResults`.

**DSP-SCR-001 (11)**  
`D-DSP-SCR-001-Create-Default`, `D-DSP-SCR-001-Create-Draft`, `D-DSP-SCR-001-Create-Error`, `D-DSP-SCR-001-Create-Loading`, `D-DSP-SCR-001-Review-PendingApproval`, `D-DSP-SCR-001-Review-Approved`, `D-DSP-SCR-001-Review-ReturnedForCorrection`, `D-DSP-SCR-001-Review-ConcurrentUpdate`, `D-DSP-SCR-001-Review-PermissionRestricted`, `M-DSP-SCR-001-Create-Default`, `M-DSP-SCR-001-Create-Error`.

**DSP-SCR-002 (11)**  
`D-DSP-SCR-002-Create-Default`, `D-DSP-SCR-002-Create-Draft`, `D-DSP-SCR-002-Create-Error`, `D-DSP-SCR-002-Create-Loading`, `D-DSP-SCR-002-Review-PendingApproval`, `D-DSP-SCR-002-Review-Approved`, `D-DSP-SCR-002-Review-ReturnedForCorrection`, `D-DSP-SCR-002-Review-ConcurrentUpdate`, `D-DSP-SCR-002-Review-PermissionRestricted`, `M-DSP-SCR-002-Create-Default`, `M-DSP-SCR-002-Create-Error`.

**DSP-SCR-003 (12)**  
`D-DSP-SCR-003-Review-Default`, `D-DSP-SCR-003-Review-Draft`, `D-DSP-SCR-003-Review-PendingApproval`, `D-DSP-SCR-003-Review-Approved`, `D-DSP-SCR-003-Review-Error`, `D-DSP-SCR-003-Review-PermissionRestricted`, `M-DSP-SCR-003-Review-Default`, `D-DSP-SCR-003-Review-ReturnedForCorrection`, `D-DSP-SCR-003-Review-Reversed`, `D-DSP-SCR-003-Review-Loading`, `D-DSP-SCR-003-Review-ConcurrentUpdate`, `M-DSP-SCR-003-Review-PendingApproval`.

**WST-SCR-001 (13)**  
`D-WST-SCR-001-List-Default`, `D-WST-SCR-001-Create-Default`, `D-WST-SCR-001-Create-Draft`, `D-WST-SCR-001-Create-Error`, `D-WST-SCR-001-List-Filtered`, `D-WST-SCR-001-List-Empty`, `D-WST-SCR-001-List-Loading`, `D-WST-SCR-001-List-PermissionRestricted`, `M-WST-SCR-001-List-Default`, `M-WST-SCR-001-Create-Default`, `D-WST-SCR-001-List-NoResults`, `D-WST-SCR-001-List-Error`, `M-WST-SCR-001-List-NoResults`.

**WST-SCR-002 (9)**  
`D-WST-SCR-002-Review-PendingApproval`, `D-WST-SCR-002-Review-Approved`, `D-WST-SCR-002-Review-ReturnedForCorrection`, `D-WST-SCR-002-Review-Cancelled`, `D-WST-SCR-002-Review-Loading`, `D-WST-SCR-002-Review-PermissionRestricted`, `M-WST-SCR-002-Review-PendingApproval`, `D-WST-SCR-002-Review-Reversed`, `D-WST-SCR-002-Review-ConcurrentUpdate`.

**WST-SCR-003 (8)**  
`D-WST-SCR-003-Review-PendingApproval`, `D-WST-SCR-003-Review-Approved`, `D-WST-SCR-003-Review-ReturnedForCorrection`, `D-WST-SCR-003-Review-Reversed`, `D-WST-SCR-003-Review-Loading`, `D-WST-SCR-003-Review-ConcurrentUpdate`, `D-WST-SCR-003-Review-PermissionRestricted`, `M-WST-SCR-003-Review-PendingApproval`.

Visual states use `?frame=` only when `CI_ENVIRONMENT` is `development` or `testing`. Production ignores `?frame=` (`FrameSimulationHttpTest` + `Phase3FrameHttpTest::testProductionEnvironmentIgnoresFrameQuery`).

---

## 5. Direct-post compatibility-frame verification

Exact copy (`FrameCatalog::INWARD_COMPAT_COPY`):

`Not applicable — this inward type posts directly on Save and has no approval workflow.`

Asserted on every INW-002/003/004 frame whose name contains `PendingApproval`, `Approved`, or `ReturnedForCorrection`. Those frames also must not contain `>Approve<`, `Approve Return`, `Return for Correction`, `Submit for approval`, or `PENDING_APPROVAL`.

Operational Save posts:

| Screen | `source_type` | `movement_type` | Approval FSM |
|---|---|---|---|
| INW-002 | `PLANT_PRODUCTION` | `PLANT_PRODUCTION_INWARD` | None |
| INW-003 | `OPENING_STOCK` | `OPENING_STOCK_INWARD` | None |
| INW-004 reusable | `RETURN_RECOVERY` | `RETURN_RECOVERY_INWARD` | None |
| INW-004 not reusable | `RETURN_RECOVERY` | none | None |

C2: recipe grid is read-only; Save creates exactly one finished-product movement (no RM movements). Evidence: `Phase3InwardProductionOpeningTest`.

---

## 6. Return/Recovery entry-path matrix (C1)

| Path | Fields | Reusable Save | Not-reusable Save |
|---|---|---|---|
| `CHALLAN_LINE` | paired `original_challan_id` + `original_challan_line_id`; `client_id` from challan; `disposition_record_id` NULL | `RETURN_RECOVERY_INWARD` + Available + | inward row + pending USER_RECORDED wastage; `source_bucket` NULL; Available unchanged |
| `DISPOSITION` | `disposition_record_id` set; challan pair NULL | distinct `RETURN_RECOVERY_INWARD`; **does not** rewrite seeded 3.000 MT `RETURNED_TO_AVAILABLE` | same wastage routing |
| `NON_CUSTOMER` | customer docs may be NULL; `return_reason` identifies source | `RETURN_RECOVERY_INWARD` | pending wastage; no MLB row |

Do **not** use `REJECTION_RECOVERY` or `RETURNED_TO_AVAILABLE` for this inward. Tests: `Phase3ReturnAndReversalTest`.

---

## 7. Inward reversal dependency and concurrency

- `INWARD.REVERSE` only. Mandatory OV-06 reason.
- Allocated lots (`LOT-2026-00212`, `LOT-2026-00230`) block reverse and name `CH-2026-000481`.
- One compensating `REVERSAL` (`reversal_of_key` unique). Second reverse → `INW_ALREADY_REVERSED`. Duplicate-key race maps to the same code.
- Same idempotency key replays without a second replacement row.
- Correction = reverse original (qty unchanged) + new `inward_entry` with `replaces_inward_entry_id`.
- PermissionRestricted (Plant Manager Kumar): Reverse/Correct **absent**. Store Keeper Patil has the action; does not union Billing Clerk.

Tests: `Phase3ReturnAndReversalTest`, `Phase3AuthorizationHttpTest`.

---

## 8. Disposition reconciliation evidence (C4, C5, G2)

**C5:** DSP-001 Save writes `REJECTION_RECOVERY` → `REJECTED_PENDING`. Available unchanged.

**G2 dedicated close** (new 12.000 MT production lot + challan + rejection, **not** `REJ-2026-000044`):

| Path | Qty | Movement |
|---:|---:|---|
| Return to Availability | 3.000 | `RETURNED_TO_AVAILABLE` |
| Reassign to Product | 7.000 | `REASSIGNMENT_RESERVATION` |
| Record as Wastage | 2.000 | `WASTAGE_FROM_REJECTED` |
| Status | FULLY_DISPOSED | remaining 0.000 |

**DSP-ERR-017:** 5.000 + 7.000 + 3.000 against 12.000 rejected.

**Frozen seed `REJ-2026-000044`:** still 3.000 + 7.000 + 2.000 = 12.000 MT. `WST-2026-000067` remains Approved. No `REVERSED` status. No reverse route.

**C4 compatibility:** `D-DSP-SCR-003-Review-Reversed` renders `Not applicable — disposition records are permanent history and cannot be reversed.` Save form and Reverse control omitted.

Store Keeper DSP-003 omits `name="reassign_qty"` (no `DISPATCH.REASSIGN`); Return and Wastage remain.

---

## 9. Wastage approval / reversal and inventory impact

| Event | Inventory |
|---|---|
| WST-001 Save/Submit | none |
| WST-002 Approve (bucket set) | −source bucket on **exact lot**, one `WASTAGE_FROM_*` |
| Approve replay (same idempotency key) | no second reduction |
| Not-reusable return wastage Approve (`source_bucket` NULL) | no MLB row, `movement_id` NULL |
| WST-003 Reverse | `REVERSAL` restoring the source lot/bucket |
| SYSTEM_EXPIRY | already Approved; no Approve control |
| `WST-2026-000067` | **always Approved**; never mutated |

C3 fixtures are `?frame=` / testing-dev only. Production ignores `?frame=`.

---

## 10. Production-route safety scan

`ProductionRouteAbsenceTest` plus Phase 3 frame HTTP:

- Routes.php has no `seed` / `fixture` / `demo` / `probe` / `/test` shortcuts.
- Former seed/probe paths 404.
- No `/inward/returns/approve`, `/inward/{id}/approve`, `/rejections/{id}/reverse`.
- Health JSON `"phase": 3`.

---

## 11. Desktop and mobile browser QA

Dev server: `php spark serve --host 127.0.0.1 --port 43221`. PHPUnit HTTP covers all 110 frames including 19 `M-*` (`viewport-mobile`).

**Desktop (N. Joshi, Super Admin), viewing only — no Save/Approve/Reverse submits:**

| Screen | Result |
|---|---|
| INW-002 `/inward/production` | Direct-post lede, informational recipe, Save, no Approve |
| INW-002 posted `/inward/production/3` | `INW-2026-000230` / `LOT-2026-00230`, no Save/Approve |
| INW-003 `/inward/opening-stock` | Cutover lede, Save, no Approve |
| INW-004 `/inward/returns` | Three entry paths, reusability radios, Save, no Approve Return |
| INW-005 `/inward` | After idempotent grant seed: allocated `INW-2026-000230` and `INW-2026-000199` show disabled Reverse naming `CH-2026-000481`. R. Kumar (Plant Manager) sees no Reverse/Correct |
| DSP-001 `/rejections/create` | Rejection recovery copy; Available unchanged on Save |
| DSP-003 `/rejections/1/disposition` | `REJ-2026-000044` FULLY_DISPOSED remaining 0.000; **3.000 + 7.000 + 2.000**; no Reverse |
| WST-001 `/wastage` | `WST-2026-000067` Approved |
| WST-002 `/wastage/1` | Status **APPROVED**, history SAVED (S. Patil) then APPROVED (R. Kumar), no Approve control |

**Compatibility `?frame=` (development only):** yellow “Development visual preview” banner is Phase 1/2 `simulation_banner` when `CI_ENVIRONMENT=development`. PHPUnit `testing` omits it. Production ignores `?frame=`. Compat copy on `D-INW-SCR-002-Review-PendingApproval` and `D-DSP-SCR-003-Review-Reversed` is present. Not a defect.

**Mobile 390×844 (closeout correction):** all 19 `M-*` frames reuse `c_nav_01_shell` — Mobile Header + 44×44 hamburger (`data-nav-toggle`), sidebar as a **closed off-canvas drawer** (Figma token R1 / `--component-shell-sidebardrawer: 280px`). No stacked desktop sidebar. `@media (max-width: 767px)` applies the same drawer on production routes without `?frame=`. Full 19-frame matrix: §16.

---

## 12. Complete test and assertion counts

| | Tests | Assertions |
|---|---:|---:|
| **Phase 0–3 complete suite (closeout)** | **190** | **4807** |
| Phase 2 close (frozen baseline) | 154 | 2980 |
| Prior Phase 3 close (checkpoint G-fix) | 184 | 4477 |
| Closeout correction added | +6 methods | +330 |

Closeout methods: mobile-frame HTTP (2), authorization Super Admin / Billing Clerk / Kumar 403-on-correct, mandatory reason, plant-scope independent of permission. Frame HTTP still covers all **110** CSV names.

---

## 13. Phase 0–2 frozen-regression matrix

| Identity | Result |
|---|---|
| Ledger closing M30 Chakan | **116.0000** (`120 + 25 − 30 + 3 − 2`) |
| Allocation | 18.000 + 12.000 = **30.000** MT on CH-2026-000481 |
| `LOT-2026-00230` remaining | **13.000** MT |
| Disposition `REJ-2026-000044` | **3.000 + 7.000 + 2.000 = 12.000** MT |
| `WST-2026-000067` | **Approved**, USER_RECORDED, source lot `LOT-2026-00212` |
| Invoice `INV-2026-000119` | **₹177,592.00** |
| Document numbers / FKs / numbering | unchanged |
| Company/plant isolation | unchanged |
| Active-role permissions | plus **only** `INWARD.REVERSE` |
| Audit | existing events preserved; new types are additive |
| No IRN job / no automatic IRN retry | unchanged |
| CI4 + MySQL 8 InnoDB | unchanged |

Proven by existing `LedgerReconciliationTest`, `AuthoritativeSampleDataTest`, `ImmediateInvoiceAndChallanLifecycleTest`, plus `Phase3SchemaTest::testFrozenLedgerAndApprovedWastageSurviveSchema`.

---

## 14. Commit hashes for each checkpoint

| Checkpoint | Hash | Subject |
|---|---|---|
| Plan v1 | `06532ab` | Document Phase 3 implementation plan |
| Plan v2 | `4231ab9` | Conditional-approval rewrite |
| **0 — approval, no code** | **`2fc6478`** | Record Phase 3 implementation approval |
| **A — schema / permission / 110 catalog** | **`810fd10`** | Additive schema, `INWARD.REVERSE`, 110-frame catalog |
| **A-fix (rollback order)** | **`7dfb935`** | Rollback order; nullable `rejected_at_utc` |
| **A-fix (refresh + seed columns)** | **`9763ea9`** | Drop history first; field cache; 067 lot identity |
| **B — INW-002/003 + coupled screens** | **`d88d495`** | Direct-post inward and operational services/views |
| **C — INW-004/005 tests** | **`0f50d3b`** | Return/Recovery and reversal tests |
| **D — DSP-001/002/003 tests** | **`91a7cae`** | Rejection and G2 tests |
| **E — WST-001/002/003 tests** | **`50b5f6b`** | Wastage approval/reversal tests |
| **F — 110-frame HTTP** | **`065d2fd`** | All 110 frames over HTTP |
| **G — auth, route scan, this report** | **`a80ac6a`** | Authorization HTTP + production-route + report |
| **G-fix — idempotent INWARD.REVERSE grants** | **`741926b`** | Permission/Role seeders skip existing rows |
| **H — closeout (mobile R1 + 403 POST)** | **`25ef02a`** | Drawer shell, 19-frame mobile QA, unauthorized reverse/correct 403. Store Keeper grant **not** changed. |

Application PHP for C–E is in **B** because `app/Config/Routes.php` cannot reference missing controllers.

---

## 15. Confirmation: Phase 4 was not started

No Phase 4 plan, screens, routes, migrations, permissions, or tests were added. README and `docs/PHASE3_PLAN.md` still say do not begin Phase 4. Closed C1–C5 / P1–P3 as in the corrected plan only.

---

## 16. Closeout correction (mobile shell + grant STOP)

Phase 3 remains complete. **Phase 4 was not started.** This section supersedes the earlier “stacked sidebar / no hamburger” mobile note.

### 16.1 STOP — `INWARD.REVERSE` seeded-role matrix was not changed

The closeout request asked for a conservative seed:

- Super Admin: grant
- Store Keeper / Plant Manager / Billing Clerk / Accounts Manager: do not seed

The approved plan already records a **different Business Owner-approved grant**. Per the closeout instruction, **nothing was changed**.

Quoted from `docs/PHASE3_PLAN.md` §11.2:

> `| INWARD.REVERSE grant | CSV INW-005 primary Store Keeper | Store Keeper + Super Admin (catalogue). Do not infer Plant Manager or Accounts Manager. |`

Final seeded matrix (unchanged):

| Role | `INWARD.REVERSE` |
|---|---|
| Super Admin | **Granted** (catalogue) |
| Store Keeper | **Granted** (approved §11.2 — CSV INW-005 primary) |
| Plant Manager | Not granted |
| Billing Clerk | Not granted |
| Accounts Manager | Not granted |

The permission remains in the catalogue for explicit administrator configuration. It is **not** inferred from `INWARD.ADD`, `OPENING_STOCK.ADD`, or any source-specific code. `INWARD.APPROVE` was not added.

To switch Store Keeper to “do not seed”, the Business Owner must supersede §11.2 in writing.

### 16.2 Files changed in this closeout

- `app/Views/components/nav/c_nav_01_shell.php` — hamburger `button.nav-toggle` + `.nav-scrim` (existing shell)
- `app/Views/components/nav/c_nav_02_sidebar.php` — `id="primary-nav"`
- `public/css/app.css` — closed drawer at `.viewport-mobile` and `@media (max-width: 767px)`; 44px toggle
- `public/js/app.js` — open/close drawer, scrim, Escape
- Phase 3 views — `.sticky-actions` only when a control exists; wastage list mobile sticky “Record wastage”
- `app/Views/pages/inward/register.php` + `app/Views/pages/inward/_reversal_actions.php` — same Reverse/Correct on desktop table and mobile cards
- `app/Controllers/InwardController.php` — unauthorized reverse/correct `forbidden()` → **403**
- Tests: `Phase3MobileFrameHttpTest`, `Phase3AuthorizationHttpTest`, `Phase3FrameHttpTest`, `Phase3ReturnAndReversalTest`
- `README.md`, this report

No new mobile shell or navigation component. No Phase 4 files.

### 16.3 Before / after mobile-shell evidence

| | Evidence |
|---|---|
| **Before** | Stacked Phase 0–2 sidebar consuming the 390×844 viewport |
| **After** | Mobile Header + hamburger; drawer `transform: translateX(-100%)`; `navRight = 0`; content `h1` immediately below header |

### 16.4 All 19 mobile-frame QA matrix (390×844)

Header type for every frame: **existing Mobile Header** (`header.top-header`). Navigation: **hamburger 44×44, drawer closed by default, no stacked sidebar**. Development `?frame=` shows the yellow preview banner (expected when `CI_ENVIRONMENT=development`). PHPUnit `testing` omits that banner. Horizontal overflow: **scrollWidth = 390** on every frame.

| Frame Name | Footer | Horizontal overflow | Screenshot QA |
|---|---|---|---|
| `M-INW-SCR-002-Create-Default` | Present (Save) | None | PASS |
| `M-INW-SCR-002-Create-Error` | Present (Save) | None | PASS |
| `M-INW-SCR-003-Create-Default` | Present (Save) | None | PASS |
| `M-INW-SCR-003-Create-Error` | Present (Save) | None | PASS |
| `M-INW-SCR-004-Create-Default` | Present (Save) | None | PASS |
| `M-INW-SCR-004-Create-Error` | Present (Save) | None | PASS |
| `M-INW-SCR-005-List-Default` | Absent (per-row Reverse/Correct on cards) | None | PASS |
| `M-INW-SCR-005-List-NoResults` | Absent | None | PASS |
| `M-DSP-SCR-001-Create-Default` | Present (Save) | None | PASS |
| `M-DSP-SCR-001-Create-Error` | Present (Save) | None | PASS |
| `M-DSP-SCR-002-Create-Default` | Present (Save) | None | PASS |
| `M-DSP-SCR-002-Create-Error` | Present (Save) | None | PASS |
| `M-DSP-SCR-003-Review-Default` | Present (Save) | None | PASS |
| `M-DSP-SCR-003-Review-PendingApproval` | Present (Save) | None | PASS |
| `M-WST-SCR-001-List-Default` | Present, mobile-only (Record wastage) | None | PASS |
| `M-WST-SCR-001-Create-Default` | Present (Save) | None | PASS |
| `M-WST-SCR-001-List-NoResults` | Present, mobile-only (Record wastage) | None | PASS |
| `M-WST-SCR-002-Review-PendingApproval` | Present (Approve / Return) | None | PASS |
| `M-WST-SCR-003-Review-PendingApproval` | Present (Reverse Wastage) | None | PASS |

Meaningful state (PHPUnit `Phase3MobileFrameHttpTest`, not HTTP 200 only):

| Frame | State content asserted |
|---|---|
| INW-002 Default | Title Plant Production Inward; “no approval workflow”; Save; no inward-compat banner |
| INW-002 Error | `banner-error` “Enter finished product”; Save |
| INW-003 Default | Opening Stock Entry; cutover lede; Save |
| INW-003 Error | “Enter material”; Save |
| INW-004 Default | Three entry paths; Save; no inward-compat copy |
| INW-004 Error | “Complete the entry path”; Save |
| INW-005 Default | Cards; `INW-2026-000230` Production; Reverse+Correct **disabled** with exact CH-2026-000481 reason (Super Admin holds `INWARD.REVERSE`); no page sticky footer |
| INW-005 NoResults | “No inward entries match these filters.”; Reverse/Correct absent |
| DSP-001 Default | Available quantity does not increase; Save |
| DSP-001 Error | “Select a challan line”; Save |
| DSP-002 Default / Error | Rejection Reassignment Entry; error needs replacement client |
| DSP-003 Default / PendingApproval | `REJ-2026-000044` **FULLY_DISPOSED**; 3.000+7.000+2.000; no Reverse; permanent-history lede; overlay does **not** fake a pending disposition status |
| WST-001 List Default | `WST-2026-000067` **APPROVED**; Record wastage sticky |
| WST-001 Create | Record wastage; Save |
| WST-001 NoResults | Empty copy; Record wastage sticky still present |
| WST-002 PendingApproval | `PENDING_APPROVAL` badge; Approve present |
| WST-003 PendingApproval | Reverse Wastage present |

No `M-*` Loading or PermissionRestricted Frame Names exist in the 110 CSV (those states are desktop). Loading skeleton and PermissionRestricted Reverse/Correct **removal** are asserted on the desktop frames (`D-INW-SCR-005-List-PermissionRestricted` omits Reverse/Correct entirely).

### 16.5 Permission and direct-POST results

| Check | Result |
|---|---|
| Super Admin sees Reverse/Correct | PASS (`testSuperAdminSeesReverseAndCorrect`) |
| Store Keeper sees Reverse/Correct | PASS — **approved §11.2 grant retained** (not the conservative “do not seed” matrix) |
| Plant Manager cannot see or POST | PASS — GET omits Reverse/Correct; POST reverse **403**; POST correct **403** |
| Billing Clerk POST | PASS — no grant after role switch; POST **403** |
| Accounts Manager POST reverse/correct | PASS — **403** |
| Company/plant scope independent of permission | PASS — Patil with `INWARD.REVERSE` but grants deleted → `SCOPE_DENIED:company` |
| Mandatory reason | PASS — blank reason → `INW_REVERSE_REASON` |
| Allocated inward blocked | PASS — `INW_REVERSE_BLOCKED` names `CH-2026-000481` |
| Second reversal blocked | PASS — `INW_ALREADY_REVERSED` |
| Idempotent replay | PASS — same key, one replacement row |
| Correction creates linked `inward_entry` | PASS — `replaces_inward_entry_id` |
| No in-place edit of posted inward | PASS — original quantity unchanged after reverse/correct |
| PermissionRestricted removes Reverse/Correct | PASS — desktop restricted frame |

### 16.6 Frozen regression results

| Identity | Result |
|---|---|
| Ledger closing M30 Chakan | **116.000 MT** |
| Allocation | 18.000 + 12.000 = **30.000** MT |
| `LOT-2026-00230` remaining | **13.000** MT |
| Disposition `REJ-2026-000044` | **3.000 + 7.000 + 2.000 = 12.000** MT |
| `WST-2026-000067` | **Approved** |
| Invoice `INV-2026-000119` | **₹177,592.00** |
| Frame count | **110** = 91 desktop + 19 mobile |
| `INWARD.APPROVE` | **Not added** |
| Inward approval FSM | **None** |
| Disposition reversal | **None** |

### 16.7 Production-route safety scan

`ProductionRouteAbsenceTest` still passes: no `seed` / `fixture` / `demo` / `probe` / `/test` routes. No `/inward/*/approve`. No `/rejections/*/reverse`. Production ignores `?frame=`. Health JSON `"phase": 3`.

### 16.8 Phase 4

**Not started.** No Phase 4 plan, screens, routes, migrations, permissions, or tests.
