# Phase 1 operational route matrix

Phase 1 is approved and **closed**. Invoice approval is a separate action from Challan approval, but the Challan must be Approved first.

Production has **zero** `/seed`, `/fixture`, `/demo`, `/probe`, or `/test` HTTP shortcuts. Record pages use `/resource/{id}` and load the row from MySQL. Development data stays in `DatabaseSeeder`. Browser checks look up seeded IDs (`CH-2026-000481`, `INV-2026-000119`, `INW-2026-000212`) through repositories.

`?frame=` is visual review in `development` and `testing` only. Production ignores it. Overlay never writes and never overrides permissions or persisted status.

All POST routes below are covered by the global CSRF filter.

## Production routes

| Method | Path | Controller | Permission | Status / domain guard | Transaction service | Success |
|---|---|---|---|---|---|---|
| GET | `/inward/vendor` | `InwardController::create` | `INWARD.VIEW` or `INWARD.ADD` | None (create form) | — | HTML |
| POST | `/inward/vendor` | `InwardController::store` | `INWARD.ADD` | Quantity/plant/vendor required | `InwardService::postVendorInward` | Redirect `/inward/vendor/{id}` |
| GET | `/inward/vendor/{id}` | `InwardController::show` | `INWARD.VIEW` or `INWARD.ADD` | 404 if missing | `InwardService::find` | HTML |
| GET | `/challans` | `ChallanController::index` | `CHALLAN.LIST` | Company/plant scope | `ChallanService::listForUser` | HTML |
| GET | `/challans/create` | `ChallanController::create` | `CHALLAN.ADD` or `CHALLAN.VIEW` | — | — | HTML |
| POST | `/challans` | `ChallanController::store` | `CHALLAN.ADD` | Lines required | `ChallanService::saveNew` | Redirect `/challans/{id}` |
| GET | `/challans/{id}` | `ChallanController::show` | `CHALLAN.VIEW` | 404 if missing | `ChallanService::load` | HTML |
| POST | `/challans/{id}` | `ChallanController::update` | `CHALLAN.EDIT` | DRAFT / RETURNED_FOR_CORRECTION; row version | `ChallanService::updateDraft` | Redirect `/challans/{id}` |
| POST | `/challans/{id}/submit` | `ChallanController::submit` | `CHALLAN.EDIT` | DRAFT / RETURNED_FOR_CORRECTION | `ChallanService::submitForApproval` | Redirect `/challans/{id}` |
| POST | `/challans/{id}/approve` | `ChallanController::approve` | `CHALLAN.APPROVE` | DRAFT / PENDING / RETURNED; allocation = dispatched | `ChallanService::approve` | Redirect `/challans/{id}` |
| POST | `/challans/{id}/return` | `ChallanController::returnForCorrection` | `CHALLAN.APPROVE` | PENDING_APPROVAL; reason required | `ChallanService::returnForCorrection` | Redirect `/challans/{id}` |
| POST | `/challans/{id}/cancel` | `ChallanController::cancel` | `CHALLAN.CANCEL` | DRAFT/PENDING/RETURNED, or APPROVED+UNLINKED. Block BILLED / linked. Number stays CONSUMED | `ChallanService::cancel` | Redirect `/challans/{id}` |
| POST | `/challans/{id}/invoice` | `ChallanController::createInvoice` | `INVOICE.ADD` | Challan APPROVED; U2 live link; `FOR UPDATE` | `InvoiceService::createFromChallan` | Redirect `/invoices/{id}` |
| POST | `/challans/{id}/invoice/approve` | `ChallanController::approveInvoice` | `INVOICE.APPROVE` | Invoice approval is a separate action from Challan approval, but the Challan must be Approved first. Invoice DRAFT/PENDING/RETURNED | `InvoiceService::approveFromChallan` | Redirect `/invoices/{id}` |
| GET | `/invoices` | `InvoiceController::index` | `INVOICE.LIST` | Scope via linked challan plants | `InvoiceService::listForUser` | HTML |
| GET | `/invoices/{id}` | `InvoiceController::show` | `INVOICE.VIEW` | 404 if missing | `InvoiceService::load` | HTML |
| POST | `/invoices/{id}/submit` | `InvoiceController::submit` | `INVOICE.EDIT` | DRAFT / RETURNED | `InvoiceService::submitForApproval` | Redirect `/invoices/{id}` |
| POST | `/invoices/{id}/approve` | `InvoiceController::approve` | `INVOICE.APPROVE` | Invoice draft/pending/returned; linked challans APPROVED + `FOR UPDATE` | `InvoiceService::approve` | Redirect `/invoices/{id}` |
| POST | `/invoices/{id}/return` | `InvoiceController::returnForCorrection` | `INVOICE.APPROVE` | PENDING_APPROVAL; reason | `InvoiceService::returnForCorrection` | Redirect `/invoices/{id}` |
| POST | `/invoices/{id}/cancel` | `InvoiceController::cancel` | `INVOICE.CANCEL` | Not IRN_GENERATED | `InvoiceService::cancelPreIrn` | Redirect `/invoices/{id}` |
| GET | `/invoices/{id}/irn` | `InvoiceController::irn` | `INVOICE.VIEW` or `INVOICE.RECORD_IRN_DETAILS` | Parent status eligibility | `IrnRecordingService::eligibilityReason` | HTML |
| POST | `/invoices/{id}/irn` | `InvoiceController::recordIrn` | `INVOICE.RECORD_IRN_DETAILS` | PENDING_IRN / APPROVED_PRE_IRN / IRN_FAILED; append-only | `IrnRecordingService::record` | Redirect `/invoices/{id}/irn` |
| GET | `/reports/material-ledger` | `LedgerController::index` | `REPORT.VIEW` | Plant scope | `LedgerQuery::query` | HTML |
| POST | `/reports/material-ledger/export` | `LedgerController::export` | `REPORT.EXPORT` | Same company/plant/product/date filters as the visible report | `ExportJobService::requestMaterialLedger` | Redirect; job `REQUESTED` |
| GET | `/audit` | `AuditController::index` | `AUDIT.VIEW` | Company grant | `AuditQuery::list` | HTML |
| POST | `/audit/export` | `AuditController::export` | `AUDIT.EXPORT` | Super Admin in seed | `AuditQuery::requestExport` | CSV download |

Also: `GET/POST /login`, `POST /logout`, `POST /session/switch-role`, `GET /`, `GET /session/context`, `GET /health`.

## Not in Phase 1 (cited)

| Asked | Verdict | Source |
|---|---|---|
| `POST /inward/vendor/{id}/cancel` | **Not implemented.** Vendor inward posts on Save; no approval FSM; INW-SCR-001 frames are Save/view only; posted inward is inventory source of truth. `INWARD.CANCEL` remains in the catalogue for a later type. | `docs/PHASE1_PLAN.md` §2; BRD inward approval removed; Blueprint 12.24 |
| WST / DSP / master CRUD / credit-note routes | Out of slice | Plan §3, §10, §11 |
| IRN queue / `exports:process` generating IRN | Forbidden | Plan safeguards; INV-SCR-008 manual only |

## Visual frame simulation

| Environment | `?frame=` |
|---|---|
| `production` | Ignored. Operational UI from MySQL + active-role permissions. |
| `testing` | Read-only overlay for all 88 frames. No banner. POST still uses persisted status and permissions. |
| `development` | Same overlay plus a visible “Development visual preview” banner. |

## Tests

- `ProductionRouteAbsenceTest` — seed/probe paths absent; HTTP 404
- `Phase1FrameHttpTest` — 88 frames via `/resource/{id}`
- `FrameSimulationTest` / `FrameSimulationHttpTest` — production ignore; permission 403 despite frame; invalid IRN POST
- `ImmediateInvoiceAndChallanLifecycleTest` — ADD/APPROVE split, U2, cancel. Invoice approval is a separate action from Challan approval, but the Challan must be Approved first.
- `LedgerExportTest` — `REPORT.EXPORT`, `export_job`, spark processing
- `WastageNumberingFkTest` — FK restrict, unknown id, SYSTEM_EXPIRY, rollback
- `NumberingLifecycleTest` — rollback next_value; two creates cannot share a number

Phase 2 is specified in `docs/PHASE2_PLAN.md` and is not approved for implementation.
