# Phase 1 plan (approved and closed)

**Status:** Approved and **closed**. Phase 1 screens, routes, and tests are complete. Do not reopen Phase 1. Phase 2 is closed. Do not start Phase 3 screens or routes until `docs/PHASE3_PLAN.md` is approved.  
**Stack (locked):** CodeIgniter 4, PHP 8.2+, MySQL 8 InnoDB. Phase 0 remains closed.  
**UI slice:** eight Screen IDs, server-rendered CI4 Views, 88 CSV frames including compatibility states.  
**Precedence:** BRD v0.9.22 → approved addenda → TDD v0.4.1 if no BRD contradiction → Database Architecture intent translated to MySQL → FRIS → Blueprint/Figma → CSV frames.

### Closed wording — Challan vs Invoice approval

**Invoice approval is a separate action from Challan approval, but the Challan must be Approved first.**

Do not describe this as “Immediate invoice independent of challan approval.” Create invoice (`INVOICE.ADD`) and approve invoice (`INVOICE.APPROVE`) remain separate actions. Invoice approval still requires the linked Challan to already be Approved.

## Approved conflict resolutions (12 Sep 2026)

### Final Decision 1 — Challan numbering

Follow BRD CHL-BR-001 and Database Architecture Tx 2. The earlier FRIS/Blueprint “assigned on Approve” wording is **withdrawn** for implementation.

- Visible Challan number is issued **permanently on the first successful persist/Save**.
- Database primary key (`challan_id`) is **not** the visible number.
- Number issuance and Challan persistence occur in the **same transaction**.
- Scope: Company + Financial Year + Document Type.
- Lock `document_sequence` with `SELECT … FOR UPDATE`.
- Insert registry and numbering-history rows in the same transaction.
- A failed or rolled-back Save must not persist a Challan or registry row.
- A transaction rollback leaves **no successfully issued number**. Registry and sequence changes roll back together.
- If the sequence update rolls back, the **same next_value** may be used by the next successful transaction. That is **not** “reuse of an issued gap.”
- Once a Challan number is **successfully committed**, it is permanently consumed and never reused, including after cancellation.
- Invoice reservation release/reuse remains governed by INV-BR-002/003.
- DSP-created Challans will follow the same rule when that module is implemented.

### Final Decision 2 — `AUDIT.EXPORT`

Add the permission because the approved CSV/Figma contains Audit ExportRestricted.

- `AUDIT.VIEW` — view and filter audit history.
- `AUDIT.EXPORT` — request an audit export. `REPORT.EXPORT` does **not** imply it.
- Permission absence **removes** the Export control (no disabled placeholder).
- ExportRestricted retains readable audit data and the permission explanation.
- Phase 1 seed: grant `AUDIT.EXPORT` to **Super Admin only** (via full catalogue).
- Every export request writes an audit event. Rows stay immutable.

### Final Decision 3 — Disposition terminology

The 3.000 MT path is **Return to Availability**, not Return to Vendor.

- UI label: `Return to Availability`
- Domain/state: `RETURNED_TO_AVAILABLE`
- Movement: approved return-to-available from the original disposition context
- Quantity: 3.000 MT
- Do not display “Return to Vendor”; do not create a vendor-return movement.

Reconciliation: 3.000 + 7.000 + 2.000 = 12.000 MT, Fully Disposed.

### Final Decision 4 — INV-SCR-008 states

INV-SCR-008 has **no** independent IRN-attempt approval workflow. Parent Invoice status controls eligibility. Do not store PendingApproval / Approved / ReturnedForCorrection on `irn_attempt`.

| Frame | Meaning |
|---|---|
| Review-PendingApproval | Parent invoice Pending Approval; IRN recording unavailable |
| Review-Approved | Parent Approved and eligible for manual IRN recording (statutory deadline still applies) |
| Review-ReturnedForCorrection | Parent Returned for Correction; IRN recording unavailable |
| Create/Review-IRNFailed | Failed manual attempt exists; document IRN state Failed; `Retry IRN manually` starts a fresh attempt |
| Create/Review-IRNGenerated | Successful terminal IRN; no Record or Retry |
| PermissionRestricted | Active role lacks IRN-recording permission; recording controls absent |
| ConcurrentUpdate | Row-version conflict while recording |

IRN attempt history is append-only. No automatic generation, queue, cron, or retry worker.

### Implementation safeguards

1. **SYSTEM actor** — not an interactive login: no usable password, cannot authenticate at `/login`, no browser session, identifiable as SYSTEM in audit, authority limited to coded scheduled operations, not Super Admin.
2. **SYSTEM_EXPIRY insert order** — avoid circular FK: lock/revalidate lot → issue/consume WST number → insert Approved `wastage_record` (`movement_id` NULL) → insert `material_movement` referencing the wastage id → set `wastage_record.movement_id` once in the same transaction → update MLB-1 → audit → commit. Never UPDATE an append-only movement row. Never commit a null movement link.

---

## 1. Reconciled 88-frame inventory

CSV source: `uploads/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1_4e22.csv`. Count is **88**. Overlay dialogs (OV-05/06/07/09/11/14) are shared components, not extra Screen IDs. One tablet frame only: `T-CHL-SCR-002-Create-Approved`.

| Screen ID | Name | D | T | M | Total | FR range |
|---|---|---:|---:|---:|---:|---|
| INW-SCR-001 | Vendor Material Inward | 9 | 0 | 2 | **11** | FR-0217–FR-0227 |
| CHL-SCR-001 | Challan List | 7 | 0 | 2 | **9** | FR-0338–FR-0346 |
| CHL-SCR-002 | Challan Create/Review (immediate invoice) | 14 | 1 | 2 | **17** | FR-0347–FR-0363 |
| INV-SCR-003 | Invoice Review | 12 | 0 | 2 | **14** | FR-0385–FR-0398 |
| INV-SCR-004 | Invoice List | 7 | 0 | 2 | **9** | FR-0399–FR-0407 |
| INV-SCR-008 | Invoice IRN Outcome Recording | 12 | 0 | 3 | **15** | FR-0420–FR-0434 |
| RPT-SCR-003 | Material Ledger Report | 6 | 0 | 1 | **7** | FR-0491–FR-0497 |
| AUD-SCR-001 | Audit History | 5 | 0 | 1 | **6** | FR-0601–FR-0606 |
| **Total** | | **72** | **1** | **15** | **88** | |

### INW-SCR-001 — every frame (11)

| FR | Frame | Treatment |
|---|---|---|
| FR-0217 | `D-INW-SCR-001-Create-Default` | Operational create. Blank entry. Primary action is **Save** (posts). CSV “Save draft” is template language, not a stored draft status. |
| FR-0218 | `D-INW-SCR-001-Create-Draft` | Operational **pre-save populated form** (working copy). No `inward` approval/draft status column exists (DB Arch §10.4). Do not persist a false DRAFT status. |
| FR-0219 | `D-INW-SCR-001-Create-Error` | Operational validation (summary + inline). |
| FR-0220 | `D-INW-SCR-001-Create-Loading` | Operational skeletons. |
| FR-0221 | `D-INW-SCR-001-Review-PendingApproval` | **Compatibility only** — see §2. |
| FR-0222 | `D-INW-SCR-001-Review-Approved` | **Compatibility only** — see §2. |
| FR-0223 | `D-INW-SCR-001-Review-ReturnedForCorrection` | **Compatibility only** — see §2. |
| FR-0224 | `D-INW-SCR-001-Review-ConcurrentUpdate` | Operational OV-11. Concurrent save against a posted inward. No Approve control. |
| FR-0225 | `D-INW-SCR-001-Review-PermissionRestricted` | Operational IAM: missing `INWARD.ADD` / `INWARD.VIEW`. Decision/Approve controls absent because they do not exist on this type, not because of a fake “decision permission”. |
| FR-0226 | `M-INW-SCR-001-Create-Default` | Operational mobile create. |
| FR-0227 | `M-INW-SCR-001-Create-Error` | Operational mobile error. |

### Remaining screens — frame names (77)

**CHL-SCR-001 (9):** `D-…-List-Default`, `Filtered`, `Empty`, `NoResults`, `Loading`, `Error`, `PermissionRestricted`; `M-…-List-Default`, `NoResults`.

**CHL-SCR-002 (17):** `D-…-Create-Default`, `Draft`, `PendingApproval`, `Approved`, `ReturnedForCorrection`, `Error`, `Loading`; `D-…-Review-Approved`, `PendingApproval`, `IRNFailed`, `IRNGenerated`, `ReturnedForCorrection`, `ConcurrentUpdate`, `PermissionRestricted`; `M-…-Create-Default`, `Create-Error`; `T-…-Create-Approved`.  
These Create/Review approval names are **real challan/invoice workflow states**, not INW compatibility.

**INV-SCR-003 (14):** `D-…-Review-PendingIRN`, `IRNFailed`, `IRNGenerated`, `Cancelled`, `Draft`, `PendingApproval`, `Approved`, `ReturnedForCorrection`, `Reversed`, `Loading`, `ConcurrentUpdate`, `PermissionRestricted`; `M-…-Review-PendingIRN`, `PendingApproval`.

**INV-SCR-004 (9):** same list pattern as CHL-SCR-001.

**INV-SCR-008 (15):** `D-…-Create-Default`, `IRNGenerated`, `IRNFailed`, `PermissionRestricted`, `Draft`, `Error`, `Loading`; `D-…-Review-PendingApproval`, `Approved`, `ReturnedForCorrection`, `ConcurrentUpdate`, `PermissionRestricted`; `M-…-Create-IRNGenerated`, `Create-Default`, `Create-Error`.  
IRN recording is a persist of `irn_attempt`, not an inward-style approval FSM. Those Review-* names are **not** in the INW compatibility treatment (Task 1). They stay mapped to IRN create/record outcomes; they are not a second fake inward approval machine. Do not store IRN “PendingApproval” as a document status.

**RPT-SCR-003 (7):** `D-…-Report-Default`, `Filtered`, `Empty`, `Loading`, `Error`, `PermissionRestricted`; `M-…-Report-Default`.

**AUD-SCR-001 (6):** `D-…-View-Default`, `Filtered`, `Empty`, `Loading`, `PermissionRestricted`; `M-…-View-Default`. Approved aliases (View-Default / Filtered / PermissionRestricted) remain.

**Exit criterion:** all **88** frames are renderable, including the three INW compatibility states in §2.

---

## 2. Direct-post inward compatibility-frame matrix

**Authoritative rule (do not implement an inward approval state machine):**

- BRD: inward approval removed for Vendor / Plant Production / Opening Stock.
- Blueprint 12.24: “Posts DIRECTLY to inventory on Save. No approval step and no status badge.”
- DB Arch `inward_entry`: no `approved_by` / approval-status column. Quantity becomes Available in the same transaction as Save (`material_movement` + `material_lot_balance`).

### Affected frames (complete list)

There are **three** affected frames. All are **desktop**. There is **no** mobile (or tablet) PendingApproval / Approved / ReturnedForCorrection frame for INW-SCR-001.

| FR | Device | Frame name | Stored application state | Rendered copy | Controls that must not appear |
|---|---|---|---|---|---|
| FR-0221 | Desktop | `D-INW-SCR-001-Review-PendingApproval` | Manifest/UI compatibility only. No row with pending-approval status. Fixture is a **posted** vendor inward (or empty shell keyed by frame id), never `PENDING_APPROVAL`. | Exactly: `Not applicable — this inward type posts directly on Save and has no approval workflow.` | Approve, Return for Correction, approval-status badge, Submit-for-approval |
| FR-0222 | Desktop | `D-INW-SCR-001-Review-Approved` | Same. Do not store `APPROVED` as an inward status. | Same sentence. | Same. Read-only posted inward may still show quantities/lots; the approval chrome is replaced by the sentence. |
| FR-0223 | Desktop | `D-INW-SCR-001-Review-ReturnedForCorrection` | Same. Do not store `RETURNED_FOR_CORRECTION`. | Same sentence. | Same. No reason-banner-as-return-from-approver. |

**Not affected (do not apply the compatibility sentence as a substitute for real behaviour):**

| Frame | Why not compatibility-approval |
|---|---|
| `D/M-…-Create-Default`, `Create-Error`, `Create-Loading` | Real create / validation / loading. |
| `D-…-Create-Draft` | Populated pre-save form. CSV “Submit or approve” is template language and is **not** implemented. Sticky action is Save (posts). |
| `D-…-Review-ConcurrentUpdate` | Real OV-11 on a posted inward. |
| `D-…-Review-PermissionRestricted` | Real missing `INWARD.*`. No Approve control to hide as “decision permission”. |
| `M-INW-SCR-001-Create-Default`, `M-INW-SCR-001-Create-Error` | No mobile approval-named frames exist. |

**Storage prohibition:** no column, enum, seed, or audit event may record inward `PENDING_APPROVAL`, `APPROVED`, or `RETURNED_FOR_CORRECTION`. Coverage tests assert the sentence is visible and that those controls/status values are absent.

---

## 3. Updated migration order

Phase 0 tables already exist (`app_user`, `role`, `permission`, sessions, `audit_event`, `job_run`, `job_run_lot`, org grants, etc.). Additive only.

Authoritative names from Database Architecture v1.2. MySQL types per `docs/POSTGRES_MYSQL_TRANSLATION.md`.

| Step | Objects | Notes |
|---:|---|---|
| 1 | Master tables required by FKs (no CRUD routes): `unit_master`, `product` / raw-material (including `shelf_life_controlled`, duration+unit — never INTERVAL), `vendor`, `client`, `client_company`, `client_site`, `vehicle`, `rate_master`, `additional_charge_master`, `financial_year`, `document_sequence`, GST/HSN snapshot sources as specified | Seed development rows. Selectors query **active**. Historical txns still display inactive FKs. |
| 2 | Numbering: `document_number_registry`, `numbering_history`; invoice reservation tables only if invoice persist requires them (`invoice_number_reservation`, `invoice_number_reservation_challan`) | Wastage/challan/invoice consume registry rows. No reservation UI screen. |
| 3 | Inventory source of truth: `inward_entry` (DB-CHG-005 vendor fields; DB-CHG-006 columns exist for later Return/Recovery — Vendor Inward Save does not use them) | `inward_entry_id` **is** `batch_lot_id`. No separate lot table. |
| 4 | `material_movement` (append-only), `material_lot_balance` (**MLB-1** projection, §5) | Projection updated in the same transaction as the movement. |
| 5 | **Minimum wastage backend (no WST UI/routes):** `wastage_record`, `wastage_approval_history` | Required so SYSTEM_EXPIRY can insert an already-Approved wastage with a consumed WST number. USER_RECORDED FSM is not exposed. |
| 6 | Challan: `challan_header`, `challan_line`, `allocation_trace_link`, `challan_approval_history`, `challan_status_history` | Visible CH-* issuance is **blocking** — see §7 and §11. |
| 7 | Invoice: `invoice_header`, `invoice_line`, `invoice_charge_line`, `invoice_challan_link`, `invoice_challan_link_status_history`, `irn_attempt` | Follow INV-BR-002/003. No IRN queue/job. |
| 8 | DSP **backend seed-only** tables needed for ledger 116.000 MT: `rejected_dispatch`, `disposition_record` (and `reassignment` only if the 7.000 MT reassignment path is seeded). **No DSP/WST screens or routes.** | See §6. |
| 9 | SYSTEM actor `app_user` row (reserved service account). `created_by` / `approved_by` FKs on SYSTEM_EXPIRY wastage and movements. | Not a Super Admin. No interactive login in tests except via the job. |
| 10 | Views/indexes: date-wise ledger query over `material_movement` (DB Arch `v_material_ledger_datewise` intent). Collation: codes `utf8mb4_0900_as_cs`; human names table default `utf8mb4_0900_ai_ci`. | |

**Not in this migration list:** WST/DSP/INW-list/master CRUD routes; credit-note tables (not required for the eight screens or the 116.000 MT ledger); IRN background jobs.

**Schema gap (do not invent a column):** `wastage_record` has **no** `batch_lot_id` / `product_id`. Lot linkage is `wastage_record.movement_id` → `material_movement.batch_lot_id` (FK to `inward_entry`). Idempotency is on that movement + `source_type='SYSTEM_EXPIRY'` + `source_bucket`, not a new unpublished column. See §4.

---

## 4. Operational expiry backend design

When `material_lot_balance` exists, `inventory:process-expiry` **must not** remain a no-op. Phase 0 `ExpiryProcessor` currently short-circuits and also assumes non-authoritative columns (`material_lot_balance_id`, `use_by_utc`). Rewrite against DB Arch names. **Never** create an IRN job.

### Authoritative objects

| Concern | Authoritative object |
|---|---|
| Wastage header | `wastage_record` (`source_type='SYSTEM_EXPIRY'`, `reason='EXPIRED'`, `wastage_status='APPROVED'` inserted directly — TDD §5.5 / DB Arch §10.7) |
| Number | `document_sequence` + `document_number_registry` issue-and-consume (Tx 13). WST numbers never `AVAILABLE_FOR_REUSE`. |
| Lot linkage | `material_movement.batch_lot_id` → `inward_entry.inward_entry_id`; `wastage_record.source_bucket` ∈ `AVAILABLE` / `REJECTED_PENDING` / `RESERVED_REASSIGNMENT`; `wastage_record.movement_id` set in the same transaction |
| Compensating movement | `WASTAGE_FROM_AVAILABLE` / `WASTAGE_FROM_REJECTED` / `WASTAGE_FROM_RESERVED_REASSIGNMENT` with `reason='EXPIRED'`. No Expired bucket. |
| Projection | `material_lot_balance` reduced by the exact eligible bucket qty (**MLB-1**) |
| Job run | Existing `job_run` / `job_run_lot` |
| Audit | Existing `audit_event`, append-only, **same transaction**. Actor = SYSTEM (`acted_by` NULL per DB Arch §10.8, or SYSTEM user_id on `wastage_record.created_by` / `material_movement.created_by` as specified) |
| Scope | `company_plant_id` from the source lot. Job iterates company+plant, never bare `plant_id`. |

`wastage_approval_history` exists for USER_RECORDED. SYSTEM_EXPIRY **does not** write a fake human approval row. `approved_by` on `wastage_record` uses SYSTEM-actor semantics (NULL / SYSTEM user per TDD §13.2 Step 5).

USER_RECORDED wastage **table** CHECKs remain (DRAFT/PENDING_APPROVAL/…) so the dictionary is intact. No WST screens, no user Save-and-Approve service in this UI slice. Seeded `WST-2026-000067` is USER_RECORDED Approved (ledger −2.000) written by the seeder, not by the expiry job.

**Complete wastage backend?** The dictionary does not require USER_RECORDED application services to insert SYSTEM_EXPIRY. It **does** require the full `wastage_record` table, numbering, movement types, and lot-balance CHECKs. That is the minimum. If approval later requires the USER_RECORDED service, it is still **no WST UI**.

### Job behaviour (required)

1. Find eligible expired lots using the **UTC Use-By instant** on `inward_entry.use_by_datetime` (`product.shelf_life_controlled = true`) joined to `material_lot_balance` for non-zero qty in any of the three buckets. Eligibility: Use-By **has passed** (TDD §13.1). Compare `use_by_datetime < UTC now` so the instant itself is not expired. DB Arch §18.2 `<=` yields to TDD “has passed”. UX-DEC-004 is a **statutory IRN** exclusive boundary, not this job.
2. `SELECT … FOR UPDATE SKIP LOCKED` on `material_lot_balance` in lock order `(company_plant_id, product_id, batch_lot_id)`.
3. Recheck eligibility after the lock (Use-By still passed, bucket qty still > 0, no existing SYSTEM_EXPIRY movement for that lot+bucket).
4. Process **one lot + one source bucket** per transaction.
5. Create `wastage_record` already `APPROVED`, `source_type='SYSTEM_EXPIRY'`, consume a WST number (Tx 13 condensed into one step).
6. Reduce the **exact** eligible bucket balance on **MLB-1**.
7. Insert the corresponding `material_movement` (`effective_datetime` = lot Use-By, `system_entry_datetime` = job time — TDD §13.4).
8. Insert `audit_event` in the **same** transaction. Preserve company/plant from the source lot.
9. Prevent double expiry: (a) recheck under lock; (b) existing SYSTEM_EXPIRY for that `batch_lot_id` + `source_bucket` skips; (c) **MLB-1** non-negative CHECKs; (d) unique generated/partial unique on one EXPIRED wastage movement per `(batch_lot_id, movement_type)` — MySQL analogue of TDD §13.3.
10. Any failure rolls the lot/bucket transaction back completely. `job_run_lot` records FAILED. The lot remains eligible for the next scheduled run. Deadlock retry only if the boundary is marked idempotent (`TransactionManager`, max 3).
11. **Never** create an IRN background job.

Reserved-for-reassignment: cancel/invalidate not-yet-approved planned reassignment in the same lock (TDD §13.2 Step 2) and set `invalidated_reassignment_note`. If reassignment tables are not yet seeded, the AVAILABLE and REJECTED_PENDING buckets still process.

---

## 5. Corrected constraint / projection register

**Do not reuse G1 for lot balance.** Phase 0 `docs/PROJECTION_TABLES.md` candidate table that labelled G1 as lot quantity is **withdrawn**. Established meanings:

| ID | Exact business invariant | Original PostgreSQL / DB-Arch identifier | MySQL enforcement | Tables | Lock order | Database-level guard | Service/domain validation | Concurrency test | Delivery phase |
|---|---|---|---|---|---|---|---|---|---|
| **G1** | Cumulative credit-note quantity/value (and charges) across notes in `APPROVED`/`PENDING_IRN`/`IRN_GENERATED` never exceeds the original invoice eligible quantity/value | DB Arch §13 constraint trigger on `credit_note_line` / `credit_note_charge_line`; §20.2 | Same-Tx `FOR UPDATE` on original `invoice_header` then lines in ascending `invoice_line_id` / `invoice_charge_line_id`; service sum; **no** same-table trigger (error 1442) | `credit_note_*`, `invoice_header`, `invoice_line`, `invoice_charge_line` | Invoice header, then lines ascending | Optional later projection **only** if documented before migration; not G-renamed | Domain cumulative check at approval and again on successful CN IRN | Concurrent two-credit-note approval | **Not this UI slice** (no credit-note screens). Do not install G1 tables “because Super Admin has all permissions”. |
| **G2** | Combined disposition quantity never exceeds (and at Fully Disposed equals) `rejected_quantity`; a unit is never disposed twice | DB Arch §13 constraint trigger `SUM(disposition quantity) ≤ rejected_quantity` on `disposition_record`; DSP-BR-001 | Ordered `FOR UPDATE` on `rejected_dispatch` then `disposition_record`; service exclusive-path check | `rejected_dispatch`, `disposition_record`, `reassignment`, `wastage_record` (USER_RECORDED from rejected) | Parent rejection, then disposition rows by id | CHECK qty > 0; application sum; MySQL generated unique if a single open disposition counter is introduced | Split paths 3+7+2=12 | Concurrent split-disposition | **Backend seed in Phase 1** if ledger +3 / WST −2 are written; **DSP UI verification reserved** |
| **G3** | Persisted invoice totals match the calculation engine (8-step order, taxable = product subtotal − discount + ordinary charges); `grand_total = pre_roundoff_total + round_off_adjustment`; round-off always displayed including ₹0.00 | TDD §9; DB Arch `invoice_header` CHECKs on totals; discount/charge cross-row rules §22.1 | CHECKs on header totals; service recomputation on save; no same-table trigger | `invoice_header`, `invoice_line`, `invoice_charge_line` | Header then lines/charges by id | Header CHECKs | `CalculationContract` / BCMath | Concurrent edit vs approve | **Phase 1** (CHL-SCR-002 / INV-SCR-003) |
| **MLB-1** | Lockable current-balance projection is **not** source of truth; buckets ≥ 0; Available + Rejected-pending + Reserved-reassignment = current on-hand; updated in the same transaction as `material_movement` | DB Arch §10.4 `material_lot_balance`; INW-FR-003/005. **No G-code in source docs** — new id **MLB-1** | Composite PK `(company_plant_id, product_id, batch_lot_id)`; CHECK each bucket ≥ 0; write in same Tx as movement | `material_lot_balance`, `material_movement`, `inward_entry` | `(company_plant_id, product_id, batch_lot_id)` | Bucket CHECKs; scheduled reconcile-from-ledger (DB Arch §10.4) is out of this UI slice but the projection must not drift in Phase 1 writes | Re-validate qty under lock before allocation/wastage/expiry | Concurrent allocation vs expiry vs inward | **Phase 1** |
| **U1** | At most one current vehicle assignment | Partial unique `UNIQUE(vehicle_id) WHERE assigned_until IS NULL` | Stored generated column + UNIQUE (Phase 0 pattern) | `vehicle_supplier_assignment_history` | Vehicle id | Generated UNIQUE | Service | Concurrent assign | When vehicle assignment is seeded/used |
| **U2** | At most one live billing link per challan | Partial unique `UNIQUE(challan_id) WHERE link_status IN ('DRAFT_RESERVED','FINAL_ACTIVE')` | Generated unique | `invoice_challan_link` | Challan ids ascending (Tx 27.1) | Generated UNIQUE | Service recheck under lock | Concurrent invoice create | **Phase 1** |
| **U5** | One active rate per scope (company-specific and universal may coexist) | Split partial uniques on `rate_master` | Generated CONCAT + UNIQUE allowing NULL inactive | `rate_master` | Scope key | Generated UNIQUE | Rate resolution PRC-FR-001–004 | Concurrent rate activate | **Phase 1** (selectors) |
| **NUM-CH / NUM-INV / NUM-WST** | Company+FY+DocType uniqueness; CHALLAN/CREDIT_NOTE/WASTAGE never `AVAILABLE_FOR_REUSE`; invoice controlled reuse only | `document_number_registry` composite UNIQUEs; CONSUMED partial unique on `numbering_history` | UNIQUE + generated partial unique; `FOR UPDATE` on `document_sequence` | registry, sequence, consuming header | Sequence row then registry | UNIQUE; CHECK reuse state is INVOICE-only | Numbering service | Concurrent issue | **Phase 1**; **challan visible-number timing blocking** (§7, §11) |
| **REJ-CUM** | Cumulative rejected qty ≤ challan line dispatched qty | §13 trigger on `rejected_dispatch` | Lock line; service sum | `rejected_dispatch`, `challan_line` | Challan line then rejection | Application + tests | — | Concurrent rejection | Backend seed if REJ-2026-000044 is written |
| **APPEND** | No UPDATE/DELETE on ledger/audit/IRN attempts | PG REVOKE | App insert-only; document cPanel privilege | `material_movement`, `audit_event`, `irn_attempt`, `numbering_history` | n/a | No update/delete in repositories | — | Attempted update fails | **Phase 1** |
| **SKIP-LOCK** | Expiry workers do not wait on locked lots | Native `FOR UPDATE SKIP LOCKED` | Native | `material_lot_balance` | MLB-1 key | n/a | Recheck after lock | Two concurrent job processes | **Phase 1** expiry |

---

## 6. Exact sample-data and reconciliation matrix

Required identifiers (seeds **and** tests **and** RPT-SCR-003):

| Kind | Identifier |
|---|---|
| Challan | `CH-2026-000481` |
| Invoice | `INV-2026-000119` |
| Vendor inward | `INW-2026-000212` |
| Wastage | `WST-2026-000067` |
| Lots | `LOT-2026-00212`, `LOT-2026-00230` |

Visible LOT-* strings are display labels. Canonical lot key remains `inward_entry.inward_entry_id`.

### Allocation (CH-2026-000481 line 1, M30 RMC)

| Lot | Allocated |
|---|---:|
| LOT-2026-00212 | 18.000 MT |
| LOT-2026-00230 | 12.000 MT |
| **Total** | **30.000 MT** |
| LOT-2026-00230 remaining after allocation | **13.000 MT** (25.000 produced − 12.000 allocated) |

LOT-2026-00212 is **fully** allocated (18.000 of 18.000 available on that lot).

### Material Ledger — M30 RMC, Apex RMC, Chakan Plant — must close at **116.000 MT**

| Line | Qty (MT) | Running |
|---|---:|---:|
| Opening | 120.000 | 120.000 |
| Inward (production `INW-2026-000230`) | +25.000 | 145.000 |
| Consumption/despatch (`CH-2026-000481`) | −30.000 | 115.000 |
| Return/recovery (from `REJ-2026-000044`, movement `RETURNED_TO_AVAILABLE`) | +3.000 | 118.000 |
| Wastage (`WST-2026-000067`, reason Contaminated, USER_RECORDED Approved) | −2.000 | **116.000** |

Report, database seed, and automated test must all equal **116.000 MT**. No second closing figure.

### Disposition (DSP-BR-001) — 12.000 MT rejected from CH-2026-000481 M30, Fully Disposed

| Path | Qty (MT) | Authoritative movement |
|---|---:|---|
| Return to availability | 3.000 | `RETURNED_TO_AVAILABLE` (this is the ledger +3.000). **Not** a new “return to vendor” movement — that type is not in the catalogue. |
| Reassign | 7.000 | `REASSIGNMENT_RESERVATION` (+ later `REASSIGNED_DISPATCH` only if a replacement challan is seeded). User label “Reassign to Product” maps to this path, not a product-master rewrite. |
| Record as Wastage | 2.000 | Same `WST-2026-000067` / `WASTAGE_FROM_REJECTED` as the ledger −2.000 |
| **Total rejected/disposed** | **12.000** | Final state: **Fully Disposed** |

**Label conflict (not silently renamed in data):** Task 4 says “Return to Vendor: 3.000 MT”. BRD DSP-BR-001 and Blueprint §8 say **return to availability** 3.000, and the ledger line is “Return from REJ-2026-000044 +3.000”. Quantities are binding. Path is **Return to availability**. See §11 if “Return to Vendor” must mean a different movement.

### Document / lot binding (avoid Blueprint’s LOT-2026-00212 collision)

Blueprint §8 uses LOT-2026-00212 both for OPC 53 Cement 40.000 on `INW-2026-000212` **and** for M30 18.000 opening/allocation. That mixing is **not** seeded.

| Identifier | Phase 1 seed meaning |
|---|---|
| `INW-2026-000212` | Vendor inward (OPC 53 Cement and other vendor lines as needed for INW-SCR-001). **Cement lots are not LOT-2026-00212.** |
| `LOT-2026-00212` | M30 RMC lot with **18.000 MT** available used in CH-2026-000481 (part of opening 120.000, not extra inward). |
| `INW-2026-000230` / `LOT-2026-00230` | Production inward M30 **25.000 MT**. |
| Opening 120.000 | M30 at PL1 as at 01/09/2026. May use additional opening lots besides 00212 so that 18+others=120. Do not use 18.000 as the ledger opening. |
| `WST-2026-000067` | USER_RECORDED, Approved, 2.000 MT, Contaminated, from REJ-2026-000044. Raised U2 / approved U1 in seed history. **Not** created by the expiry job. |
| `INV-2026-000119` | Immediate invoice from CH-2026-000481. IRN recorded 06/09/2026 15:04 IST, acknowledgement `112026000123456` for INV-SCR-008. |

**Out of Phase 1 UI, do not seed contradictory extras:** `CRN-2026-000031`, `INV-2026-000122`, `RES-000014`, `CH-2026-000478` are Blueprint neighbours. Omit unless a frame on the eight screens requires them. Do not change 116.000.

DSP/WST **screens** are out of this slice. Seed the backend rows the ledger, allocation drawer, and expiry tests need. Full DSP/WST visual verification is reserved.

---

## 7. Numbering lifecycle matrix

**Rule used here:** do not assign a permanent visible number merely because a draft row needs a primary key. Surrogate `*_id` is the draft internal identity (DB Arch §11).

### Invoice — previously approved reservation lifecycle (INV-BR-001/002/003, TDD §6.3, DB Tx 3–6, 10)

This is **not** silently substituted with “number on first PK”.

| Topic | Authoritative rule |
|---|---|
| Draft internal identity | `invoice_id` (surrogate). Never the visible INV-* string. |
| When the visible invoice number is **reserved** | INV-BR-002: authorised user (or the immediate-invoice path that holds a number) reserves the next sequential number against a **fixed** client and challan set. Registry: ISSUED → RESERVED. Party/challans cannot change on that reservation cycle. |
| When it is **consumed** | Attached to a created `invoice_header` (TDD “Consumed”; DB Tx 5 direct-create ISSUED→CONSUMED, or Tx 6 RESERVED→CONSUMED). `invoice_number` is the immutable snapshot. `document_number_registry_id` is set in that same transaction. |
| Unsaved form | No `invoice_header` row, no number. |
| Release and reuse | INV-BR-003 **only**: unused reservation cancelled → RELEASED, number `AVAILABLE_FOR_REUSE` for a **new** reservation cycle (possibly different party/challans). Pre-IRN cancelled invoice likewise releases. Sequence counters never move backward. Original reservation/invoice rows remain forever. |
| Before IRN | Cancel / release allowed per INV-BR-003/004/007. Linked challans revert per billing-link rules. |
| After IRN | Number **permanently** non-reusable. Invoice commercial fields immutable. Credit note is a separate document/number. |
| Scope | Company + Indian FY (1 Apr–31 Mar) + document type Invoice. Plants share the company sequence. |

CHL-SCR-002 immediate invoice uses this lifecycle (reserve-or-direct-consume at invoice persist), **not** challan’s disputed timing.

### Challan — **blocking** (see §11)

Sources **conflict** after stating each rule. Precedence would pick BRD, but FRIS/Blueprint were not withdrawn. This plan **does not choose**.

| Source | Visible CH-* issued | Draft identity |
|---|---|---|
| BRD **CHL-BR-001** | Trigger: “A challan is **saved for the first time**.” Non-reuse once issued to a saved challan (CHL-BR-002). | `challan_id`; visible number at first save |
| DB Arch Tx 2 / `challan_header.document_number_registry_id NOT NULL` | Issue-and-consume at **challan_header INSERT** (create). Never `AVAILABLE_FOR_REUSE`. | Same |
| FRIS **CHL-SCR-002-FLD-003** | Default `[Assigned on Approve]` | Implies no visible number while Draft |
| Blueprint action matrix | “Challan APPROVED, **number issued at this point**”; OV-05 names the number to be issued | Draft without CH-* |
| FRIS DSP-SCR-002-FLD-004 | `[Assigned on Save]` for a **new** reassignment challan — **internal FRIS contradiction** with FLD-003 | — |

| Topic | Approved rule (Final Decision 1) |
|---|---|
| Draft internal identity | `challan_id` (surrogate PK). |
| Visible Challan number permanently issued | **First successful persist/Save**, same transaction as registry consume (CHL-BR-001 + Tx 2). FRIS/Blueprint “on Approve” is withdrawn. |
| Reserved / consumed / released | Challan has **no** reservation. ISSUED→CONSUMED only. **Never** released or reused **once committed**. A rolled-back Save is not an issued number; the same next_value may be used next. That is not reuse of an issued gap. |
| Before / after IRN | IRN is an invoice concern. Cancelled challan **keeps** its number. |
| Scope | Company + FY + Challan. No plant fork. |

### Wastage (backend only)

| Topic | Rule |
|---|---|
| Internal identity | `wastage_id` |
| Visible WST-* | Issue-and-consume in the same transaction that inserts `wastage_record` (Tx 13), including SYSTEM_EXPIRY |
| Reuse | Never |
| Scope | Company + FY + Wastage |

### Inward

No company document sequence for vendor inward in DB Arch. Display `INW-2026-000212` is an illustrative reference (OD-B3-04 still notes format). Internal lot identity = `inward_entry_id`, labelled LOT-* on Save.

---

## 8. Permission provenance matrix

Super Admin receiving the whole **existing** catalogue is **not** a reason to invent a code.

| Proposed code | Trace | Verdict |
|---|---|---|
| **`INVOICE.SET_MANUAL_RATE`** | BRD §14.1 distinct `Invoice – Set Manual Rate`; Billing User matrix; INV processing rules; FRIS CHL-SCR-002-FLD-013; Blueprint “Set Manual Rate”; DB Arch `invoice_line.manual_rate_amount` | **Authoritative — add** in Phase 1 with role mappings and tests. |
| **`AUDIT.EXPORT`** | Approved CSV/Figma ExportRestricted state; Final Decision 2 (12 Sep 2026) | **Add.** Super Admin only in Phase 1 seed. `REPORT.EXPORT` does not imply it. |
| `REPORT.EXPORT` | BRD RPT-FR-002, already in Phase 0 catalogue | Keep. Does **not** grant audit-log export. |

### `INVOICE.SET_MANUAL_RATE` role mapping (when implementation is approved)

| Role | Mapping |
|---|---|
| Billing Clerk | Grant (BRD Billing User). |
| Accounts Manager | Do **not** infer. BRD lists Set Manual Rate on Billing User, not Accounts. Hide the control. |
| Plant Manager | Do **not** widen. No `INVOICE.SET_MANUAL_RATE`. |
| Store Keeper | No. |
| Super Admin | Receives it **only after** the code exists in the catalogue. |

Tests: user without the permission sees no Set Manual Rate control; missing rate blocks approval (CHL-ERR-008); manual entry is audited.

### AUD-SCR-001 until `AUDIT.EXPORT` is approved

- `AUDIT.VIEW` remains the only audit permission.
- Export control is **absent** (frame `D-AUD-SCR-001-View-PermissionRestricted` is still renderable: export absent).
- Do not silently reuse `REPORT.EXPORT` for the audit log.
- Adding `AUDIT.EXPORT` is listed in §11.

---

## 9. Updated automated-test plan

Keep Phase 0 suite green (64 tests as of close). Add Phase 1 tests only after implementation approval.

| Area | Tests |
|---|---|
| Frames | Every one of the **88** frames renders (HTTP feature or view fixtures), including the three INW compatibility states with the **exact** sentence and **no** Approve/Return/status controls or stored approval enums. |
| Inward Save | One transaction: `inward_entry` + `VENDOR_INWARD` movement + **MLB-1** + audit. No status column. Concurrent Save → OV-11. Inactive vendor still displays on historical inward; selector lists active only. |
| Expiry | Eligible UTC Use-By; `SKIP LOCKED`; recheck; one lot/bucket Tx; SYSTEM_EXPIRY already Approved; exact bucket reduction; movement + audit same Tx; second run does not double-waste; failure rolls back; two workers skip locked rows; **no** IRN job class/cron. SYSTEM actor. Company/plant isolation on shared Chakan. |
| Ledger | Seed as §6. RPT-SCR-003 closing **116.000 MT**. Allocation 18+12=30; lot 00230 remaining 13.000. |
| G3 | Engine vs stored totals; round-off ₹0.00 displayed; concurrent save/approve. |
| U2 | Concurrent invoice linking the same challan. |
| MLB-1 | Concurrent allocation vs expiry; CHECK rejects negative buckets. |
| Invoice numbering | Reserve → consume; release unused; pre-IRN cancel reuse; IRN-generated never reused; CHALLAN/WASTAGE never reused. **Challan visible-number tests wait on §11.** |
| SET_MANUAL_RATE | Present/absent by role; cannot approve blank rate. |
| AUDIT.EXPORT | **Not** asserted as a catalogue code. Export control absent without approval. |
| IRN | Manual `irn_attempt` only; Failed is a recorded attempt/document treatment per FRIS/Figma (not a queue). No auto-retry. UX-DEC-004 exclusive statutory deadline on INV-SCR-008. |
| Masters | FKs to real rows; Chakan shared without mixing company inventory; GSTIN / document codes / vehicle registration `as_cs`; names `ai_ci`. |
| Permissions | Active role never unions. Plant Manager does not gain `INVOICE.APPROVE` from Figma copy. |

---

## 10. Updated entry / exit criteria

### Entry (already met)

- Phase 0 approved and closed: auth, IAM, sessions (`MySQLiHandler`), audit writer, tokens, layouts, calculation contract, `TransactionManager`, expiry command **stub**, 64 tests.
- Sample identities unchanged (R. Kumar Plant Manager; S. Patil Store Keeper + Billing Clerk, one active; A. Deshpande Accounts Manager; N. Joshi Super Admin).
- This corrected plan reviewed.

### Exit (Phase 1 implementation, after approval of this plan **and** of §11 blockers)

- All **88** CSV frames renderable, **including** INW compatibility states with the approved sentence and no false inward approval data.
- Vendor inward posts on Save; no inward approval FSM; no Approve/Return controls on INW-SCR-001.
- `inventory:process-expiry` operational per §4 (not a no-op once **MLB-1** exists). No IRN job.
- Constraint register §5 installed for Phase 1 rows (G3, MLB-1, U2, U5, APPEND, SKIP-LOCK, numbering as decided). G1 not installed. G2 enforced on any seeded disposition rows.
- Seeds/tests/report reconcile to **116.000 MT** and the allocation/disposition quantities in §6.
- Invoice numbering follows INV-BR-002/003. Challan visible-number timing matches the **approved** §11 decision.
- `INVOICE.SET_MANUAL_RATE` in catalogue with mappings and tests. `AUDIT.EXPORT` only if §11 approves it.
- Masters: real tables/repos/seed; **no** catalogue CRUD screens; FK/selector/inactive/history/Chakan/collation rules in § Task 7.
- No WST/DSP/credit-note/master CRUD routes. No mock APIs / localStorage application records.
- Automated tests in §9 pass.

### Stop condition

**Do not implement Phase 1 until this plan is approved**, including explicit decisions in §11.

---

## 11. Remaining blocking decisions

**None for Phase 1.** Final Decisions 1–4 (12 Sep 2026) resolved challan numbering, `AUDIT.EXPORT`, Return to Availability terminology, and INV-SCR-008 parent-invoice state mapping.

Non-blocking (recorded, not waiting):

- OD-B3-04/05 inward reference format / duplicate vendor document — Blueprint placeholders stand.
- Credit-note G1 and CRN-2026-000031 — out of slice.
- DB Arch expiry `use_by <= now()` vs TDD “has passed” — this plan uses exclusive `<` UTC.
- Phase 0 `ExpiryProcessor` column names — rewrite to dictionary names when implementation starts.
- User-recorded wastage screens — out of slice; tables exist for SYSTEM_EXPIRY and seeded WST-2026-000067.

---

## Master-data boundary (Task 7) — confirmation

| Rule | Phase 1 |
|---|---|
| Real tables, repositories, development seed | Yes |
| CRUD routes / catalogue screens | **No** |
| Transaction FKs | Point at real master rows |
| Selectors | Active records from MySQL |
| Historical txns | Still display inactive referenced records |
| Company/plant mapping | Enforced on every write (`company_plant_id`, never plant alone) |
| Chakan Plant | Shared physical plant; inventory **not** mixed across companies |
| GSTINs, document codes, vehicle registration | `utf8mb4_0900_as_cs` |
| Human-readable names | `utf8mb4_0900_ai_ci` (table default); unique name columns stay as Phase 0 already defined |

---

*End of approved and closed Phase 1 plan. Implementation followed Final Decisions 1–4 and safeguards 1–2. Phase 2 is specified in `docs/PHASE2_PLAN.md` and must not start until that plan is approved.*
