# Phase 5 plan (Dashboard and Reporting) — implemented

**Status:** Business Owner decisions **P5-D1 through P5-D17 are approved and binding.** **Phase 5 is implemented.** **Phase 6 is not started.**  
**Closed baseline:** Phase 0–4 annotated tag `phase-4-final-approved` on `7e6a00d2dd022d5b533d007c2f9c0cbb2fea66dc`. Suite **233 tests / 7,234 assertions / 0 failures.**  
**Stack (locked):** CodeIgniter 4, PHP 8.2+, MySQL 8 InnoDB, server-rendered CI4 Views, existing CSS/JS, existing Phase 0–4 responsive shell. `php spark exports:process`. PHPUnit.

Do not introduce React, Next.js, a Node frontend, PostgreSQL, a BI platform, JWT, Shield, Alankit, or another stack.

**This document remains the binding specification.** Implementation lives in the CodeIgniter 4 application (Dashboard, 13 reports, ExportJob threshold/snapshot/download, 99 frames).

---

## Binding decisions (P5-D1–P5-D17)

| ID | Title | Binding outcome |
|---|---|---|
| **P5-D1** | Export threshold | 1–5,000 rows synchronous; 5,001+ asynchronous; configurable default 5,000; server-side count only; async must stream/chunk; existing ExportJob retry policy; never IRN terminology |
| **P5-D2** | Date range | No maximum range; From ≯ To; paginated UI; no ten-year PHP loads; Asia/Kolkata **half-open** UTC intervals |
| **P5-D3** | GST Summary scope | IRN status **GENERATED** invoices (+GST) and Credit Notes (−GST) only |
| **P5-D4** | CN GST reduction | Snapshot signed reduction; intra CGST+SGST / inter IGST; CRN-031 amounts below; Invoice 119 not mutated |
| **P5-D5** | Inventory Position map | §12; **REVERSAL is Adjustments, not skipped**; bucket transfers do not change Closing; unknown types fail |
| **P5-D6** | Adjustments | Signed REVERSAL (+ formally approved correction types only); drill to source + reversed movement |
| **P5-D7** | IRN failure KPI | Document count; latest FAILURE; still Pending IRN; still needs recording; no later success |
| **P5-D8** | CN awaiting IRN | APPROVED + PENDING_IRN only; one count per Credit Note |
| **P5-D9** | Seeded permissions | Existing matrix unchanged; permission-driven Quick Actions, not role-name hard-coding |
| **P5-D10** | RPT-013 export | `REPORT.VIEW` / `REPORT.EXPORT` only; **do not create, seed, or infer `AUDIT.EXPORT`** |
| **P5-D11** | Branding snapshot | Listed fields onto ExportJob at request; later master edits must not change generated files |
| **P5-D12** | Material Ledger | Integration/regression only; keep `LedgerQuery`, routes, ExportJob, 116.000 MT; allowed presentation/state/403/download alignment |
| **P5-D13** | Empty copy | Headline + supporting + `Clear all filters` identical on all 13 reports |
| **P5-D14** | Dashboard | Six KPIs, three possible Quick Actions, collapse gaps; **`/` is authenticated Dashboard** |
| **P5-D15** | No valuation | No cost, profit, margin, COGS, GL, financial inventory values; RPT-006 physical qty only |
| **P5-D16** | CSV / frames | Use the 99 names FR-0470–FR-0568 from the approved Phase 5 prompt; CSV may remain external; no invented aliases |
| **P5-D17** | Export files | 30-day configurable expiry; download auth + VIEW + live scope; opaque ids; outside public root; audit; no public/shareable URLs or emails |

The earlier plan’s proposal to **skip `REVERSAL`** is **rejected** (P5-D5 / P5-D6).

---

## 1. Executive scope and exclusions

### In scope

- **DSH-SCR-001** at `/` after login: six KPI cards; three permission-driven Quick Actions; 1440×1024 / 1024×768 / 390×844.
- **RPT-SCR-001 … RPT-SCR-013** with viewing, filtering, pagination, export, and the seven (or eight) CSV states.
- **99 frames** FR-0470–FR-0568 (83 desktop, 14 mobile, 2 tablet).
- **RPT-SCR-003** integration only (P5-D12).
- Reuse **ExportJob** + `php spark exports:process` with P5-D1 / P5-D11 / P5-D17.
- Company/plant isolation, active-role IAM, `DisplayTime::formatMoney`, Asia/Kolkata half-open UTC bounds.

### Out of scope / frozen

- Automatic IRN, IRN queue, IRN retry, Alankit, “Generate IRN.”
- Credit Note cancel/delete/void; G1 and reservation rule changes.
- Inventory valuation, unit cost, COGS, profit, margin, GL postings (P5-D15).
- Fabricated reports (Delivery Summary, Payment Ageing, Vehicle Utilisation, Profitability).
- Per-report permission codes.
- Creating, seeding, or inferring **`AUDIT.EXPORT`** in Phase 5 (P5-D10). RPT-SCR-013 never uses it. Phase 5 does not modify Phase 1 `PermissionCatalog` / seeders.
- Scheduled or emailed reports; publicly shareable export URLs.
- Production `?frame=`; seed/demo/fixture/probe/test routes.
- Rebuilding Phase 0–4 tables.
- `INWARD.APPROVE`. `INWARD.REVERSE` remains Store Keeper + Super Admin.
- Phase 6.

### Frozen operational figures

| Item | Value |
|---|---|
| Ledger / Inventory Position closing (Apex RMC / Chakan / M30) | **116.000 MT** |
| 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** |
| WST-2026-000067 | Approved |
| INV-2026-000119 | **₹177,592.00** stored and displayed (not mutated by GST Summary) |
| CRN-2026-000031 | **₹75,089.00**; GST reduction in §13 |

---

## 2. Source-precedence register

| Rank | Source |
|---|---|
| 1 | **P5-D1–P5-D17** (this document) |
| 2 | BRD v0.9.22 |
| 3 | Approved DB addenda |
| 4 | TDD v0.4.1 where it does not contradict BRD |
| 5 | Database Architecture v1.2 → MySQL 8 InnoDB |
| 6 | FRIS v1.0.1 |
| 7 | Figma Production Blueprint v1.1 |
| 8 | Authoritative CSV manifest v1.1 — Frame Name strings |
| 9 | Existing Phase 0–4 implementation except where a Phase 5 decision overrides presentation/integration |

CSV Frame Names are the visual coverage contract. Functional behaviour follows higher ranks.

### CSV treatment (P5-D16)

The authoritative CSV is an **external handoff artefact**. It is not in this git repository. A session search found only generated `writable/exports/*.csv` files, so **no copy was placed** in `docs/`. That **does not block** later implementation: the 99 names and FR-0470–FR-0568 inventory in §3 are canonical. If the physical CSV later appears in the session, copy it read-only into `docs/reference/` without using it as a FrameCatalog key source of aliases.

Internal/Figma aliases remain traceability metadata only.

---

## 3. Exact 99-frame CSV inventory

**99 frames. FR-0470–FR-0568.** Do not rename `Empty` to `NoData`. FrameCatalog uses these strings only.

| Screen ID | D | M | T | Total | FR range |
|---|---:|---:|---:|---:|---|
| DSH-SCR-001 | 5 | 1 | 1 | **7** | FR-0470–FR-0476 |
| RPT-SCR-001 | 6 | 1 | 0 | **7** | FR-0477–FR-0483 |
| RPT-SCR-002 | 6 | 1 | 0 | **7** | FR-0484–FR-0490 |
| RPT-SCR-003 | 6 | 1 | 0 | **7** | FR-0491–FR-0497 |
| RPT-SCR-004 | 6 | 1 | 0 | **7** | FR-0498–FR-0504 |
| RPT-SCR-005 | 6 | 1 | 0 | **7** | FR-0505–FR-0511 |
| RPT-SCR-006 | 6 | 1 | 1 | **8** | FR-0512–FR-0519 |
| RPT-SCR-007 | 6 | 1 | 0 | **7** | FR-0520–FR-0526 |
| RPT-SCR-008 | 6 | 1 | 0 | **7** | FR-0527–FR-0533 |
| RPT-SCR-009 | 6 | 1 | 0 | **7** | FR-0534–FR-0540 |
| RPT-SCR-010 | 6 | 1 | 0 | **7** | FR-0541–FR-0547 |
| RPT-SCR-011 | 6 | 1 | 0 | **7** | FR-0548–FR-0554 |
| RPT-SCR-012 | 6 | 1 | 0 | **7** | FR-0555–FR-0561 |
| RPT-SCR-013 | 6 | 1 | 0 | **7** | FR-0562–FR-0568 |
| **Total** | **83** | **14** | **2** | **99** | |

### DSH-SCR-001

| FR | Exact Frame Name |
|---|---|
| FR-0470 | `D-DSH-SCR-001-View-Default` |
| FR-0471 | `D-DSH-SCR-001-View-Loading` |
| FR-0472 | `D-DSH-SCR-001-View-Error` |
| FR-0473 | `D-DSH-SCR-001-View-Empty` |
| FR-0474 | `D-DSH-SCR-001-View-PermissionRestricted` |
| FR-0475 | `T-DSH-SCR-001-View-Default` |
| FR-0476 | `M-DSH-SCR-001-View-Default` |

### Report frames

Desktop: `D-{SCR}-Report-Default`, `Report-Filtered`, `Report-Empty`, `Report-Loading`, `Report-Error`, `Report-PermissionRestricted`.  
Mobile: `M-{SCR}-Report-Default`.  
RPT-SCR-006 extra: `T-RPT-SCR-006-Report-Default` (FR-0519).

RPT-SCR-003’s seven names already live in Phase 1 `FrameCatalog`. Reuse them; do not alias.

Example RPT-SCR-001: FR-0477–FR-0483 `D-RPT-SCR-001-Report-Default` … `M-RPT-SCR-001-Report-Default`.

---

## 4. Existing RPT-SCR-003 and ExportJob assessment (P5-D12)

**RPT-SCR-003 is integration and regression only. Do not replace `LedgerQuery` with a second contradictory ledger.**

Preserve: `LedgerQuery`, `GET /reports/material-ledger`, `POST /reports/material-ledger/export`, ExportJob material-ledger path, deterministic `effective_datetime_utc` then `movement_id`, closing **116.000 MT**.

| Allowed alignment | Current gap |
|---|---|
| CSV columns Date, Movement Type, Reference, In, Out, Running Balance | View is When / Type / Category / Lot / Qty |
| Exact Empty state (P5-D13) | Different ledger-only sentences |
| Loading, Error, Filtered, PermissionRestricted | Partial overlay; PermissionRestricted copy currently implies no VIEW |
| Mobile cards | Desktop table only |
| Unauthorised export POST **403** | Redirect + flash |
| Authorised download if missing | No download GET; `downloaded_at_utc` unused |
| FrameCatalog keys = CSV names | Already the seven CSV names |
| Explicit classification including `REVERSAL` (P5-D5) | `categoryFor` returns `null` for `REVERSAL`, `REJECTION_RECOVERY`, `REASSIGNMENT_RESERVATION` |

**Classification extension (not a second engine):** add an explicit map for every `chk_mm_type` value. Physical Closing used by running balance / RPT-006 must include **signed `REVERSAL`**. Bucket transfers stay physical-zero. Golden seed has no reversals, so existing Opening 120 / Inward 25 / Despatch −30 / Return 3 / Wastage −2 / Closing 116 remain. A dedicated reversal fixture must change Adjustments and Closing (P5-D5/D6 tests).

ExportJob today: always async, `MATERIAL_LEDGER` only, files under `writable/exports/` (already outside public root), spark `exports:process`, retry = re-run FAILED jobs via spark. **Preserve that retry policy** (P5-D1). Never describe it as IRN retry.

---

## 5. Requirement traceability

| ID | Requirement | Decision |
|---|---|---|
| DSH-KPI-1…6 | Six cards only | P5-D14, D7, D8 |
| DSH-QA | Three possible actions, permission-driven | P5-D9, D14 |
| DSH-ROUTE | Authenticated `/` is Dashboard | P5-D14 |
| TZ | Half-open IST→UTC | P5-D2 |
| RPT-COLS | §7 | Prompt + D12/D15 |
| EMPTY | Shared three-part copy | P5-D13 |
| GST | GENERATED only; CN signed reduction | P5-D3, D4 |
| INV-006 | §12 map; unknown type fails | P5-D5, D6 |
| IAM | Seeded matrix; no AUDIT.EXPORT on 013 | P5-D9, D10 |
| EXP | Threshold, snapshot, download, expiry | P5-D1, D11, D17 |
| VAL | No valuation | P5-D15 |

---

## 6. Dashboard KPI / query definitions (P5-D7, D8, D14)

Active Company + Plant. **Today** = Asia/Kolkata calendar date as UTC half-open `[local midnight, next local midnight)` (P5-D2). Never the PHP default timezone.

| # | Card | Definition |
|---|---|---|
| 1 | Today’s Challans | Count and total quantity of challans whose `effective_datetime_utc` falls in today’s interval, in scope. Exclude `CANCELLED_VOID`. |
| 2 | Today’s Invoices | Count and `formatMoney(SUM(grand_total))` for invoices in today’s interval. Exclude `CANCELLED`. |
| 3 | Unbilled Challans | Count of INV-SCR-001 **eligible** rows (`UnbilledChallanQuery`, no other-user selection lock). |
| 4 | Reserved Invoice Numbers | Count `reservation_status = 'RESERVED'` in scope. |
| 5 | Credit Notes Awaiting IRN | Count distinct Credit Notes with status **APPROVED or PENDING_IRN**. Exclude Draft, Pending Approval, Returned for Correction, IRN Generated. Multiple failed attempts still count **once** (P5-D8). |
| 6 | IRN Recording Failures | Count of **documents** (Invoice **or** Credit Note), not attempts, where: latest authoritative non-superseded manual attempt is **FAILURE**; document remains **Pending IRN**; fresh manual recording is still required; **no later SUCCESS**. Later success **removes** the document (P5-D7). |

Do not add a seventh card. No Recent Activity, Sales, Profit, or valuation KPIs.

### Quick Actions (exactly three possible)

Permission codes — **never role names**:

| Label | Route | Permission | If missing |
|---|---|---|---|
| Create Challan | `/challans/create` | `CHALLAN.ADD` | Remove; collapse |
| Create Invoice | `/invoices/unbilled` | `INVOICE.ADD` | Remove; collapse |
| View Reports | `/reports` | `REPORT.VIEW` | Remove; collapse |

Empty Quick Actions section if none remain. No disabled stubs.

### Routing (P5-D14)

- Authenticated `GET /` → DSH-SCR-001.
- Unauthenticated `/` → existing login redirect (unchanged).
- Direct module routes (`/challans`, `/invoices`, …) unchanged.

### States

- Empty: all six queries succeeded and values are genuine zeros — not an error.
- Error: retrieval failure; no stale numbers; omit failed card values and announce failure.
- Loading: six skeletons.
- Production missing `DASHBOARD.VIEW` → **403**. CSV PermissionRestricted is a development overlay.

---

## 7. Report identity / column / filter matrix

Unchanged from the approved Phase 5 prompt (columns and filters). No net-of-credit invoice column. RPT-006 physical quantity only (P5-D15).

### Date and filter rules (P5-D2)

- Any valid range within retention and authorised scope. **No maximum span.**
- From Date after To Date → reject (inline validation).
- UI always paginated. Export may be larger (P5-D1).
- Do not load ten years of rows into PHP.
- Default periods may be suggested (e.g. current month); users may widen.
- **Boundaries:** local Asia/Kolkata date `D` → UTC `D 00:00:00` **inclusive** through **next local midnight exclusive**. Predicate: `>= startUtc AND < endExclusiveUtc`.
- Filters never broaden Company/Plant. Options scoped. Bound parameters only.
- Filtered chips; clear filters restores default authorised scope (`Clear all filters` on Empty — P5-D13).
- Pagination and export share one normalised filter object.

---

## 8. Report state-rendering matrix

| State | Render |
|---|---|
| Default | Scope line + default filters + rows |
| Filtered | Chips + reduced rows + same scope line |
| Empty | **Headline:** `No data available for the selected filters` **Supporting:** `Adjust the date range or filters and try again.` **Action:** `Clear all filters` — identical casing/punctuation on all 13 reports (P5-D13) |
| Loading | Skeleton columns = that report’s column count (006 = 10); no stale rows |
| Error | Contextual error; no stale rows |
| PermissionRestricted | `REPORT.VIEW` without `REPORT.EXPORT`: data readable; Export **absent**; `Your active role does not have permission to export this report.` |
| No VIEW | HTTP **403**, no payload |

---

## 9. Schema / data-source map

Unchanged table ownership from the prior plan. `invoice_header` still has no stored tax totals; GST uses `InvoiceCalculator` on **immutable snapshots**. No reporting projection tables.

RPT-006 uses `material_movement` with the **P5-D5 classifier**, not a second movement store.

---

## 10. Report query design

Shared `ReportFilter` DTO. Date predicates use half-open UTC (P5-D2). Page size 50 for HTML.

`LedgerQuery` remains owner of RPT-003. Opening-before-range must be SQL-aggregated; then `LIMIT` the window. Export COUNT(*) (or equivalent) uses the **same** filters/scope to choose sync vs async (P5-D1) — never a client-supplied count.

Async workers **stream/chunk**; they must not `getResultArray()` the full set.

---

## 11. Material Ledger reconciliation (P5-D12)

Apex RMC / Chakan / M30, unfiltered:

| Bucket | Golden |
|---|---|
| Opening | 120.000 MT |
| Inward (existing five-card summary) | 25.000 MT |
| Despatch | −30.000 MT |
| Return | 3.000 MT |
| Wastage | −2.000 MT |
| Closing | **116.000 MT** |

Order: `effective_datetime_utc`, `movement_id`.

Screen/export columns: Date, Movement Type, Reference, In, Out, Running Balance. In/Out from signed physical effect. Running balance cumulative in that order. Range queries: SQL opening for `[−∞, from)` then page `[from, to)`.

---

## 12. Inventory Position reconciliation (P5-D5, P5-D6)

Closing formula:

`Opening + Inward + Production − Consumed − Despatched + Returned − Wastage + Adjustments`

### Explicit classification of every current `chk_mm_type`

| `movement_type` | Column / treatment | Physical Closing effect |
|---|---|---|
| `OPENING_STOCK_INWARD` | Opening (up to as-at) | + qty |
| `VENDOR_INWARD` | Inward | + qty |
| `PLANT_PRODUCTION_INWARD` | Production | + qty |
| *(no type)* | Consumed | **0** in Phase 5 (no production-consumption engine) |
| `ALLOCATION_DISPATCH` | Despatched | − qty |
| `RETURNED_TO_AVAILABLE` | Returned | + qty |
| `RETURN_RECOVERY_INWARD` | Returned | + qty |
| `WASTAGE_FROM_AVAILABLE` | Wastage | − qty |
| `WASTAGE_FROM_REJECTED` | Wastage | − qty |
| `WASTAGE_FROM_RESERVED_REASSIGNMENT` | Wastage | − qty |
| `REVERSAL` | **Adjustments** | **Signed** physical effect of the reversal (not skipped) |
| `REJECTION_RECOVERY` | Bucket transfer — disclose only | **0** (do not add again) |
| `REASSIGNMENT_RESERVATION` | Bucket transfer — disclose only | **0** |
| `REASSIGNED_DISPATCH` | Traceability; do **not** double-subtract original physical dispatch | **0** toward Closing |
| **Any unknown future type** | Diagnostic | **Fail the report/test** — do not silently ignore |

Adjustments (P5-D6) contain only:

- Signed `REVERSAL`
- Other **formally approved** correction movement types (none others exist today)

**Not Adjustments:** unexplained differences, rounding, `REJECTION_RECOVERY`, `REASSIGNMENT_RESERVATION`, master-data edits, numbers without a `material_movement` row.

Every Adjustment row must drill to its movement and `linked_original_movement_id`.

### Golden M30 row (as-at now)

| Opening | Inward | Production | Consumed | Despatched | Returned | Wastage | Adjustments | Closing |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 120.000 | 25.000 | 0.000 | 0.000 | 30.000 | 3.000 | 2.000 | 0.000 | **116.000** |

120+25+0−0−30+3−2+0 = 116. Tests must split `VENDOR_INWARD` vs `PLANT_PRODUCTION_INWARD` vs `RETURN_RECOVERY_INWARD` so Inward 25 / Production 0 / Returned 3 hold.

RPT-003 and RPT-006 both close **116.000 MT** on this snapshot. A reversal fixture must change 006 Adjustments and Closing; 003 running balance uses the same physical classifier so they cannot diverge.

---

## 13. Financial / GST reporting (P5-D3, P5-D4)

- `Decimal` + `DisplayTime::formatMoney`. Invoice Register shows original INV-119 **₹177,592.00**. GST Summary does **not** mutate the invoice.
- Intra-state CN/invoice: CGST+SGST; IGST ₹0.00. Inter-state: IGST only. Never convert types.

### RPT-SCR-009 statutory basis (disclose in scope text)

Eligibility = **IRN status GENERATED** (successful recorded outcome), not “externally attempted.”

| Document | GST contribution |
|---|---|
| Invoice `irn_status = GENERATED` | Positive taxable and tax |
| Credit Note `irn_status = GENERATED` | Negative, from **CN snapshots** |
| Draft, Pending Approval, Approved-not-IRN, Pending IRN, latest-Failure | **Excluded** |

Group amounts by the document’s **effective/document period**. Eligibility still depends on successful IRN.

**Invoice Count** = included IRN-generated **Invoices only**. Credit Notes are not invoices.

### CRN-2026-000031 contribution (intra-state)

| Line | Signed amount |
|---|---|
| Taxable Value | **−₹63,634.50** |
| CGST | **−₹5,727.11** |
| SGST | **−₹5,727.11** |
| IGST | **₹0.00** |
| Total Tax | **−₹11,454.22** |

---

## 14. Permissions (P5-D9, P5-D10)

Approved seeded matrix — **unchanged**:

| Role | DASHBOARD.VIEW | REPORT.VIEW | REPORT.EXPORT |
|---|---|---|---|
| Super Admin | Yes | Yes | Yes |
| Plant Manager | Yes | Yes | Yes |
| Accounts Manager | Yes | Yes | Yes |
| Billing Clerk | Yes | Yes | No |
| Store Keeper | Yes | No | No |

- Active role only; never union.
- Missing `REPORT.VIEW` → 403, no data.
- Missing `REPORT.EXPORT` → Export control absent.
- Unauthorised export POST → **403**.
- Company/Plant enforced separately.

RPT-SCR-013: `REPORT.VIEW` + `REPORT.EXPORT`. **No `AUDIT.EXPORT`.** Phase 5 must not create, seed, or infer `AUDIT.EXPORT`. No Edit/Delete on the summary.

Quick Actions use `CHALLAN.ADD` / `INVOICE.ADD` / `REPORT.VIEW` only.

---

## 15. Export architecture (P5-D1, D11, D17)

Reuse `export_job` statuses and spark. **Export retries are ExportJob retries. Never IRN language.**

### Threshold (P5-D1)

- Count rows with the **same normalised filters and server-side scope** as the report. **Never** trust a client-supplied count.
- Configurable `export.sync_max_rows`, **default 5,000**.
- **1–5,000 inclusive:** synchronous CSV allowed (exactly 5,000 = sync).
- **> 5,000:** asynchronous ExportJob **mandatory** (exactly 5,001 = async).
- Async worker **streams/chunks**; must not load the entire result into PHP.
- Preserve existing FAILED → spark re-process retry unless a proven defect needs a new decision.

### Branding snapshot at request (P5-D11)

Store on the job (additive JSON; no secrets/tokens): Company ID, display/legal name, GSTIN, registered address; Plant ID, name, address if any; report title; normalised filters; selected local timezone; requested-at UTC and local; requesting user ID; active role ID/name.

Generated files **must use this snapshot**. Later company/plant master edits must not change a completed export’s identity.

### Files and download (P5-D17)

- Default expiry **30 days**, configurable.
- Files **outside public web root** (continue `writable/exports/`).
- Opaque identifiers (job id / token) — **never** filesystem paths in URLs.
- No public or shareable URL; no scheduled emails.
- **Request** requires `REPORT.EXPORT` + scope.
- **Download** requires current authentication, `REPORT.VIEW`, and **current** access to the **snapshotted** Company/Plant. Losing that access **blocks** download even if the user requested the file earlier.
- Validate ownership/scope server-side.
- Cleanup deletes expired files; **keeps** immutable `export_job` + `audit_event` rows (`file_reference` null after purge).
- Audit: request, completion, failure, download.

CSV: column order = screen. Formula injection: neutralize text cells starting with `= + - @` without turning signed money (`+₹0.28`, `−₹422.10`) or quantities into text unnecessarily.

Unauthorised export POST: **403**.

---

## 16. Route / controller / service / view matrix

Proposed at implementation time only:

| Method | Path | Permission |
|---|---|---|
| GET | `/` | `DASHBOARD.VIEW` (403 if missing) |
| GET | `/reports` | `REPORT.VIEW` |
| GET/POST | `/reports/material-ledger` (+ `/export`) | existing 003 |
| GET/POST | `/reports/{slug}` (+ `/export`) | VIEW / EXPORT |
| GET | `/exports/(:num)/download` | VIEW + live snapshotted scope (P5-D17) |

Existing `/reports/material-ledger` stays. Other module routes stay.

---

## 17. Frame Name → server-render mapping

`Phase5Frames` (92 new) + existing `FrameCatalog` (7× 003). Production ignores `?frame=`. States map as §8. `T-*` → `viewport-tablet`. `M-*` → `viewport-mobile`.

---

## 18. Responsive design

Dashboard: six KPIs on desktop, tablet, mobile; Quick Actions collapse. Reports: desktop table; mobile cards; **no** mobile wide table; page `overflowX = 0`. RPT-006 tablet 1024×768: Material pinned; other nine columns in an inner horizontal scroller with affordance; Closing reachable; page itself does not overflow.

44×44 mobile targets. Existing drawer shell.

---

## 19. Accessibility

Named `<th>` tables; labelled mobile cards; keyboard filters/export; existing focus; text status not colour-only; loading/error/empty announcements; export completion/failure; full timezone names on RPT-012; tabular numerals; 44×44; tablet scroller keyboard-accessible.

---

## 20. Performance

No reporting tables in v1. Paginate HTML. Async export chunks. Additive indexes only after EXPLAIN (not in this turn): challan/invoice/CN `(scope, effective_datetime_utc)`. Half-open range predicates use those indexes. Memory: never unbounded `getResultArray` for registers or 006.

---

## 21. Audit

Append-only `audit_event`. Export request/complete/fail/download. RPT-013 is a **summary** (date, user, active role, action count, entities touched). No Edit/Delete.

---

## 22. Sample-data reconciliation

| Check | Expected |
|---|---|
| 003 and 006 Closing | **116.000 MT** |
| 006 golden columns | §12 table |
| Allocation / lot / disposition / WST | Frozen Phase 4 figures |
| INV-119 | ₹177,592.00 unchanged |
| CRN-031 GST Summary | −₹63,634.50 taxable, −₹11,454.22 tax |
| Dashboard “today” | Freeze Clock or insert same-day rows; do not move INV-119’s date |

---

## 23. Automated test plan (includes all P5-D1–D17)

Required at minimum:

**Export**

- 5,000 rows → synchronous; 5,001 → asynchronous.
- Export uses identical screen filters and scope; ignores client-supplied counts.
- CSV formula injection neutralized; money/qty not mangled.
- File outside public root; opaque id; unauthorised/expired download blocked.
- Access removed after generation blocks download.
- Branding snapshot unchanged after company/plant master edit.
- Existing ExportJob retry preserved; no IRN terms in export code/copy.

**Dates**

- Arbitrary valid historical range supported; From > To rejected.
- Half-open IST→UTC boundaries.

**GST**

- Non-IRN documents excluded.
- IRN-generated Invoice adds GST; IRN-generated Credit Note subtracts.
- CRN-031 subtracts ₹63,634.50 taxable and ₹11,454.22 tax.
- Invoice Count excludes Credit Notes.
- INV-119 original values unchanged.

**Inventory / ledger**

- Every `movement_type` has an explicit 006 classification.
- `REVERSAL` changes Adjustments and Closing.
- Bucket transfers do not change Closing.
- Unknown type → diagnostic / test failure.
- RPT-003 and RPT-006 both close at **116.000 MT**.

**Dashboard**

- Exactly six KPI definitions.
- Failure KPI counts **documents**, not attempts; later success removes the document.
- Credit Notes Awaiting IRN = Approved + Pending IRN only.
- Quick Actions permission-driven (not role names).
- `/` Dashboard when authenticated; unauthenticated `/` → login.

**IAM / empty / valuation**

- Missing `REPORT.VIEW` → 403.
- Missing `REPORT.EXPORT` removes control; unauthorised POST 403.
- RPT-013 uses `REPORT.EXPORT` and **no** `AUDIT.EXPORT`.
- All 13 Empty states use the exact three-part P5-D13 wording.
- No valuation/cost/profit/margin columns.

**Coverage**

- All **99** CSV frames with meaningful state assertions.
- 14 mobile frames at 390×844; 2 tablet frames at 1024×768 (006 pin/scroll).
- Complete Phase 0–4 suite remains green.

---

## 24. Phase 0–4 frozen-regression matrix

Tag `phase-4-final-approved` / `7e6a00d` behaviour remains the floor except approved 003 presentation/integration. 233 tests / 7,234 assertions floor until Phase 5 adds tests. Ledger 116.000, allocation 30, lot 13, disposition 12, INV-119 / CRN-031 money, grouped INR, isolation, active-role IAM, manual IRN only, no production seed routes, G1/reservation/no CN cancel, `INWARD.REVERSE`, no `INWARD.APPROVE`, tables not rebuilt.

---

## 25. Proposed implementation checkpoints

**Do not execute until implementation is explicitly authorised.**

| CP | Scope |
|---|---|
| **0** | Tag + 233/7,234; no Phase 6 |
| **A** | 99 Frame Names; shared Empty/skeleton/chips |
| **B** | Dashboard at `/`; six KPIs; Quick Actions |
| **C** | RPT-003 integration (columns, states, 403 export, download) |
| **D** | 001, 002, 010 |
| **E** | 004, 005, 009 (P5-D3/D4) |
| **F** | 006/007/008 + classifier + 116.000 + reversal fixture |
| **G** | 011, 012, 013 (REPORT.EXPORT only) |
| **H** | Export threshold, snapshot, expiry, download, CSV safety |
| **I** | 99-frame HTTP + 14 mobile + 2 tablet QA + Phase 0–4 regression |

---

## 26. Decision register (closed)

P5-D1–P5-D17 are **approved**. Residual non-blocking notes:

| ID | Note |
|---|---|
| CSV file | External; 99 names in §3 are canonical (P5-D16). Not copied this session. |
| Home diagnostic | Replaced by Dashboard at `/` when implementation starts (P5-D14). |
| `AUDIT.EXPORT` in Phase 1 catalogue | Phase 5 does not modify seeders/catalogue and does not use the code. |

No remaining blockers **inside** D1–D17. Implementation is complete for Phase 5. Phase 6 is not started.

---

## 27. Entry and exit criteria

### Entry (implementation)

1. Explicit Business Owner **implementation** authorisation (this plan correction is **not** that go-ahead).  
2. Tag `phase-4-final-approved` still `7e6a00d2dd022d5b533d007c2f9c0cbb2fea66dc`.  
3. Suite 233 / 7,234 / 0 on that baseline.  
4. No Phase 6.

### Exit

99 frames; six KPIs; three possible Quick Actions; `/` Dashboard; P5-D13 Empty copy; 003/006 116.000; GST D3/D4; export D1/D11/D17; no valuation; no `AUDIT.EXPORT` on 013; 14 mobile + 2 tablet QA; Phase 0–4 green; no Phase 6.

### Stop

Do not start Phase 6.
