# Phase 3 plan (implementation not started)

**Status:** Corrected plan **approved for implementation** (12 Sep 2026). Implementation follows this document. Do not begin Phase 4.  
**Phase 0–2:** Approved and **closed**. Preserve every existing route, seed quantity, ledger figure, permission catalogue (except the single additive `INWARD.REVERSE`), and passing test.  
**Stack (locked):** CodeIgniter 4, PHP 8.2+, MySQL 8.0.16+ InnoDB, server-rendered CI4 Views, progressive JavaScript only, Composer, CI4 migrations and seeders, database sessions, Spark + cron, PHPUnit, existing Phase 0–2 modular architecture.

Do not introduce React, Next.js, NestJS, PostgreSQL, JWT, localStorage repositories, mock APIs, Shield, or another framework.

**Closed Phase 1 wording (documentation only, already recorded):**  
Invoice approval is a separate action from Challan approval, but the Challan must be Approved first.

**Precedence for Phase 3:** this plan’s non-negotiable rules (including the 12 Sep 2026 decisions) → BRD v0.9.22 → approved addenda (DB-CHG-001–006) → Database Architecture v1.2 intent translated to MySQL 8 InnoDB → TDD v0.4.1 if no BRD contradiction → FRIS v1.0.1 → Blueprint/Figma → CSV Frame Names.

DB-CHG-006 is **approved**. Its approval document points to **Database Architecture v1.2** as the authoritative implementation specification for Return/Recovery columns and posting.

Where FRIS or Blueprint page titles contradict the non-negotiable rules in this plan, this plan wins. CSV **Frame Name** strings remain the visual coverage contract.

**Authoritative CSV:** `uploads/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1_4e22.csv` (product-wide 606 frames; filename is historical).

**UI slice:** ten Screen IDs — INW-SCR-002, INW-SCR-003, INW-SCR-004, INW-SCR-005, DSP-SCR-001, DSP-SCR-002, DSP-SCR-003, WST-SCR-001, WST-SCR-002, WST-SCR-003.

---

## Approved decisions (12 Sep 2026) — binding

These replace the earlier C1–C5 proposals, the INW-004 approval FSM, and the `INWARD.APPROVE` provenance row.

| ID | Decision |
|---|---|
| **C1** | Return/Recovery is a **direct-post inward** with a mandatory Save-time reusability decision. Three entry paths (challan line, DSP-003 disposition, non-customer recovery). Authoritative fields and types in §5.1 / §8.1. Do **not** use `REJECTION_RECOVERY` or `RETURNED_TO_AVAILABLE` for this source. |
| **C2** | Plant Production Inward records **finished-product quantity only**. Recipe grid is read-only informational. **No** automatic RM consumption and **no** manufacturing-consumption engine in Phase 3. |
| **C3** | Keep `WST-2026-000067` **Approved**. Ledger identity: `120.000 + 25.000 − 30.000 + 3.000 − 2.000 = 116.000 MT`. Other visual states use testing fixtures / development-only visual-state data under an **environment guard**. No fake production records. Do not mutate 067. |
| **C4** | DSP-SCR-003 `Review-Reversed` is **compatibility coverage only**. Disposition records are permanent history. **No** disposition reversal, **no** Reverse action, **no** fabricated `REVERSED` status, **no** new disposition-reversal permission. Downstream wastage reversal remains. |
| **C5** | DSP-SCR-001 Save creates `movement_type = REJECTION_RECOVERY` into `REJECTED_PENDING`. It must **not** increase Available. Available increases only via DSP `RETURNED_TO_AVAILABLE` or a **reusable** `RETURN_RECOVERY_INWARD` Save. |
| **P1** | **Do not add `INWARD.APPROVE`.** There is no inward approval FSM on Vendor, Plant Production, Opening Stock, or Return/Recovery. |
| **P2** | **Add `INWARD.REVERSE` only.** No other new permission codes. Unified INW-SCR-005 Reverse/Correct. PermissionRestricted **removes** the action. |
| **P3** | INW-002, INW-003, and INW-004 approval-named CSV frames are **compatibility-only** with the exact inward sentence in §2.2 / §2.5. |

Frozen regressions (must not change): **110** CSV frames (91 desktop + 19 mobile); ledger **116.000 MT**; allocation **18.000 + 12.000 = 30.000 MT**; LOT-2026-00230 remaining **13.000 MT**; disposition **3.000 + 7.000 + 2.000 = 12.000 MT**; invoice **₹177,592.00**; existing Phase 0–2 schemas, routes, permissions (plus only `INWARD.REVERSE`), tests, and behaviour; CI4 + PHP 8.2+ + MySQL 8 InnoDB; **additive migrations only**.

---

## 1. Exact manifest frame inventory by Screen ID and breakpoint

**Phase 3 total: 110 frames. Desktop 91. Mobile 19. Tablet 0.**

Tablet count is zero on every Screen ID. Blueprint: the responsive rule fully describes tablet adaptation; no separate T frame is drawn.

**None of the 12 approved frame-name aliases apply.** Those aliases are RPT-SCR-003/006/012, INV-SCR-008/012, and AUD-SCR-001 only. All 110 Phase 3 Frame Names are **direct CSV matches**. Do not rename them. Do not invent extra application frames.

CSV QA Status on all 110 rows: Not started. Build priority: 88 P1 + 22 P2 (INW-SCR-002 and INW-SCR-003 only).

Frame IDs: **FR-0228–FR-0337** (contiguous after Phase 1 INW-SCR-001 FR-0217–0227).

| Screen ID | Approved function | CSV Screen Name | Template | D | M | T | Total | Frame IDs | Priority |
|---|---|---|---|---:|---:|---:|---:|---|---|
| INW-SCR-002 | Plant Production Inward | Plant Production Inward | T03 | 9 | 2 | 0 | **11** | FR-0228–0238 | P2 |
| INW-SCR-003 | Opening Stock Entry | Opening Stock Entry | T03 | 9 | 2 | 0 | **11** | FR-0239–0249 | P2 |
| INW-SCR-004 | Material Return / Recovery Entry | Material Return / Recovery Entry | T03 | 13 | 2 | 0 | **15** | FR-0250–0264 | P1 |
| INW-SCR-005 | Inward Register and Reversal | Inward Register and Reversal | T01 | 7 | 2 | 0 | **9** | FR-0265–0273 | P1 |
| DSP-SCR-001 | Customer Rejection Entry | Customer Rejection Entry | T03 | 9 | 2 | 0 | **11** | FR-0274–0284 | P1 |
| DSP-SCR-002 | Rejection Reassignment Entry | Rejection Reassignment Entry | T03 | 9 | 2 | 0 | **11** | FR-0285–0295 | P1 |
| DSP-SCR-003 | Disposition Resolution | Disposition Resolution | T04 | 10 | 2 | 0 | **12** | FR-0296–0307 | P1 |
| WST-SCR-001 | Wastage Register and Entry | Wastage Register and Entry | T01 | 10 | 3 | 0 | **13** | FR-0308–0320 | P1 |
| WST-SCR-002 | Wastage Review and Approval | Wastage Review and Approval | T04 | 8 | 1 | 0 | **9** | FR-0321–0329 | P1 |
| WST-SCR-003 | Wastage Reversal | Wastage Reversal | T04 | 7 | 1 | 0 | **8** | FR-0330–0337 | P1 |
| **Total** | | | | **91** | **19** | **0** | **110** | FR-0228–0337 | |

### 1.1 Exact CSV Frame Names (110)

**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`.

### 1.2 Overlay contract (shared components, not extra Screen IDs)

| Overlay | Screens | Use |
|---|---|---|
| OV-05 Confirmation | Not an inward-approval overlay. Optional Save confirmation for a **posted** reusable Return/Recovery is operational success, not an approval decision. Do not wire OV-05 as “Approve Return”. |
| OV-06 Mandatory Reason | INW-005 Reverse/Correct; WST-002 Return for Correction; WST-003 Reverse Wastage | Mandatory reason. **Not** used for inward Return-for-Correction (no inward approval FSM). **Not** used for DSP-003 Reversed (compatibility; no reverse action). |
| OV-07 Filter Drawer | INW-005, WST-001 | List filters |
| OV-11 Concurrency | INW-002/003/004, DSP-001/002/003, WST-002/003 | `row_version` conflict; Reload |
| OV-15 Attachment Preview | WST-001 Create-Draft | Optional evidence preview |

CSV component columns on WST-001 Create frames repeat the T01 list set. Implementation still instances **Input, File-Upload, and Attachment-Row** on Create as specified by Blueprint P17 / UX-DEC-008. Frame **names** stay CSV-exact.

---

## 2. Exact identities of INW-SCR-002, INW-SCR-003, and INW-SCR-004

Confirmed from CSV Screen Name + Prompt Pack P14/P15 + Phase 1 plan §2 + 12 Sep 2026 decisions.

| Screen ID | Authoritative identity | FRIS | `inward_entry.source_type` | `material_movement.movement_type` | Posts on | Approval FSM |
|---|---|---|---|---|---|---|
| **INW-SCR-002** | **Plant Production Inward** | 11.4 | `PLANT_PRODUCTION` | `PLANT_PRODUCTION_INWARD` | Save | **None** |
| **INW-SCR-003** | **Opening Stock Entry** | 11.5 | `OPENING_STOCK` | `OPENING_STOCK_INWARD` | Save | **None** |
| **INW-SCR-004** | **Material Return / Recovery Entry** | 11.14.1 | `RETURN_RECOVERY` | `RETURN_RECOVERY_INWARD` (reusable Save only) | Save (reusability decision) | **None** |

INW-002 and INW-003 are the same class of direct-post inward as Phase 1 **INW-SCR-001 Vendor Material Inward**. INW-004 is the same **no-approval** class: reusability is a **Save-time** decision, not a separate approval workflow.

### 2.1 Direct-post rules (002, 003, and 004)

- Vendor, Plant Production, Opening Stock, and Return/Recovery have **no** inward approval FSM.
- **Do not add `INWARD.APPROVE`.** That code would create a false workflow.
- No stored `PENDING_APPROVAL` / `APPROVED` / `RETURNED_FOR_CORRECTION` on `inward_entry` for any of these types.
- No Approve or Return for Correction controls on these screens.
- CSV “Save draft / Submit or approve” is **template language**. Sticky action is **Save**. Create-Draft is a populated **pre-save** working copy, not a persisted DRAFT status.
- Company/Plant scope via existing `ScopeService`. Selectors: **active** Products / Raw Materials / Units only. Historical rows still display inactive FKs.
- UTC `DATETIME(6)` storage; Company timezone display (`DisplayTime`, Asia/Kolkata for Apex samples).
- Never physically delete an Inward. Never edit a posted inward in place (correction = new replacement row; §10).
- Use BCMath/`Decimal` canonical strings only. Never PHP floats.

INW-002/003 Save creates `inward_entry`, lot identity (`inward_entry_id` = `batch_lot_id`), the matching inward movement, MLB-1 Available increase, and `audit_event` in **one** `READ COMMITTED` transaction.

INW-004 Save is specified in §8.1 (reusable posts `RETURN_RECOVERY_INWARD` immediately; not reusable does not increase Available).

### 2.2 Compatibility frames for INW-SCR-002 and INW-SCR-003

Exact copy, already in `FrameCatalog::INWARD_COMPAT_COPY`:

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

| FR | Frame | Treatment |
|---|---|---|
| FR-0232 | `D-INW-SCR-002-Review-PendingApproval` | Compatibility only. Fixture is a **posted** production inward. Never store pending-approval. |
| FR-0233 | `D-INW-SCR-002-Review-Approved` | Compatibility only. |
| FR-0234 | `D-INW-SCR-002-Review-ReturnedForCorrection` | Compatibility only. |
| FR-0243 | `D-INW-SCR-003-Review-PendingApproval` | Compatibility only. Fixture is a **posted** opening-stock inward. |
| FR-0244 | `D-INW-SCR-003-Review-Approved` | Compatibility only. |
| FR-0245 | `D-INW-SCR-003-Review-ReturnedForCorrection` | Compatibility only. |

Must not appear on those frames: Approve, Return for Correction, approval-status badge, Submit-for-approval, or a stored false approval status.

Operational (not compatibility): Create-Default/Draft/Error/Loading, Review-ConcurrentUpdate (OV-11), Review-PermissionRestricted (missing `INWARD.ADD` / `OPENING_STOCK.ADD` / `INWARD.VIEW` — **no** fake decision-permission chrome), both mobile Create frames.

### 2.3 INW-SCR-002 recipe display (C2 approved)

Plant Production Inward records **produced Finished Product quantity only**.

The recipe grid is **read-only and informational** (Beta + M30 sample: OPC 53 Cement 0.350 MT, 20 mm Aggregate 0.700 MT, M-Sand 0.650 MT, scaled to produced quantity with BCMath). It is not independently editable.

Saving Plant Production Inward **must not** automatically consume Raw Materials or create Raw Material stock movements. **No manufacturing-consumption engine** is introduced in Phase 3.

INW-002 has no Cancel permission on the type (P14 / OD-B3-06). Reversal/correction is **only** via INW-005 when authorised by `INWARD.REVERSE`.

### 2.4 INW-SCR-003 cutover

One-time cutover. As-at date has a restricted range; out-of-range entry states the cutover reason (P14). Existing seed opening lots `INW-2026-000200` (102.000 MT) and `INW-2026-000199` / `LOT-2026-00212` (18.000 MT) remain. New opening-stock posts must not rewrite those rows.

CSV Primary Role for 002 and 003 is Plant Manager. Mapping onto existing `INWARD.ADD` / `OPENING_STOCK.ADD` is a **role_permission grant**, not a new catalogue code (§11.2).

### 2.5 Compatibility frames for INW-SCR-004 (no-approval principle)

Apply the **same no-approval principle** as 002/003. CSV approval-named states are **compatibility coverage**, not an inward approval FSM.

Exact copy (same sentence):

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

| FR | Frame | Treatment |
|---|---|---|
| FR-0252 | `D-INW-SCR-004-Create-PendingApproval` | Compatibility only. Do not persist pending-approval. No Approve Return control. |
| FR-0253 | `D-INW-SCR-004-Create-Approved` | Compatibility only. Do not store `APPROVED`. Reusable posting is shown on operational Save/success, not as an approval state. |
| FR-0254 | `D-INW-SCR-004-Create-ReturnedForCorrection` | Compatibility only. No OV-06 inward return-for-correction. |
| FR-0259 | `D-INW-SCR-004-Review-PendingApproval` | Compatibility only. |
| FR-0260 | `D-INW-SCR-004-Review-Approved` | Compatibility only. |
| FR-0261 | `D-INW-SCR-004-Review-ReturnedForCorrection` | Compatibility only. |

Must not appear: Approve, Approve Return, Return for Correction, approval-status badge, Submit-for-approval, or stored false approval statuses.

Operational INW-004 frames: Create-Default (reusable path form), Create-Draft (not-reusable / routed to wastage **form values**, pre-save), Create-Error, Create-Loading, Create-PermissionRestricted (`INWARD.ADD` absent — Save **removed**), Review-ConcurrentUpdate, Review-PermissionRestricted (`INWARD.VIEW`/`ADD` absent — no fake decision chrome), both mobile Create frames.

Reusability is a **mandatory Save-time field** (`reusability_confirmed`) for any role that can Save (`INWARD.ADD`). It is not gated on a non-existent Approve permission. Blueprint UX-DEC-006/007 “approver confirms reusability / approval required before Available” is **withdrawn** for implementation by the 12 Sep 2026 decision.

---

## 3. Requirement traceability matrix

| Screen | CSV / FRIS | BRD / addenda / TDD / DB Arch | Blueprint / Figma (P14–P17) | Phase 3 behaviour |
|---|---|---|---|---|
| INW-002 | FRIS 11.4; Inward — Create | Inward approval removed; `PLANT_PRODUCTION` / `PLANT_PRODUCTION_INWARD` | T03; posts on Save; derived recipe grid | Direct post; recipe display-only (C2); compatibility Review-* |
| INW-003 | FRIS 11.5; Inward — Opening Stock | `OPENING_STOCK` / `OPENING_STOCK_INWARD`; `OPENING_STOCK.ADD` | T03; cutover date range | Direct post; compatibility Review-* |
| INW-004 | FRIS 11.14.1; Return — Create | **DB-CHG-006** → Database Architecture v1.2 fields; `RETURN_RECOVERY` / `RETURN_RECOVERY_INWARD` | T03 approval-named frames exist | **No approval FSM.** Reusability on Save. Compatibility for approval-named frames. Three entry paths (§8.1). |
| INW-005 | FRIS 11.14.2, 11.16; Inward — View / Inward — Reverse | Four source types; compensating `REVERSAL`; correction = new row | Reverse disabled when allocated, naming challan | Register + Reverse/Correct (`INWARD.REVERSE`) |
| DSP-001 | FRIS 4.1–4.4; Rejection — Create | REJ-CUM; C5 | Save creates rejection | `REJECTION_RECOVERY` → `REJECTED_PENDING`; Available unchanged |
| DSP-002 | FRIS 4.1–4.4; Disposition — Reassign | Exclusive paths; DSP-ERR-018 | Replacement client/site | `REASSIGNMENT_RESERVATION`; ordered locks |
| DSP-003 | FRIS 4.1–4.5 | **G2** / DSP-BR-001; `RETURNED_TO_AVAILABLE`; C4 | Exclusive paths; DSP-ERR-017; Reversed frame | Operational reconciliation; Reversed = compatibility only |
| WST-001 | FRIS 12.3; Wastage — View / Create | USER_RECORDED vs SYSTEM_EXPIRY; UX-DEC-008 | Origin column; optional evidence | Create does **not** reduce inventory |
| WST-002 | FRIS 12.3; Wastage — Approve | Inventory reduces only on USER_RECORDED Approve; C3 | Effect notice | Atomic approve; 067 stays Approved |
| WST-003 | FRIS 12.3; Wastage — Reverse | Compensating movement; restore source lot | **Wastage Reversal** / **Reverse Wastage** | Supported downstream reversal; not disposition reversal |

Closed Phase 1 decisions that Phase 3 must not reopen: Final Decision 3 (`RETURNED_TO_AVAILABLE`, not Return to Vendor); SYSTEM actor; SYSTEM_EXPIRY insert order; MLB-1; G2 meaning; challan number on first Save; Invoice approval separate from Challan approval.

---

## 4. Existing Phase 1/2 table and service gap analysis

Phase 1 **explicitly** seeded DSP/WST **without screens or routes**. That seed-only backend must become a **complete operational domain** in Phase 3. Do not rebuild Phase 0–2 tables.

### 4.1 Already present (reuse)

| Object | Status | Gap |
|---|---|---|
| `inward_entry` | `source_type` CHECK includes `OPENING_STOCK`,`VENDOR_INWARD`,`PLANT_PRODUCTION`,`RETURN_RECOVERY`. Vendor fields exist (DB-CHG-005). | **No** DB-CHG-006 Return/Recovery columns yet. Additive ALTER only (§5.1). **Do not** add approval-status columns. |
| `material_movement` | Types include `PLANT_PRODUCTION_INWARD`, `OPENING_STOCK_INWARD`, `RETURN_RECOVERY_INWARD`, `REJECTION_RECOVERY`, `RETURNED_TO_AVAILABLE`, `REASSIGNMENT_RESERVATION`, `WASTAGE_FROM_*`, `REVERSAL` | Reverse uses existing `REVERSAL` + `linked_original_movement_id`. Do not substitute `REJECTION_RECOVERY` / `RETURNED_TO_AVAILABLE` for Return/Recovery inward. |
| `material_lot_balance` | MLB-1 CHECKs ≥ 0; `LotBalanceService::applyDelta` | Reuse. Lock `(company_plant_id, product_id, batch_lot_id)`. |
| `rejected_dispatch` | Seed `REJ-2026-000044` 12.000 MT `FULLY_DISPOSED` | Additive reason/remarks/date/`row_version`. No `REVERSED` status (C4). |
| `disposition_record` | Paths `RETURNED_TO_AVAILABLE`, `REASSIGN_TO_PRODUCT`, `RECORD_AS_WASTAGE`; 3+7+2 seeded | Permanent history. No reversal column. Optional G2 projection. |
| `reassignment` | Thin (`notes` only) | Missing replacement client/site FKs required by DSP-002. Additive. |
| `wastage_record` | USER_RECORDED + SYSTEM_EXPIRY; statuses include DRAFT/PENDING/RETURNED/APPROVED/REVERSED | Additive `product_id`, `batch_lot_id`, `unit_id` so pending USER_RECORDED can show Source Lot / Origin **before** `movement_id` exists. No evidence table. |
| `wastage_approval_history` | SAVED / SAVE_AND_APPROVED / APPROVED / RETURNED / REVERSED | Reuse (wastage FSM only). |
| `ExpiryProcessor` + `inventory:process-expiry` | SYSTEM_EXPIRY inserted Approved by SYSTEM | Preserve. |
| `InwardService::postVendorInward` | Vendor only | Add production, opening, return/recovery Save. Do not change vendor posting. |
| `PermissionCatalog` | `INWARD.LIST/VIEW/ADD/EDIT/CANCEL`, `OPENING_STOCK.ADD`, `WASTAGE.*`, `DISPATCH.*` | Add **only** `INWARD.REVERSE`. Do **not** add `INWARD.APPROVE`. |
| Routes | `/inward/vendor` only | No production/opening/return/register, no DSP, no WST UI. |
| Attachments | None | Additive evidence table. |
| Idempotency store | Retry flag on `TransactionManager` only | Additive command table for reverse/correct/wastage approve/disposition save. |

### 4.2 Seed-only domain that must become operational

| Domain | Seed today | Missing application services |
|---|---|---|
| Plant production inward | `INW-2026-000230` / `LOT-2026-00230` 25.000 MT posted | `InwardService::postPlantProduction` (FP qty only; no RM movements) |
| Opening stock | `INW-2026-000200`, `INW-2026-000199` | `InwardService::postOpeningStock` |
| Return/Recovery | `source_type` allowed; **no** RETURN_RECOVERY sample row; ledger +3 is **DSP** `RETURNED_TO_AVAILABLE`, not INW-004 | `ReturnRecoveryService::save` (reusability on Save; no approve method) |
| Inward register / reverse | Vendor + production + opening rows exist | `InwardQuery` + `InwardReversalService` (Reverse/Correct) |
| Customer rejection | `REJ-2026-000044` + `REJECTION_RECOVERY` 12.000 on `LOT-2026-00212` | `RejectionService` |
| Reassignment | `reassignment` row + `REASSIGNMENT_RESERVATION` 7.000 | `ReassignmentService` |
| Disposition | Three `disposition_record` rows, Fully Disposed | `DispositionService` with G2; **no** reverse method |
| USER_RECORDED wastage | `WST-2026-000067` already **APPROVED** | `WastageService` create/submit/approve/return/reverse |
| SYSTEM_EXPIRY | Job inserts Approved | List/review only; **no** Approve control |

### 4.3 DSP-001 inventory on Save (C5 approved)

DSP-SCR-001 Save creates:

- `rejected_dispatch` row (no physical Delete afterwards)
- append-only `material_movement` with `movement_type = REJECTION_RECOVERY`
- destination bucket `REJECTED_PENDING` on the **source allocation lots** (preserve lot identity)
- MLB-1 `rejected_pending_qty` increase; **Available unchanged**

Prompt Pack P16 “no inventory movement” is treated as **no Available restock**. Available increases only through:

1. an explicit DSP-003 **Return to Availability** (`RETURNED_TO_AVAILABLE`), or
2. a **reusable** INW-004 Save (`RETURN_RECOVERY_INWARD`).

DSP-001/002 CSV Review-PendingApproval / Approved / ReturnedForCorrection names are T03 leftovers. They are **not** an approval FSM. Map them to operational rejection/reassignment states (Pending Disposition / posted / read-only). Do **not** store false approval statuses. Do **not** show Approve or Return for Correction. Do **not** reuse the INW compatibility sentence on DSP (that sentence is for direct-post **inward** types).

DSP-003 Review-PendingApproval / Approved / Draft names map to **disposition progress** (undisposed / partial / fully disposed), not an approver workflow.

DSP-003 Review-Reversed: §8.2 (C4).

---

## 5. Additive migration plan

Do **not** rebuild or drop Phase 0–2 tables. Additive `ALTER` / new tables only. Document any projection in `docs/PROJECTION_TABLES.md` **before** writing that migration.

| Step | Change | Notes |
|---:|---|---|
| 1 | `ALTER inward_entry` DB-CHG-006 fields | §5.1. Vendor/production/opening Save leave them NULL. **No** `return_status` / `approved_by` approval columns. |
| 2 | `ALTER inward_entry` Reverse/Correct linkage | `replaces_inward_entry_id` (FK self, NULL unless this row is a correction replacement); reversal recorded via movement + append-only history — **not** by editing original qty. |
| 3 | `ALTER rejected_dispatch` | `rejection_reason`, `remarks`, `rejected_at_utc DATETIME(6)`, `row_version`. Do **not** add `REVERSED` to `disposition_status`. |
| 4 | `ALTER reassignment` | `client_company_id`, `client_site_id` FKs RESTRICT; `reassigned_at_utc`; `row_version`. |
| 5 | `ALTER wastage_record` | Additive `product_id`, `batch_lot_id`, `unit_id` (RESTRICT). Do not break SYSTEM_EXPIRY insert order. |
| 6 | `evidence_attachment` | Parent `INWARD_ENTRY` / `WASTAGE_RECORD`; local `writable/uploads/evidence/`; optional; never gates wastage approval. |
| 7 | `idempotent_command` | UNIQUE (`command_type`,`resource_id`,`idempotency_key`). Types: `INWARD_REVERSE`, `INWARD_CORRECT`, `REJECTION_SAVE`, `REASSIGN_SAVE`, `DISPOSITION_SAVE`, `WASTAGE_APPROVE`, `WASTAGE_REVERSE`. **No** `RETURN_APPROVE`. |
| 8 | `inward_reversal_history` | Append-only: original id, reversal movement id, replacement inward id (nullable), mandatory reason, actor, active_role, `acted_at_utc`. |
| 9 | Generated UNIQUE | At most one `REVERSAL` movement per original `movement_id` (`reversal_of_key`). |
| 10 | G2 projection (optional, after PROJECTION_TABLES.md) | §6. |
| 11 | Permission rows | Insert **`INWARD.REVERSE` only** into `permission` + `role_permission`. Super Admin receives it because it is in the catalogue after the add, not as a reason to invent further codes. |
| 12 | CHECK widen only if needed | Do not remove existing CHECKs. Do not add inward approval CHECKs. |

No physical DELETE migrations. No new document-type enum that would allow Wastage/Challan number reuse.

### 5.1 Approved `inward_entry` Return/Recovery columns (DB-CHG-006 / Database Architecture v1.2)

Do **not** restrict Material Return/Recovery to `CHALLAN_LINE` and `REJECTED_DISPATCH` only.

Additive columns (NULL on non-`RETURN_RECOVERY` rows except where a CHECK requires pairing):

| Column | Rule |
|---|---|
| `original_challan_id` | Set for a direct customer return from an eligible original Challan. Paired with `original_challan_line_id` where applicable. |
| `original_challan_line_id` | Paired with `original_challan_id` where applicable. FK `challan_line`. |
| `original_invoice_id` | Optional linkage when the return traces to an invoice. FK `invoice_header` RESTRICT. |
| `original_credit_note_id` | Optional linkage when a credit note exists. FK only if credit-note tables exist; otherwise nullable with no FK until that module. Do not create credit-note tables solely for this column. |
| `client_id` | Set when the return is a customer return. May be NULL for legitimate **non-customer recovery**. |
| `return_reason` | Mandatory text/code on Save. |
| `reusability_confirmed` | TINYINT(1) NOT NULL on `RETURN_RECOVERY` rows. **Mandatory Save-time decision.** CHECK `IN (0, 1)`. |
| `disposition_record_id` | Populated **only** for a return originating from DSP-SCR-003. FK `disposition_record` RESTRICT. NULL on challan-line returns and non-customer recoveries. |

Pairing CHECK (MySQL): when either original challan field is non-NULL, **both** `original_challan_id` and `original_challan_line_id` are non-NULL, and the line belongs to that challan (service-validated under lock; optional generated pair key).

`disposition_record_id` CHECK: NULL unless this inward is disposition-originated.

Non-customer recovery: identifiable source in `return_reason` / remarks / evidence; customer document fields may all be NULL; `client_id` may be NULL.

`row_version` already exists.

**Forbidden on `inward_entry`:** `return_status`, `approved_by`, `approved_at_utc`, or any inward approval enum.

---

## 6. G2 / MySQL constraint-equivalence design

**G2 (do not rename):** combined disposition quantity never exceeds (and at Fully Disposed equals) `rejected_quantity`; a unit is never disposed twice. DSP-BR-001. Paths exclusive: Return to Availability, Reassign to Product, Record as Wastage.

**Do not use “Return to Vendor.”** Domain/path/movement: `RETURNED_TO_AVAILABLE`.

This DSP path is **distinct** from INW-004 `RETURN_RECOVERY_INWARD`. Do not substitute one for the other. Seeded +3.000 MT remains DSP `RETURNED_TO_AVAILABLE` on `LOT-2026-00212`.

MySQL cannot copy the PostgreSQL same-table constraint trigger (error 1442). Equivalence (all required):

1. **Transaction** — `TransactionManager` `READ COMMITTED`.
2. **Deterministic locks** — `SELECT … FOR UPDATE` on `rejected_dispatch` (PK) then `disposition_record` by `disposition_record_id` ASC, then affected `reassignment`, then `wastage_record` if RECORD_AS_WASTAGE, then MLB-1 keys `(company_plant_id, product_id, batch_lot_id)` ASC.
3. **Revalidate after lock** — eligible remaining = rejected − SUM(existing disposition qty); new paths ≤ remaining; BCMath.
4. **Service/domain** — exclusive path per unit; DSP-ERR-017 when sum > rejected (name all three entered quantities plus rejected).
5. **Optional projection** (only after `docs/PROJECTION_TABLES.md` entry): table `rejected_dispatch_g2` PK `rejected_dispatch_id`, `disposed_qty DECIMAL(14,4)` CHECK `disposed_qty >= 0`, CHECK against stored `rejected_qty_snapshot`. Written in the **same** transaction as `disposition_record` insert.

**REJ-CUM:** lock `challan_line` then existing `rejected_dispatch` for that line (id ASC); `SUM(rejected_quantity) + new <= dispatched_quantity`.

Fully Disposed iff `Decimal::cmp(sum(paths), rejected_quantity, 4) === 0`. Partial if `0 < sum < rejected`. Pending if sum = 0.

**No disposition reversal** (C4). G2 rows are permanent history.

Illustrative G2 close (seed `REJ-2026-000044`, must remain):

| Path | Qty | Movement |
|---|---:|---|
| Return to Availability | 3.000 MT | `RETURNED_TO_AVAILABLE` |
| Reassign to Product | 7.000 MT | `REASSIGNMENT_RESERVATION` |
| Record as Wastage | 2.000 MT | `WASTAGE_FROM_REJECTED` / `WST-2026-000067` |
| Total disposition | **12.000 MT** | Remaining **0.000 MT** |
| Final state | **Fully Disposed** | Wastage reference **WST-2026-000067** |

DSP-ERR-017 sample: sum exceeds rejected by **3.000 MT**; error names all three path quantities.

---

## 7. Quantity-bucket and movement-type matrix

Buckets: `AVAILABLE`, `REJECTED_PENDING`, `RESERVED_REASSIGNMENT`. Never negative. Never an Expired bucket (expiry writes wastage movements).

| Event | Movement type | From | To | Inventory effect | When |
|---|---|---|---|---|---|
| Plant production Save | `PLANT_PRODUCTION_INWARD` | NULL | AVAILABLE | +available (finished product only; **no RM movements**) | INW-002 Save |
| Opening stock Save | `OPENING_STOCK_INWARD` | NULL | AVAILABLE | +available | INW-003 Save |
| Vendor inward (existing) | `VENDOR_INWARD` | NULL | AVAILABLE | +available | Unchanged |
| Return/Recovery Save, **reusable** | `RETURN_RECOVERY_INWARD` | NULL | AVAILABLE | +available immediately | INW-004 Save |
| Return/Recovery Save, **not reusable** | — (no `RETURN_RECOVERY_INWARD`) | — | — | **Available unchanged**; qty routed to USER_RECORDED wastage workflow | INW-004 Save |
| Inward Reverse | `REVERSAL` | original to-bucket | NULL | inverse of original movement; `linked_original_movement_id` | INW-005 |
| Inward Correct | `REVERSAL` then new inward movement on the **replacement** row | as reverse + new post | as new post | Original not edited; replacement is a new `inward_entry` | INW-005 |
| Customer rejection Save | `REJECTION_RECOVERY` | NULL | REJECTED_PENDING | +rejected_pending; **Available unchanged** | DSP-001 Save |
| Return to Availability | `RETURNED_TO_AVAILABLE` | REJECTED_PENDING | AVAILABLE | exclusive qty | DSP-003 |
| Reassignment reserve | `REASSIGNMENT_RESERVATION` | REJECTED_PENDING | RESERVED_REASSIGNMENT | exclusive qty | DSP-002 / DSP-003 |
| Disposition wastage | `WASTAGE_FROM_REJECTED` | REJECTED_PENDING | NULL | USER_RECORDED Approved in same tx for this path | DSP-003 |
| USER_RECORDED wastage Save/Submit | — | — | — | **None** | WST-001 |
| USER_RECORDED Approve | `WASTAGE_FROM_AVAILABLE` / `_REJECTED` / `_RESERVED_REASSIGNMENT` | source bucket | NULL | −bucket | WST-002 |
| Not-reusable return wastage Approve | Existing wastage type; **no new movement_type** | NULL if qty never entered a bucket | NULL | **No Available increase**; no MLB-1 decrease if qty never entered a bucket | WST-002 |
| SYSTEM_EXPIRY | same wastage-from-* , reason `EXPIRED` | source bucket | NULL | Already Approved at insert | Job (preserve) |
| Wastage Reverse | `REVERSAL` | NULL | original source bucket | restore **exact source lot** | WST-003 |
| Disposition reverse | — | — | — | **Not implemented** | DSP-003 Reversed frame only |

Preserve quantity **and Unit** on every row. Unit is read-only from the product/RM master.

Allocation/dispatch (`ALLOCATION_DISPATCH`) unchanged. Do not edit or delete original movements.

---

## 8. State mapping for INW-004, DSP, and WST

### 8.1 INW-004 Return/Recovery (no approval FSM)

**Entry paths (all `source_type = RETURN_RECOVERY`):**

1. **Direct customer return** from an eligible original Challan line — `original_challan_id` + `original_challan_line_id` paired; `client_id` set; `disposition_record_id` NULL.
2. **Return originating from DSP-SCR-003** — `disposition_record_id` set; do **not** double-post the seeded 3.000 MT `RETURNED_TO_AVAILABLE`. A new INW-004 row from a disposition path is a **distinct** `RETURN_RECOVERY_INWARD` only when that path is an approved Return/Recovery inward, not a substitute for G2 `RETURNED_TO_AVAILABLE`.
3. **Legitimate non-customer recovery** with an identifiable source — customer document fields may be NULL; `return_reason` (and optional evidence) identify the source.

**Save-time reusability (`reusability_confirmed`) is mandatory:**

```
INWARD.ADD Save
  ├── Reusable (1)
  │     inward_entry
  │     + RETURN_RECOVERY_INWARD
  │     + MLB-1 Available increase
  │     + audit
  │     (one transaction)
  └── Not reusable (0)
        inward_entry
        + NO RETURN_RECOVERY_INWARD
        + NO Available increase
        + USER_RECORDED wastage workflow (pending; inventory still unchanged until Wastage Approve)
        + audit
```

Do **not** implement Approve / Return for Correction / pending-approval storage. Do **not** substitute `REJECTION_RECOVERY` or `RETURNED_TO_AVAILABLE` for this inward source.

Returned quantity must not exceed eligible source quantity (despatched on the challan line, or remaining on the disposition path, as applicable). Preserve Unit.

### 8.2 DSP rejection and disposition

```
DSP-001 Save
  → disposition_status = PENDING
      REJECTION_RECOVERY → REJECTED_PENDING
      Available unchanged
DSP-002 / DSP-003 path writes
  → PARTIALLY_DISPOSED when 0 < disposed < rejected
  → FULLY_DISPOSED when disposed = rejected
      statuses are only PENDING | PARTIALLY_DISPOSED | FULLY_DISPOSED
```

DSP-003 `D-DSP-SCR-003-Review-Reversed` (C4 — **compatibility coverage only**):

- Disposition records are **permanent history**.
- Render exact explanation: `Not applicable — disposition records are permanent history and cannot be reversed.`
- **No** Reverse action.
- **No** mutation control.
- **No** fabricated `REVERSED` disposition state in the database or seed.
- **No** new disposition-reversal permission.
- Downstream records may still use their own supported reversal (WST-003 Wastage Reversal).

CSV Review-ReturnedForCorrection on DSP-001/002/003: **no** stored return-from-approver. Read-only posted document without Return-for-Correction chrome.

### 8.3 WST USER_RECORDED (unchanged operational FSM)

Wastage **does** have an approval FSM. That is not an inward approval FSM.

```
Save / Submit (WASTAGE.ADD)
  → DRAFT or PENDING_APPROVAL
    Inventory unchanged.
Approve (WASTAGE.APPROVE)
  → APPROVED + wastage movement + MLB-1 reduction (when a bucket exists) + audit (one tx)
Return for Correction (WASTAGE.APPROVE + OV-06)
  → RETURNED_FOR_CORRECTION; edit does not reduce inventory
Reverse (WASTAGE.CANCEL_REVERSE + OV-06)
  → REVERSED + compensating REVERSAL restoring source lot/bucket
```

SYSTEM_EXPIRY: inserted `APPROVED` by SYSTEM. No Approve, no Return for Correction. WST-002 Review-Cancelled **is** the SYSTEM_EXPIRY row (CSV data scenario).

**C3 environment guard:** `WST-2026-000067` remains Approved in every environment’s operational seed. PendingApproval, ReturnedForCorrection, and other visual-state examples use `?frame=` / development-only visual-state data when `CI_ENVIRONMENT` is not `production`. Production **ignores** `?frame=` (existing Phase 2 rule). Those fixtures must not insert fake production `wastage_record` rows and must not modify 067.

WST-003 CSV Review-PendingApproval / ReturnedForCorrection are T04 leftovers, **not** a reversal-approval workflow:

| Frame | Operational meaning |
|---|---|
| WST-003 Review-PendingApproval | Approved USER_RECORDED, **eligible to reverse** |
| WST-003 Review-Approved | Approved, not reversed; Reverse visible if `WASTAGE.CANCEL_REVERSE` |
| WST-003 Review-ReturnedForCorrection | Compatibility / read-only wastage that is RETURNED_FOR_CORRECTION from WST-002. **Do not** use “Write-Off Review”. |
| WST-003 Review-Reversed | Already reversed; Reverse **visible+disabled** with full reason (status-blocked). Permission absence **removes** Reverse. |
| WST-003 PermissionRestricted | Reverse control **absent** |

Labels: **Wastage Reversal**, **Reverse Wastage** only.

---

## 9. Transaction and deterministic row-lock matrix

All writes: `TransactionManager`, isolation `READ COMMITTED`, `Decimal` only, `AuditWriter` in the same transaction. Re-check Company/Plant scope after locks. Revalidate eligibility after lock. Deadlock retry **only** when the boundary is marked idempotent (max 3).

Global lock order (lowest key first):  
`document_sequence` (if numbering) → `challan_header` → `challan_line` id ASC → `rejected_dispatch` → `disposition_record` id ASC → `reassignment` → `inward_entry` (lot) id ASC → `material_lot_balance` (`company_plant_id`, `product_id`, `batch_lot_id`) → `wastage_record` → `idempotent_command` unique insert.

| Tx | Locks | Writes (same tx) | Failure |
|---|---|---|---|
| INW-002 Save | company_plant; finished product; unit; recipe **read-only** | inward_entry; `PLANT_PRODUCTION_INWARD`; MLB-1; audit. **No** RM movements | Inactive product/unit; scope; OV-11 |
| INW-003 Save | company_plant; product; unit | same pattern with `OPENING_STOCK_INWARD` | Cutover date; `OPENING_STOCK.ADD` |
| INW-004 Save reusable | source challan_line and/or disposition_record; lots | inward_entry; `RETURN_RECOVERY_INWARD`; MLB-1 Available; audit | Qty > eligible; missing reusability |
| INW-004 Save not reusable | source docs; wastage numbering | inward_entry (`reusability_confirmed=0`); pending USER_RECORDED wastage; **no** Available; audit | Missing reason; no `RETURN_RECOVERY_INWARD` |
| INW-005 Reverse | original inward; allocation_trace_link; downstream rejection/wastage/disposition refs; MLB-1; original movement (share) | one `REVERSAL`; MLB-1 inverse; history; audit | Downstream deps; already reversed; missing reason |
| INW-005 Correct | same as Reverse + masters for replacement | Reverse writes **plus** new `inward_entry` (`replaces_inward_entry_id`) + its posting movement | Same; replacement validation |
| DSP-001 Save | challan_line; existing rejections; allocation lots MLB-1 | rejected_dispatch; `REJECTION_RECOVERY`; MLB-1 rejected_pending; audit | REJ-CUM exceed |
| DSP-002 Save | rejected_dispatch; disposition rows; MLB-1 | disposition REASSIGN; reassignment; RESERVATION; MLB-1; G2; audit | Over-reserve; DSP-ERR-018 |
| DSP-003 Save | same + wastage numbering if wastage path | exclusive path rows; movements; maybe WST consume; MLB-1; G2; status; audit | DSP-ERR-017 |
| DSP-003 reverse | — | **None.** No route. | Compatibility frame only |
| WST-001 Save | lot MLB-1 (validate only) | wastage_record DRAFT/PENDING; evidence; numbering; history SAVED; audit | Qty > eligible; **no** MLB-1 write |
| WST-002 Approve | wastage; MLB-1 | APPROVED; movement; MLB-1 if bucket exists; history; audit | Repeat approve; SYSTEM_EXPIRY |
| WST-003 Reverse | wastage; original movement; MLB-1 | REVERSED; compensating REVERSAL; MLB-1 restore; history; audit | Second reverse; not APPROVED |
| SYSTEM_EXPIRY | existing job order | unchanged | Preserve SKIP LOCKED |

Never UPDATE/DELETE `material_movement` or `audit_event`. Never UPDATE original `inward_entry` quantity/source. Wastage `movement_id` may be set **once** from NULL after insert (existing SYSTEM_EXPIRY pattern).

---

## 10. Reversal, correction, and idempotency strategy

### 10.1 Unified INW-SCR-005 Reverse/Correct (`INWARD.REVERSE`)

`INWARD.REVERSE` authorises this operation **only**. It does **not** authorise direct editing of posted inward entries. It does **not** authorise an inward approval step.

Rules:

- Mandatory reason (OV-06).
- Row locks as §9; **revalidate** dependencies after lock.
- Create **one** compensating `REVERSAL` movement (`linked_original_movement_id` = original inward movement). Inverse MLB-1 via `LotBalanceService`.
- **Never** edit or delete the original movement or the original `inward_entry` business fields.
- A **second reversal is blocked** (generated UNIQUE on original movement id).
- If downstream dependencies exist (allocations, rejections, dispositions, wastage, later inwards that consume the lot), reversal is **blocked until they are resolved in reverse dependency order**. Status-blocked Reverse remains **visible and disabled** with a complete Disabled Reason naming the blocking document (e.g. `CH-2026-000481`).
- A **correction** is saved as a **new replacement** `inward_entry` with `replaces_inward_entry_id` = original. The replacement posts according to its source type (vendor/production/opening/return rules). Original remains historically readable.
- PermissionRestricted (no `INWARD.REVERSE`): Reverse/Correct **absent**, not disabled.
- Historical rows remain readable.

Wastage reversal (WST-003) is a **separate** supported workflow (`WASTAGE.CANCEL_REVERSE`). Disposition reversal is **not** supported (C4).

### 10.2 Idempotency

Commands that retry under deadlock **must** carry an idempotency key:

`INWARD_REVERSE`, `INWARD_CORRECT`, `REJECTION_SAVE`, `REASSIGN_SAVE`, `DISPOSITION_SAVE`, `WASTAGE_APPROVE`, `WASTAGE_REVERSE`.

**No** `RETURN_APPROVE` / inward-approve keys.

Insert `idempotent_command` first (UNIQUE). If the key exists, return the original result; do not post a second movement. Repeated Wastage Approve must not reduce inventory twice. Repeated Reverse must not restore twice.

`TransactionManager::run(..., idempotent: true)` for those boundaries.

---

## 11. Route and permission matrix

Active-role permissions only. Never union held roles (S. Patil Store Keeper vs Billing Clerk). Permission-denied actions are **absent**. Status-blocked actions stay **visible and disabled** with a full reason. Super Admin does not justify inventing missing permissions.

### 11.1 Map CSV labels onto existing codes (no new code except §11.2)

| CSV label | Existing code | Notes |
|---|---|---|
| Inward — Create | `INWARD.ADD` | INW-002, INW-004 Save, vendor |
| Inward — Opening Stock | `OPENING_STOCK.ADD` | INW-003 |
| Inward — View | `INWARD.VIEW` / `INWARD.LIST` | INW-005 |
| Return — Create / Return — Approve | `INWARD.ADD` only | CSV “Return — Approve” is **compatibility wording**. There is no inward approve action and **no** `INWARD.APPROVE`. |
| Rejection — Create | `DISPATCH.ADD` | DSP-001 |
| Disposition — Reassign | `DISPATCH.REASSIGN` | DSP-002 and DSP-003 reassignment path |
| Disposition — Return / Wastage | `DISPATCH.DISPOSE` | DSP-003 Return and Wastage paths. Do **not** invent `DISPATCH.RETURN` / `DISPATCH.WASTAGE`. |
| Wastage — View / Create | `WASTAGE.LIST` / `WASTAGE.VIEW` / `WASTAGE.ADD` | WST-001 |
| Wastage — Approve | `WASTAGE.APPROVE` | WST-002 |
| Wastage — Reverse | `WASTAGE.CANCEL_REVERSE` | WST-003. Do **not** invent `WASTAGE.REVERSE`. |

DSP-003 PermissionRestricted: omit the Reassignment path entirely when `DISPATCH.REASSIGN` is absent (Store Keeper). Frame is visibly shorter. Return and Wastage paths remain with `DISPATCH.DISPOSE`.

WST-001 PermissionRestricted: Record wastage **absent** without `WASTAGE.ADD`.

WST-002 PermissionRestricted: Approve **absent** without `WASTAGE.APPROVE`. Creator self-approval for **wastage** still depends only on the active role holding `WASTAGE.APPROVE`.

### 11.2 Permission-provenance matrix

| Code | Trace | Verdict |
|---|---|---|
| **`INWARD.APPROVE`** | CSV “Return — Approve”; withdrawn UX-DEC-007 | **Do not add.** Would create a false inward approval workflow. Vendor, Plant Production, Opening Stock, and Return/Recovery all post (or record reusability) on Save. |
| **`INWARD.REVERSE`** | CSV “Inward — Reverse”; FRIS 11.16; 12 Sep 2026 P2 | **Add — the only new catalogue code.** Authorises unified INW-SCR-005 Reverse/Correct. Does not authorise in-place edit. Mandatory reason. One compensating `REVERSAL`. Locks + revalidate. Second reverse blocked. Downstream deps block until resolved in reverse order. Correction = new replacement `inward_entry`. PermissionRestricted **removes** the action. |
| `INWARD.CANCEL` | Phase 1 catalogue | **Keep unused** for this slice. Reverse ≠ cancel. Do not reuse CANCEL for Reverse/Correct. |
| `DISPATCH.REVERSE` | DSP-003 Review-Reversed | **Do not add** (C4). |
| `WASTAGE.REVERSE` | CSV Wastage — Reverse | **Do not add.** Use `WASTAGE.CANCEL_REVERSE`. |
| Any other new code | — | **Do not add.** |
| `INWARD.ADD` grant → Plant Manager | CSV INW-002 primary role; FRIS 11.4 | **Existing code.** Additive `role_permission` only (not a new code). |
| `OPENING_STOCK.ADD` grant → Plant Manager | CSV INW-003 primary role; FRIS 11.5 | **Existing code.** Additive grant only. |
| `INWARD.REVERSE` grant | CSV INW-005 primary Store Keeper | Store Keeper + Super Admin (catalogue). Do not infer Plant Manager or Accounts Manager. |

### 11.3 Planned routes (after **explicit** implementation approval of this corrected plan)

No `/seed`, `/fixture`, `/demo`, `/probe`, `/test` routes. CSRF on POST. `?frame=` development/testing overlay only; **production ignores `?frame=`**. Production seed/probe route count remains **zero**.

| Method | Path | Screen | Permission |
|---|---|---|---|
| GET/POST | `/inward/production`, `/inward/production/{id}` | INW-002 | VIEW/ADD |
| GET/POST | `/inward/opening-stock`, `/inward/opening-stock/{id}` | INW-003 | VIEW + `OPENING_STOCK.ADD` |
| GET/POST | `/inward/returns`, `/inward/returns/{id}` | INW-004 | VIEW/ADD. **No** `/approve` or `/return` routes |
| GET | `/inward` | INW-005 | `INWARD.LIST` |
| POST | `/inward/{id}/reverse` | INW-005 | `INWARD.REVERSE` + OV-06 |
| POST | `/inward/{id}/correct` | INW-005 | `INWARD.REVERSE` (replacement body) + OV-06 |
| GET/POST | `/rejections/create`, `/rejections/{id}` | DSP-001 | `DISPATCH.ADD` / VIEW |
| GET/POST | `/rejections/{id}/reassign` | DSP-002 | `DISPATCH.REASSIGN` |
| GET/POST | `/rejections/{id}/disposition` | DSP-003 | `DISPATCH.DISPOSE` and/or `REASSIGN` (paths collapse). **No** reverse route |
| GET | `/wastage` | WST-001 list | `WASTAGE.LIST` |
| GET/POST | `/wastage/create`, `/wastage/{id}` | WST-001 create / WST-002 | ADD / VIEW |
| POST | `/wastage/{id}/approve` | WST-002 | `WASTAGE.APPROVE` |
| POST | `/wastage/{id}/return` | WST-002 | `WASTAGE.APPROVE` |
| GET | `/wastage/{id}/reverse` | WST-003 | VIEW |
| POST | `/wastage/{id}/reverse` | WST-003 | `WASTAGE.CANCEL_REVERSE` |

Keep existing `/inward/vendor`. Sidebar items omitted without LIST/VIEW.

Direct URL without VIEW → existing 403 page-scope message.

---

## 12. CI4 module/file plan

Follow `app/Modules/{Domain}/Services` + `app/Controllers` + `app/Views/pages` + migrations/seeders. Progressive JS only (filter drawer, file upload, OV-11/15, unsaved flag). No SPA.

| Area | Planned PHP | Views | Reuse |
|---|---|---|---|
| Inward | Extend `InwardService`; add `ReturnRecoveryService` (**save only**), `InwardReversalService`, `InwardQuery` | `pages/inward/production.php`, `opening.php`, `return.php`, `register.php` | Vendor form, `FrameCatalog` compat copy, `LotBalanceService`, `ScopeService` |
| Disposition | `RejectionService`, `ReassignmentService`, `DispositionService` (**no reverse**) | `pages/rejection/form.php`, `reassign.php`, `pages/disposition/resolve.php` | Decimal, TransactionManager, AuditWriter |
| Wastage | `WastageService` (USER_RECORDED); do not rewrite `ExpiryProcessor` except new lot columns | `pages/wastage/list.php`, `create.php`, `review.php`, `reverse.php` | NumberingService Tx 13 |
| HTTP | controllers listed in §11.3 | — | Auth filters |
| UI | `Phase3Frames` (110 names) | Partials: OV-06/07/11/15, File-Upload, Attachment-Row, Quantity Summary, Disabled Reason | `layouts/app.php`, `tokens.css` |
| Evidence | `EvidenceStorage` (local) | Attachment-Row | No cloud SDK |
| Seed | Additive only; **do not change** 116.000 / 3+7+2 / 177,592.00 | Visual-state data **development/testing only** (C3 guard) | Existing identities |
| Tests | `Phase3FrameHttpTest` (110), integration G2/REJ-CUM/reverse/idempotency/reusability, browser subset | — | Existing suite stays green |

---

## 13. Figma-to-View/component mapping

| Template | Screens | View mapping | Components |
|---|---|---|---|
| T03 | INW-002/003/004, DSP-001/002 | Entity form pages | App Shell, Breadcrumb, Editable Line-Item Grid, Quantity Input, Searchable Select, Calculation/Quantity Summary, Sticky Action Bar, Inline Error |
| T01 | INW-005, WST-001 | Register pages; WST create is a T01 Create state on the same Screen ID | Data Table, Row Action Menu, Status Badge, Empty State, Loading Skeleton, Pagination, OV-07; Create: Input, File-Upload, Attachment-Row |
| T04 | DSP-003, WST-002, WST-003 | Review/resolution pages | Definition List, Status Badge, Status Timeline, Audit Timeline, Transaction Chain, Sticky Action Bar, Disabled Reason Tooltip |

P14 — INW-002: Finished product *, Produced quantity *, Production date *, Batch reference; **read-only** recipe grid (informational).  
INW-003: As-at date *; lines Material *, Quantity *, Unit (read-only), Lot reference.  
INW-004: Entry-path selector (challan line / disposition / non-customer recovery); paired original challan fields where applicable; `disposition_record_id` only for disposition origin; `client_id` when customer return; `return_reason` *; **mandatory reusability** on Save; optional invoice/credit-note refs; optional evidence. Sticky action **Save**. No Approve Return.  
INW-005 columns: Reference, Date, Source type (Vendor / Production / Opening Stock / Return), Counterparty, Lines, Total quantity, Reversal, Actions. Reverse/Correct in row menu when `INWARD.REVERSE` present.  
DSP-001: Source challan *; Client/Product/Despatched derived; Rejected qty *; Quantity Summary.  
DSP-002: Rejection reference; replacement client *; site *; exclusivity warning.  
DSP-003: Three exclusive path inputs (permission-absent path omitted); live Remaining. Reversed frame: compatibility sentence only.  
WST-001: Origin User / System; optional evidence.  
WST-002: Exact quantity+Unit; inventory reduces on approval; 067 Approved in operational data.  
WST-003: Source lot to restore; **Reverse Wastage**; history.

Do not use “Write-Off Review” or “Approve Write-Off” anywhere.

---

## 14. Responsive implementation plan

| Breakpoint | Rule |
|---|---|
| Desktop ≥ 1024 | CSV D frames. T03 two-column + grid; T01 table; T04 summary + timeline |
| Tablet 768–1023 | **No CSV T frames.** Filter drawer + count badge; P3 columns behind chooser; radios 44px; sticky bar 72 |
| Mobile < 768 | CSV M frames only. Lists → record cards; forms stacked; Quantity Summary pinned above sticky bar; WST create offers camera and file sources |

Loading skeletons after 300ms. Empty vs NoResults distinct copy. PermissionRestricted frames **remove** controls (shorter), never leave blank gaps.

INW-002/003/004 have no mobile Review-* frames — do not invent them. WST-002/003 have a single mobile PendingApproval frame each.

---

## 15. Exact sample-data plan

Do **not** fabricate Horizon / LPT / Sahyadri. Do **not** seed Blueprint extras `CH-2026-000478`, `INV-2026-000122`, `CRN-2026-000031`, `RES-000014` unless a Phase 3 frame cannot render without them (none of the ten screens require those identities as production rows). Do **not** change Phase 1/2 operational quantities.

Preserve:

| Item | Value |
|---|---|
| Ledger M30 Apex RMC Chakan | **116.000 MT** = 120.000 + 25.000 − 30.000 + 3.000 − 2.000 |
| Allocation CH-2026-000481 | 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** Fully Disposed |
| Invoice INV-2026-000119 | **₹177,592.00**, round-off **₹0.00** |
| Documents | CH-2026-000481, INV-2026-000119, INW-2026-000212, INW-2026-000230, WST-2026-000067, REJ-2026-000044, LOT-2026-00212, LOT-2026-00230 |
| Frame count | **110** = 91 desktop + 19 mobile |

| Identity | Phase 3 use |
|---|---|
| `INW-2026-000230` / `LOT-2026-00230` | INW-002 posted production; Reverse **status-blocked** (allocated 12.000 to CH-2026-000481) when `INWARD.REVERSE` present |
| Opening `INW-2026-000200` / `LOT-2026-00212` | INW-003 / register; 00212 Reverse **status-blocked** (fully allocated 18.000) |
| `INW-2026-000212` cement vendor | Register Vendor type |
| `REJ-2026-000044` | DSP-003 Fully Disposed 3 / 7 / 2 and WST-2026-000067. In-progress CSV Default/Draft remaining figures are **`?frame=` fixtures** only |
| `WST-2026-000067` | **Always Approved.** Origin User, raised by S. Patil, source lot LOT-2026-00212. PendingApproval / ReturnedForCorrection visual examples: **testing fixtures or development-only visual-state data** (C3). Never fake production rows. Never modify 067’s lifecycle |
| SYSTEM_EXPIRY row | WST-001 Origin System; WST-002 Review-Cancelled; actor SYSTEM; no Approve |
| A. Deshpande | Illustrative name on WST-003 fixture/history copy only. Do not grant Accounts Manager `WASTAGE.CANCEL_REVERSE` |
| DSP-003 Reversed | Compatibility fixture; **no** REVERSED row |

New operational rows created through UI in tests must use Company/Plant scope (Apex RMC + Chakan) and must not alter `LedgerReconciliationTest`. Reverse/idempotency tests use **dedicated** lots created inside the test, not 067’s 2.000 MT and not the frozen 3+7+2 disposition.

---

## 16. Validation and error-code matrix

Use FRIS codes where known. Independent checks. BCMath comparisons at quantity scale 4.

| Code | When | UI |
|---|---|---|
| INW qty/plant/product/unit required | Missing or inactive master | Inline + summary |
| INW-002 | Recipe missing is **not** a Save blocker (grid informational). Do not post RM stock | Grid empty state |
| INW-003 date out of range | Cutover window | Date field + stated reason |
| INW-004 reusability missing | Save without Reusable / Not reusable | Inline + summary |
| INW-004 challan pair | One of original_challan_id / line set without the other | Pairing error |
| INW-004 qty > eligible | Exceeds despatched / disposition remaining | Create-Error |
| INW-004 disposition_record_id on non-DSP path | Populated when origin is not DSP-003 | Inline |
| INW reverse missing reason | OV-06 | Cannot submit |
| INW reverse downstream | Dependencies not resolved in reverse order | Visible + disabled + full reason |
| INW reverse already reversed | Second reverse | Visible + disabled + reason |
| INW reverse permission | No `INWARD.REVERSE` | Action **absent** |
| REJ-CUM / DSP reject > eligible | Rejected > dispatched − already rejected | DSP-001 Error |
| DSP-ERR-018 | Reassignment client or site omitted | DSP-002 Error |
| DSP-ERR-017 | Disposition sum exceeds rejected (sample by 3.000 MT); names all three path quantities + rejected | DSP-003 Review-Error |
| Exclusive path | Same qty on two paths | Domain error before G2 write |
| DSP reverse | Any attempt to mutate disposition to reversed | No control; compatibility copy only |
| WST qty > eligible bucket | Create-Error | Quantity exceeds eligible |
| WST approve twice | Idempotency / status not PENDING | No second MLB-1 reduction |
| WST reverse twice | Unique reversal | Disabled Reverse (status-blocked) |
| Evidence invalid type/size | Optional upload failed | Inline; **must not** block later Wastage Approve if omitted |
| OV-11 | `row_version` mismatch | Overlay Reload |
| MLB1_NEGATIVE_BUCKET | Any delta would go negative | Rollback entire tx |
| Scope | Write outside grant | 403, no partial write |

SYSTEM_EXPIRY: no Approve action to error.

---

## 17. Automated unit / integration / feature / browser test plan

Phase 3 tests are **not** written until **explicit** implementation approval of this corrected plan.

| Layer | Coverage |
|---|---|
| Unit | `Phase3Frames` count **110** (91 D / 19 M / 0 T); exact Frame Name list; no alias usage; Decimal-only services; catalogue contains `INWARD.REVERSE` and **does not** contain `INWARD.APPROVE` |
| Feature HTTP | All **110** frames render; production ignores `?frame=`; INW-002/003/004 compatibility sentence exact on every approval-named frame; no Approve/Return controls on those types; DSP-003 Reversed has the disposition-permanence sentence and **no** Reverse control; Store Keeper DSP-003 shorter (no reassignment path); WST-002 SYSTEM_EXPIRY has no Approve; C3 fixtures not present in production |
| Feature IAM | Patil Store Keeper does not union Billing Clerk; Reverse/Correct **absent** without `INWARD.REVERSE`; Reverse **disabled+reason** when downstream deps exist; Wastage Reverse absent without `WASTAGE.CANCEL_REVERSE` |
| Integration INW-002 | Save posts FP movement+MLB-1+audit; **zero** RM movements; no approval columns |
| Integration INW-003 | Opening stock posts on Save; no approval columns |
| Integration INW-004 | Reusable Save posts `RETURN_RECOVERY_INWARD` and increases Available immediately; not reusable does **not** increase Available and does **not** write `REJECTION_RECOVERY` / `RETURNED_TO_AVAILABLE`; challan pair enforced; disposition_record_id only on DSP origin; no approve route (404) |
| Integration INW-005 | Four source types; allocated reverse blocked naming CH-2026-000481; one `REVERSAL`; original unchanged; second reverse blocked; correction inserts replacement linked to original |
| Integration DSP | C5 REJECTION_RECOVERY → REJECTED_PENDING; Available unchanged; REJ-CUM; G2 3+7+2; DSP-ERR-017; no disposition reverse API; no Return to Vendor string |
| Integration WST | Create does not reduce MLB-1; Approve reduces once; 067 remains Approved; reverse restores exact lot; evidence omitted still Approves |
| Environment | `CI_ENVIRONMENT=production` ignores visual-state fixtures; testing env may render PendingApproval **without** writing fake 067 pending rows |
| Browser | Desktop happy path per screen; mobile cards; OV-06/07/11/15; permission collapse without gaps |
| PHPUnit | No float in new services; READ COMMITTED; Phase 0–2 suite green |

---

## 18. Phase 0–2 regression plan

Must remain green:

- Entire current PHPUnit suite (Phase 2 close: 154 tests / 2980 assertions as last recorded)
- Authentication, MySQL `ci_sessions`, active-role switching
- Company/Plant scope including shared **Chakan Plant**
- Master data lifecycle / deactivation guards
- Vendor Inward direct posting (`/inward/vendor`)
- Challan vs Invoice independent approvals (Challan must be Approved first for invoice approve)
- Manual IRN recording INV-SCR-008
- SYSTEM actor + SYSTEM_EXPIRY job
- Material Ledger closing **116.000 MT**
- Allocation 18+12; LOT-2026-00230 remaining **13.000**
- Disposition 3+7+2 Fully Disposed
- Invoice **₹177,592.00**, round-off **₹0.00**
- Audit + export; rate and statutory rules; vehicle assignments; recipe uniqueness and **no stock impact**
- Production ignores `?frame=`; **zero** seed/probe routes
- Phase 0–2 migrations still apply on empty DB
- Existing permission codes unchanged except additive `INWARD.REVERSE`
- Application tests do not verify Figma attachment/detachment or P28 prototype reactions

---

## 19. Phase 3 entry and exit criteria

### Entry (implementation)

- This **corrected** plan received **explicit implementation approval** on 12 Sep 2026.
- C1–C5, P1–P3 in the binding decisions table are closed.
- Additive migrations only; no Phase 0–2 rebuild.
- C6 applied at implementation: Plant Manager receives existing `INWARD.ADD` and `OPENING_STOCK.ADD` grants.

Phase 3 implementation is authorised. Do not begin Phase 4.

### Exit (after a later implementation turn)

- All **110** CSV frames renderable (91 desktop + 19 mobile) with exact Frame Names.
- INW-002 = Plant Production Inward (FP only, recipe display-only); INW-003 = Opening Stock; INW-004 = Return/Recovery with Save-time reusability; all three plus vendor have **no** inward approval FSM; compatibility sentence exact; no false approval storage; no `INWARD.APPROVE`.
- Catalogue contains `INWARD.REVERSE` only as the new inward code; Reverse/Correct rules in §10 hold.
- DSP operational domain; C5 rejection recovery; G2 12.000; DSP-ERR-017; exclusive paths; Store Keeper path collapse; DSP-003 Reversed compatibility only.
- WST operational USER_RECORDED + SYSTEM_EXPIRY; 067 Approved; inventory reduces only on Wastage Approve; Reverse Wastage labels; second reverse blocked.
- Frozen ledger, allocation, lot remaining, disposition, invoice, and Phase 0–2 tests green.

---

## 20. Conflicts, unresolved decisions, and missing dependencies

| ID | Item | Status after 12 Sep 2026 |
|---|---|---|
| **C1** | Return/Recovery sources and posting | **Closed.** Three entry paths; DB-CHG-006 fields; Save-time reusability; `RETURN_RECOVERY` / `RETURN_RECOVERY_INWARD`; not `REJECTION_RECOVERY` / `RETURNED_TO_AVAILABLE`. |
| **C2** | Recipe consumption | **Closed.** Display-only; no RM movements; no consumption engine. |
| **C3** | WST-2026-000067 pending vs Approved | **Closed.** Keep Approved; environment-guarded fixtures only. |
| **C4** | DSP-003 Reversed | **Closed.** Compatibility coverage; no reversal feature; no new permission. |
| **C5** | DSP-001 movement | **Closed.** `REJECTION_RECOVERY` → `REJECTED_PENDING`; Available unchanged. |
| **C6** | Plant Manager lacks `INWARD.ADD` / `OPENING_STOCK.ADD` | **Closed at implementation.** Additive `role_permission` grants of existing codes (not new catalogue codes). |
| **C7** | WST-001 CSV components vs File-Upload | **Closed as implementation mapping.** Evidence UI per P17/UX-DEC-008; Frame Names unchanged. |
| **C8** | WST-003 approval-named frames | **Closed.** Mapped in §8.3; not a reversal-approval FSM. |
| **C9** | A. Deshpande vs `WASTAGE.CANCEL_REVERSE` | **Closed.** Fixture name only; no extra grant. |
| **C10** | OD-B3-04/05/06 unanswered | **Open, non-blocking.** Placeholders stand; reverse via INW-005 only. |
| **C11** | BRD/TDD/FRIS/DB Arch files not in `/workspace` | **Partially closed for C1** by the instruction that DB-CHG-006 points at Database Architecture v1.2. If a later attached extract contradicts §5.1, **stop and report**. |
| **C12** | `original_credit_note_id` FK | **Open, non-blocking.** Credit-note tables are out of this slice. Column may be nullable without FK until that module. Do not build credit notes to satisfy the column. |
| **C13** | Not-reusable return vs MLB-1 | **Specified in §7/§8.1.** Qty never enters Available; wastage workflow handles it; no new movement_type. If Database Architecture v1.2 later requires a named no-bucket wastage movement that is not in the current CHECK list, **stop and report** rather than inventing a type. |

**Missing dependencies (do not invent):** attachment virus-scan product (local upload only); G2 projection approval in `PROJECTION_TABLES.md` before that migration; credit-note module for a live `original_credit_note_id` FK.

**Non-conflicts:** 12 aliases do not apply. Tablet frames do not exist. `RETURNED_TO_AVAILABLE` remains the DSP path. SYSTEM_EXPIRY job already complete. Vendor inward remains direct-post. Wastage approval FSM remains (distinct from inward).

---

*Phase 3 implementation is authorised from this corrected plan. Do not begin Phase 4.*
