# Phase 4 implementation report

**Status:** Phase 4 implemented, tested, and **closed.** **Phase 5 was not started.**  
**Stack (unchanged):** CodeIgniter 4, PHP 8.2+, MySQL 8 InnoDB, server-rendered Views, existing CSS/JS, Phase 0–3 responsive shell.  
**Suite:** `vendor/bin/phpunit --no-coverage` → see §17.  
**Binding decisions:** D1–D12 in `docs/PHASE4_PLAN.md` were not reopened.

FrameCatalog coverage key: `D-INV-SCR-012-Create-IRNGenerated`. Figma alias `D-INV-SCR-012-IRNGenerated` is design traceability only.

---

## 1. Files changed

### Checkpoint A (`e6103a9`)

- `app/Database/Migrations/2026-09-13-400001_AddPhase4BillingSchema.php`
- `app/Database/Seeds/MasterSeeder.php`, `TransactionSeeder.php`
- `app/Modules/IAM/Domain/PermissionCatalog.php` (`CREDIT_NOTE.EDIT`, `INVOICE.RESERVE_NUMBER`, `INVOICE.RELEASE_NUMBER`)
- `app/Modules/IAM/Domain/SampleIdentities.php` (Accounts Manager + Super Admin grants)
- `app/Modules/Invoice/Services/InvoiceService.php`, `app/Modules/Numbering/Services/NumberingService.php`
- `app/Modules/Ui/FrameSimulation.php`, `app/Modules/Ui/Phase4Frames.php`
- `tests/integration/Phase4SchemaTest.php`, `tests/unit/Phase4FramesTest.php`

### Checkpoint B (`999f0d8`)

- INV-001 / INV-002: `UnbilledController`, `LaterInvoiceController`, `UnbilledChallanQuery`, `LaterInvoiceService`
- Views: `unbilled.php`, `later_create.php`, `later_review.php`
- Routes (Phase 4 paths registered before `invoices/(:num)`), sidebar, `c_dat_12`, `public/css/app.css`, `public/js/app.js`
- Migration `down()` made idempotent (delete CN `irn_attempt` rows; restore Phase 3 `chk_idem_type`; drop `company_plant_id` via information_schema)
- `tests/_support/Phase4Support.php`, `tests/integration/LaterInvoiceAndUnbilledTest.php`

### Checkpoint C (`003bb55`)

- `InvoiceReservationController`, `InvoiceReservationService`
- Views: `reservations.php`, `reservation_create.php`
- `tests/integration/InvoiceReservationTest.php`

### Checkpoint D (`5a3c9c8`)

- `InvoiceController::index`, `app/Views/pages/invoice/list.php` — integration only; Phase 1 review/IRN routes unchanged

### Checkpoint E (`b04953d`)

- `CreditNoteController`, `CreditNoteService`
- Views: `credit_note/list.php`, `create.php`, `review.php`

### Checkpoint F (`13ab3fe`)

- `CreditNoteIrnService`, `credit_note/irn.php`
- `Phase4Frames` Review overlay (`parent_cn_review` for every INV-012 Review frame)
- `tests/integration/CreditNoteG1Test.php`

### Checkpoint G (`a925e41`)

- `Phase4GoldenSeeder`, `DatabaseSeeder` call
- `tests/integration/GoldenCreditNoteTest.php`
- `Phase4SchemaTest` asserts `CRN-2026-000031` exists and INV-119 header remains `177592.00`

### Checkpoint H (this closeout)

- `tests/feature/Phase4FrameHttpTest.php` (77 frames)
- `tests/feature/Phase4MobileFrameHttpTest.php` (15 mobile)
- `tests/feature/Phase4AuthorizationHttpTest.php`
- `README.md`, `docs/PHASE4_PLAN.md` status, this report

### Live-browser QA correction (`08f072f`)

- `public/css/app.css` — `.btn` is `inline-flex` so height/min-height apply to `a.btn`; `.viewport-mobile .btn` uses the 44px touch target. Confirmed on `M-INV-SCR-002-Create-Default` and `M-INV-SCR-002-Create-Error` at 390×844 (`Return to unbilled selection` measured 22px before the fix, 48px after).

### Currency-format correction (this commit)

Presentation-only Indian Rupee grouping. Database DECIMAL values are unchanged. Formatter is `App\Common\DisplayTime::formatMoney` (BCMath via `Decimal::ofMoney` / `cmp` / `mul`; thousands grouping; two decimals). C-DAT-12 uses `DisplayTime::formatCalculationDisplay` so Views do not hard-code commas. Round-off positives render as `+₹0.28`; discounts as `−₹422.10`; zero as `₹0.00`. Rate-master 4-decimal amounts and percentages are unchanged.

- `app/Common/DisplayTime.php`
- `app/Views/components/dat/c_dat_12_calculation_summary.php`
- `app/Views/pages/invoice/review.php`, `later_review.php`
- `app/Views/pages/credit_note/review.php`
- `app/Views/pages/challan/form.php`
- `app/Views/pages/charge/configure.php` (FIXED defaults only)
- `tests/unit/DisplayTimeMoneyTest.php`
- `tests/feature/Phase1FrameHttpTest.php`, `Phase4FrameHttpTest.php`, `Phase4MobileFrameHttpTest.php`
- `tests/integration/GoldenCreditNoteTest.php`

No React/Next/Nest/PostgreSQL/JWT/Shield. No IRN job. No production `/seed`, `/fixture`, `/demo`, `/probe`, or `/test` routes. The Phase 3 zip is not in git. Phase 5 was not started.

---

## 2. Migration list and rollback evidence

**Forward:** `2026-09-13-400001_AddPhase4BillingSchema` is additive InnoDB only. It does not rebuild Phase 0–3 tables.

| Object | Change |
|---|---|
| `invoice_header` | Optional `company_plant_id` + `fk_inv_cp` RESTRICT; backfill from challan links |
| `invoice_number_reservation` | Header; generated `live_registry_key` unique while `RESERVED` |
| `invoice_number_reservation_challan` | Bound challans; generated `live_challan_key` unique while `BOUND` |
| `invoice_number_reservation_history` | Append-only RESERVED / CONSUMED / RELEASED |
| `credit_note_header` | D1 statuses; G3 `chk_cnh_grand` / `chk_cnh_round`; generated unique registry/number keys; **no CANCELLED** |
| `credit_note_line` / `credit_note_charge_line` | Snapshot + credited qty/amount; unique per original invoice child |
| `credit_note_status_history` | Append-only lifecycle |
| `irn_attempt` | `fk_irn_credit_note` RESTRICT; XOR parent remains |
| `numbering_history` | `fk_nh_credit_note` RESTRICT |
| `inward_entry` | `fk_inw_orig_cn` RESTRICT on existing nullable `original_credit_note_id` |
| `idempotent_command` | Widen `chk_idem_type` with `INVOICE_LATER_CREATE`, `INVOICE_RESERVE`, `INVOICE_RELEASE`, `CREDIT_NOTE_SAVE`, `CREDIT_NOTE_APPROVE`, `CREDIT_NOTE_IRN`. **No** `CREDIT_NOTE_CANCEL` |

**Rollback (`down()`):** drop FKs → delete CN `irn_attempt` rows (cannot NULL under XOR `chk_irn_parent`) → NULL `numbering_history.credit_note_id` → drop the seven Phase 4 tables → drop `invoice_header.company_plant_id` → delete Phase 4 idempotent types → restore Phase 3 `chk_idem_type`.

`Phase4SchemaTest::testDownRemovesOnlyPhase4Additions` runs `down()` then `up()` and asserts:

- Phase 4 tables are gone after `down()`, present after `up()`
- `invoice_header` / `inward_entry` row counts unchanged
- `inward_entry.original_credit_note_id` column remains (Phase 3)
- `company_plant_id` is removed on `down()` and restored on `up()`

---

## 3. Final schema and constraint summary

All new tables are InnoDB, `utf8mb4`. Historical/transactional FKs are `ON DELETE RESTRICT ON UPDATE RESTRICT`. No physical-delete cascade. No same-table G1 trigger.

Generated-column uniqueness (MySQL translation of partial unique indexes):

- `invoice_number_reservation.live_registry_key` — unique while `RESERVED`
- `invoice_number_reservation_challan.live_challan_key` — unique while `BOUND`
- `credit_note_header.live_registry_key` / `live_credit_note_number_key` — unique for live numbers

`document_number_registry.chk_reg_reuse_invoice` is unchanged: `AVAILABLE_FOR_REUSE` remains invoice-only. Inserting `CREDIT_NOTE` + `AVAILABLE_FOR_REUSE` raises `DatabaseException` (`testCreditNoteNumbersCannotEnterReusePool`). `NumberingService::releaseForReuse` throws `NUMBERING_NEVER_REUSE:CREDIT_NOTE`.

Credit Note header CHECK statuses: `DRAFT`, `PENDING_APPROVAL`, `RETURNED_FOR_CORRECTION`, `APPROVED`, `PENDING_IRN`, `IRN_GENERATED`. No `CANCELLED`.

---

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

| Method | Path | Screen | Permission | Service |
|---|---|---|---|---|
| GET | `/invoices/unbilled` | INV-001 | `INVOICE.LIST` / `VIEW` / `ADD` | `UnbilledChallanQuery` |
| POST | `/invoices/unbilled` | INV-001 | `INVOICE.ADD` | session selection; ineligibility on toggle |
| GET/POST | `/invoices/later` | INV-002 | `INVOICE.ADD` | `LaterInvoiceService::createFromChallans` |
| GET/POST | `/invoices/later/{id}` | INV-002 | `INVOICE.VIEW` / `EDIT` | existing `InvoiceService` submit/approve |
| GET | `/invoices` | INV-004 | `INVOICE.LIST` | `InvoiceService::listForUser` + origin/CN flags |
| GET/POST | `/invoice-reservations` · `/create` | INV-007 | `INVOICE.RESERVE_NUMBER` | `InvoiceReservationService::reserve` |
| POST | `/invoice-reservations/{id}/release` | INV-007 | `INVOICE.RELEASE_NUMBER` | `release` |
| GET | `/credit-notes` | INV-010 | `CREDIT_NOTE.LIST` / `VIEW` | `CreditNoteService::listForUser` |
| GET/POST | `/credit-notes/create` | INV-011 | `CREDIT_NOTE.CREATE` | `saveNew` |
| GET/POST | `/credit-notes/{id}` | INV-011 | `VIEW` / `EDIT` | `updateDraft` — Draft / Returned only |
| POST | `/credit-notes/{id}/submit` | INV-011 | `CREATE` / `EDIT` | `submit` |
| POST | `/credit-notes/{id}/approve` · `/return` | INV-011 | `CREDIT_NOTE.APPROVE` | `approve` / `returnForCorrection` |
| GET/POST | `/credit-notes/{id}/irn` | INV-012 | `RECORD_IRN_DETAILS` to record | `CreditNoteIrnService::record` |

Existing Phase 1 invoice review / approve / cancel / IRN routes are unchanged. Immediate `POST /challans/{id}/invoice` remains.

**Grants (D5/D8):**

| Code | Accounts Manager | Super Admin | Billing Clerk | Plant Manager | Store Keeper |
|---|---|---|---|---|---|
| Six Credit Note codes + Reserve/Release | Yes | Yes | No | No | No |

Missing permission **removes** the control. Direct unauthorised POST returns **403**. Company/plant scope is enforced in services independently of the permission check.

---

## 5. Exact 77-frame inventory

**62 desktop, 15 mobile, 0 tablet.** Exact CSV Frame Names from `Phase4Frames::all()`:

**INV-SCR-001 (10):** `D-INV-SCR-001-List-Default`, `D-INV-SCR-001-List-Draft`, `D-INV-SCR-001-List-PermissionRestricted`, `D-INV-SCR-001-List-Empty`, `D-INV-SCR-001-List-Loading`, `M-INV-SCR-001-List-Default`, `D-INV-SCR-001-List-Filtered`, `D-INV-SCR-001-List-NoResults`, `D-INV-SCR-001-List-Error`, `M-INV-SCR-001-List-NoResults`.

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

**INV-SCR-004 (9):** `D-INV-SCR-004-List-Default`, `D-INV-SCR-004-List-Filtered`, `D-INV-SCR-004-List-Empty`, `D-INV-SCR-004-List-NoResults`, `D-INV-SCR-004-List-Loading`, `D-INV-SCR-004-List-Error`, `D-INV-SCR-004-List-PermissionRestricted`, `M-INV-SCR-004-List-Default`, `M-INV-SCR-004-List-NoResults`.

**INV-SCR-007 (12):** `D-INV-SCR-007-Create-Default`, `D-INV-SCR-007-Create-Draft`, `D-INV-SCR-007-Create-Error`, `D-INV-SCR-007-List-Default`, `D-INV-SCR-007-List-Filtered`, `D-INV-SCR-007-List-PermissionRestricted`, `M-INV-SCR-007-List-Default`, `D-INV-SCR-007-List-Empty`, `D-INV-SCR-007-List-NoResults`, `D-INV-SCR-007-List-Loading`, `D-INV-SCR-007-List-Error`, `M-INV-SCR-007-List-NoResults`.

**INV-SCR-010 (9):** `D-INV-SCR-010-List-Default`, `D-INV-SCR-010-List-Filtered`, `D-INV-SCR-010-List-Empty`, `D-INV-SCR-010-List-NoResults`, `D-INV-SCR-010-List-Loading`, `D-INV-SCR-010-List-Error`, `D-INV-SCR-010-List-PermissionRestricted`, `M-INV-SCR-010-List-Default`, `M-INV-SCR-010-List-NoResults`.

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

**INV-SCR-012 (15):** `D-INV-SCR-012-Create-Default`, `D-INV-SCR-012-Create-IRNGenerated`, `D-INV-SCR-012-Create-IRNFailed`, `D-INV-SCR-012-Create-PermissionRestricted`, `D-INV-SCR-012-Create-Loading`, `M-INV-SCR-012-Create-IRNGenerated`, `D-INV-SCR-012-Create-Draft`, `D-INV-SCR-012-Create-Error`, `D-INV-SCR-012-Review-PendingApproval`, `D-INV-SCR-012-Review-Approved`, `D-INV-SCR-012-Review-ReturnedForCorrection`, `D-INV-SCR-012-Review-ConcurrentUpdate`, `D-INV-SCR-012-Review-PermissionRestricted`, `M-INV-SCR-012-Create-Default`, `M-INV-SCR-012-Create-Error`.

Overlays: OV-07 on Filtered list frames; OV-11 on ConcurrentUpdate. Production ignores `?frame=`. Overlay is read-only and does not persist status or permissions.

---

## 6. Existing-versus-new screen assessment

| Screen | Verdict |
|---|---|
| INV-SCR-004 | **Existing (Phase 1).** Integration only: Origin, Credit Status column, Record IRN / fresh outcome, Create Credit Note gate. Desktop table + mobile cards preserved. |
| INV-SCR-003 / 008 | **Existing.** Linked from list; not reconstructed. Immediate invoice from challan preserved. |
| INV-SCR-001, 002, 007, 010, 011, 012 | **New.** |

---

## 7. Invoice reservation lifecycle evidence

Registry (company + financial year):

1. `NumberingService::reserveInvoice` — ISSUED → RESERVED, or AVAILABLE_FOR_REUSE → RESERVED with REUSED history.
2. Header `reservation_status = RESERVED`; challans `BOUND`; history `RESERVED`.
3. Later/bulk persist calls `consumeReserved`; reservation `CONSUMED`; challans consumed with the invoice.
4. Unused RESERVED → `release` → registry `AVAILABLE_FOR_REUSE`; history `RELEASED`.
5. Consumed Release control is visible and disabled: `Number is consumed by INV-…`.
6. PermissionRestricted removes the Release submit control (`type="submit">Release` absent).
7. `INV-2026-000119` stays `CONSUMED`; sequence `next_value` > 119.
8. Two concurrent `reserve` calls receive different formatted numbers (`testConcurrentReservationsDoNotShareANumber`). `uq_resv_live_registry` plus `document_sequence FOR UPDATE` prevent duplicates.
9. Draft primary-key creation does not issue a visible INV-* by itself; numbers come from `NumberingService` inside the persist/reserve transaction.

Client and Challan set cannot change: consume with a different challan set throws `INV_RESERVATION_CHALLANS_LOCKED`.

---

## 8. Later/bulk Invoice transaction and concurrency evidence

`LaterInvoiceService::createFromChallans`:

1. Claim `INVOICE_LATER_CREATE` idempotency.
2. `SELECT … FROM challan_header WHERE challan_id = ? FOR UPDATE` in ID order.
3. Revalidate eligibility (Approved, U2 live link, reservation, company/plant/client/GST).
4. Insert invoice + links (`DRAFT_RESERVED`); challan `billing_status = DRAFT_LINKED`.
5. Optional `consumeReserved` for reservation-origin invoices.
6. Invoice status is `DRAFT`. Create does **not** approve. Copy never includes “Save and Approve Both.”
7. Second persist of the same live challan throws `U2_CHALLAN_ALREADY_LINKED`.
8. `InvoiceService::approve` still requires source Challans already Approved; approval is a separate POST.
9. Immediate `InvoiceService::createFromChallan` still works (`testImmediateInvoicePathRemainsAvailable`).
10. Round-off row is always rendered, including ₹0.00 (`c_dat_12`).

Policy snapshot `origin = LATER_BULK` drives INV-004 Origin “Later / bulk”.

---

## 9. Credit Note lifecycle evidence

D1 FSM:

`DRAFT` → `PENDING_APPROVAL` → `APPROVED` → `PENDING_IRN` → `IRN_GENERATED`

Alternate: `PENDING_APPROVAL` → `RETURNED_FOR_CORRECTION` → `PENDING_APPROVAL`

- Only an IRN-generated Invoice may be credited (`CN_INVOICE_NOT_IRN_GENERATED`).
- Creation is explicit (`CREDIT_NOTE.CREATE`); never automatic.
- Prefill from immutable invoice line/charge snapshots; zero-credit lines remain in the selector and are persisted.
- Number issued via `issueAndConsume` at draft save; registry `CONSUMED`; never `RELEASED` / `AVAILABLE_FOR_REUSE`.
- Creator self-approval only when the **active role** holds `CREDIT_NOTE.APPROVE`.
- Approved notes are not editable (`CN_NOT_EDITABLE`).
- No Cancel/Delete/Void action, route, or `CREDIT_NOTE.CANCEL`.
- `enterPendingIrn` runs only when `?frame=` is absent, so overlay cannot persist Pending IRN.

---

## 10. G1 calculation and locking evidence

Consuming: `APPROVED`, `PENDING_IRN`, `IRN_GENERATED`.  
Non-consuming: `DRAFT`, `PENDING_APPROVAL`, `RETURNED_FOR_CORRECTION`.

Approval lock order (`CreditNoteService::approve`):

1. `invoice_header … FOR UPDATE`
2. `invoice_line … ORDER BY invoice_line_id FOR UPDATE`
3. `invoice_charge_line … ORDER BY invoice_charge_line_id FOR UPDATE`
4. Credit Note headers for that invoice, then children, ascending ID
5. Recalculate remaining via sums over consuming statuses
6. Independent caps: line qty, line amount, each charge/discount; document total cannot hide a line over-credit (`G1_LINE_AMOUNT_EXCEEDED` even when the grand total would fit)
7. `PENDING_APPROVAL` → `APPROVED`
8. `credit_note_status_history`
9. Audit `CREDIT_NOTE_APPROVED`
10. Commit (`TransactionManager`, isolation)

A second overlapping approval waits on the same invoice-header lock and revalidates; it throws `G1_LINE_…` rather than over-credit.

Successful IRN calls `recheckG1ForSuccess` with the same invoice-graph lock before completion. Failed IRN leaves `PENDING_IRN` and does not insert a second G1 row.

---

## 11. Exact ₹75,089.00 worksheet

`InvoiceCalculator` (BCMath, first-line GST 9%/9%, intra-state):

| Row | Amount |
|---|---|
| Line 1 credit | ₹63,000.00 |
| Line 2 credit (visible) | ₹0.00 |
| Additional charge credited | +₹1,056.60 |
| Discount credited | −₹422.10 |
| Taxable value | ₹63,634.50 |
| CGST 9% | ₹5,727.11 |
| SGST 9% | ₹5,727.11 |
| Total before round-off | ₹75,088.72 |
| Round-off | +₹0.28 |
| **Credit Note Total** | **₹75,089.00** |

Seeded as `CRN-2026-000031` / `IRN_GENERATED`. Original invoice header **INV-2026-000119 = ₹177,592.00** is unchanged. Invoice `credit_status = PARTIALLY_CREDITED` only because this Credit Note reached IRN Generated.

---

## 12. Manual Credit Note IRN attempt evidence

- Eligibility: parent `APPROVED` or `PENDING_IRN`; GET without `?frame=` promotes Approved → Pending IRN.
- Success fields: IRN, acknowledgement number, acknowledgement datetime with timezone.
- Failure: remarks required; field sets swap in `public/js/app.js` `[data-irn-form]`.
- Each attempt is an append-only `irn_attempt` row (`credit_note_id` set, `invoice_id` null).
- FAILURE → parent stays `PENDING_IRN`, `irn_status = FAILED`, G1 retained once, copy says this is not an automatic retry.
- SUCCESS → `IRN_GENERATED`; further `record` throws `IRN_ALREADY_GENERATED`.
- INV-012 Review frames set `parent_cn_review`; they do not render “Approve Credit Note”.

---

## 13. Invoice Credit Status derivation evidence

`recalculateInvoiceCreditStatus` runs **only** from `markIrnGenerated` (successful CN IRN).

Remaining for this column counts **`IRN_GENERATED` only**, not Approved / Pending IRN G1 reservations (`remainingMap(..., ['IRN_GENERATED'])`).

| Event | Invoice `credit_status` |
|---|---|
| CN draft / pending / returned / approved / failed IRN | `NOT_CREDITED` |
| Successful CN IRN with leftover qty/value | `PARTIALLY_CREDITED` |
| Successful CN IRN covering remaining IRN-posted credit | `FULLY_CREDITED` |

`testInvoiceCreditStatusIgnoresApprovedNotesUntilIrnGenerated` approves a charge-only CN (no IRN), then IRN-generates a line-only CN: status becomes `PARTIALLY_CREDITED`, not `FULLY_CREDITED`.

INV-004 Create Credit Note still uses G1 remaining (`invoiceHasRemainingCredit`), so an Approved-but-not-IRN CN correctly blocks over-credit on a new draft.

---

## 14. Permission and direct-POST tests

`Phase4AuthorizationHttpTest`:

- Plant Manager GET on unbilled / later / reservations / credit-notes → **403**
- Billing Clerk (switched active role) has `INVOICE.ADD` but not CN/reserve; CN and reservation GET/POST → **403**; unbilled/list → 200
- Super Admin creates a CN; Plant Manager POST approve and reservation release → **403**
- PermissionRestricted frames remove `>Create Credit Note<`, `>Record IRN outcome<`, `type="submit">Release`, `>Reserve number<`
- Credit Note list overlay has no Cancel / Delete / Void actions

Scope: `ScopeService::assertPlant` on later create, reserve, and CN write paths.

---

## 15. Production-route safety scan

`ProductionRouteAbsenceTest` plus `CreditNoteG1Test::testNoAutomaticIrnJobOrQueueExists` and `testCreditNoteNumberIsNeverReusedAndCancelRoutesDoNotExist`:

- `Routes.php` has no seed/fixture/demo/probe/`/test` paths
- No `credit-notes/(:num)/cancel|delete|void`
- No `CREDIT_NOTE_CANCEL` idempotent type
- No `Alankit`, no “Generate IRN”, no “automatic IRN retry” in `app/`
- No `app/Commands/ProcessIrnQueue.php`
- Health JSON `phase` is **4**

---

## 16. Desktop and mobile browser QA

Automated HTTP (unchanged from Checkpoint H):

- All 77 frames: HTTP 200, exact Frame Name, titles, badges, empty/loading/error/filtered/concurrent/restricted content, ₹75,089.00 on golden review surfaces, no prohibited actions
- All 15 mobile frames: `viewport-mobile`, existing Mobile Header, `data-nav-toggle` 44×44 hamburger (`aria-controls="primary-nav"`), closed drawer (`id="primary-nav"`), `card-list mobile-only` on list defaults, `form-grid` on creates, empty-state on NoResults

Live Chromium at **exactly 390×844** (CDP `Emulation.setDeviceMetricsOverride`, role Super Admin `n.joshi`, development `?frame=` overlays, no POST against frozen INV-119 / CH-481 / CRN-031):

| Frame Name | Result | Notes |
|---|---|---|
| M-INV-SCR-001-List-Default | PASS | Cards; CH-2026-000481 ineligible: “Challan is already billed on INV-2026-000119.” |
| M-INV-SCR-001-List-NoResults | PASS | Empty copy: “No Challans match these filters.” |
| M-INV-SCR-002-Create-Default | PASS | After CSS fix: `Return to unbilled selection` is 48×44+; round-off ₹0.00 visible; no Save and Approve Both |
| M-INV-SCR-002-Create-Error | PASS | Error banner readable; same touch-target fix |
| M-INV-SCR-004-List-Default | PASS | INV-2026-000119 card; statuses separate; sticky “Select unbilled Challans” |
| M-INV-SCR-004-List-NoResults | PASS | “No invoices match these filters.” |
| M-INV-SCR-007-List-Default | PASS | Live DB has no reservation rows; operational empty-state (not a table). Sticky “Reserve number” |
| M-INV-SCR-007-List-NoResults | PASS | “No reservations match these filters.” |
| M-INV-SCR-010-List-Default | PASS | CRN-2026-000031 card, Credit Note vs IRN statuses, **₹75,089.00**, sticky Create Credit Note |
| M-INV-SCR-010-List-NoResults | PASS | “No Credit Notes match these filters.” |
| M-INV-SCR-011-Create-Default | PASS | Single-column form; CRUSH ₹0.00 remaining line visible |
| M-INV-SCR-011-Create-Error | PASS | Cap error readable; sticky Save Credit Note draft |
| M-INV-SCR-012-Create-IRNGenerated | PASS | Badges IRN_GENERATED / GENERATED; no Record / Retry / Generate IRN |
| M-INV-SCR-012-Create-Default | PASS | Success/Failure choice; Record IRN outcome present on overlay only (no POST) |
| M-INV-SCR-012-Create-Error | PASS | IRN validation error readable |

Every mobile frame: Mobile Header visible, hamburger 44×44, drawer closed then opened/closed via hamburger + scrim, sidebar `position:fixed` (not stacked), `overflowX = 0`, no Cancel/Delete/Void route, no Generate IRN, no JS `pageerror`. `.screen` padding-bottom 88px keeps sticky actions from covering content.

**Defect found and corrected (only change in this closeout):** `a.btn` used `display:inline`, so `height` / `min-height` did not apply. At 390×844 on `/invoices/later?frame=M-INV-SCR-002-Create-Default` (and Error), Super Admin, “Return to unbilled selection” measured **22px** tall. Fix in `public/css/app.css`. Re-measured **48px** height, `min-height: 44px`.

### Desktop A–F (1440×900, Super Admin, read-only)

| Path | Result |
|---|---|
| **A Unbilled** | PASS. CH-2026-000481 is ineligible with exact billed reason. No other live Challans; plant-lock / incompatible GST were **not** exercised (would require a new M30 challan and would change ledger 116.000 MT). Covered by `LaterInvoiceAndUnbilledTest` (`Plant is locked to Chakan Plant.`). No submit against frozen records. |
| **B Later/bulk** | PASS. Empty selection states the Create later path; Calculation Summary order is Line Subtotal → Additional Charges → Discount → Taxable Value → GST → Total Before Round-off → Round-off Adjustment **₹0.00** → Invoice Total. Lede: create and approve remain separate; no “Save and Approve Both.” Selected-challan flow N/A on this frozen database. |
| **C Reservations** | PASS (read-only). List Default is empty. `/invoice-reservations/create` shows CH-2026-000481 already billed. PermissionRestricted overlay: “Release is not shown for this active role.” Consumed + Release-disabled reason is covered by `InvoiceReservationTest` (no live RESERVED/CONSUMED row; none created). |
| **D Credit Note list/create** | PASS. INV-2026-000119 exposes Create Credit Note to Super Admin / Accounts Manager. CRN-2026-000031 total **₹75,089.00** via `DisplayTime::formatMoney` (was live-QA defect `₹75089.00`). CRUSH credited ₹0.00 remains visible. No Cancel / Delete / Void. Credit Note status and IRN status are separate badges. |
| **E Credit Note approval** | PASS with overlay safety. `D-INV-SCR-011-Review-PendingApproval` shows PENDING_APPROVAL vs GENERATED separately. Approve is **not** armed on frozen CRN-031 (actions follow live `IRN_GENERATED`). Returned overlay: “Returned for Correction holds no G1 reservation.” Approved overlay is read-only. PermissionRestricted removes Approve. PHPUnit covers Approve on disposable notes with `CREDIT_NOTE.APPROVE`. |
| **F Manual CN IRN** | PASS. Live `/credit-notes/1/irn` is IRN Generated: recording closed, no Record/Retry. Overlay Default: Success selected, success fields visible, failure hidden; switching to Failure swaps fields and shows Failure remarks. No Generate IRN, queue, or connection status. No POST on CRN-031. |

### Role/permission browser matrix (not PHPUnit-only)

| Identity | Active role | Unbilled | Reservations | Credit Notes | Notes |
|---|---|---|---|---|---|
| n.joshi | Super Admin | 200 | 200 + Reserve | 200 + Create | Nav includes all three |
| a.deshpande | Accounts Manager | 200 | 200 + Reserve | 200 + Create | Nav includes all three |
| r.kumar | Plant Manager | 403 | 403 `INVOICE.RESERVE_NUMBER` | 403 `CREDIT_NOTE.LIST` | None of those nav items |
| s.patil | Store Keeper (switched) | 403 | 403 | 403 | Inward/store nav only |
| s.patil | Billing Clerk (switched) | 200 | 403 | 403 | Challans/Invoices/Unbilled; **no** reservation or Credit Note nav or actions |

Switching Patil Store Keeper → Billing Clerk immediately replaces nav. Permissions are the active role only (not unioned).

### Production-route safety (logged-in GET on the live server)

All **404**: `/invoices/seed`, `/challans/seed`, `/probe/need-approve`, `/credit-notes/1/cancel`, `/credit-notes/1/delete`, `/credit-notes/1/void`, `/fixture/demo`, `/demo`, `/test`, `/seed`. `Routes.php` has no those paths and no Credit Note cancel/delete/void. `FrameSimulation::overlayAllowed('production')` is **false**. No `ProcessIrnQueue` command. Phase 4 page links crawled: no broken (non-200) in-app hrefs. Console: no JavaScript `pageerror` on reviewed pages. Horizontal overflow: **0** on all 15 mobile frames.

Live preview: `http://127.0.0.1:43221` (PHP built-in server, `CI_ENVIRONMENT=development`). **Phase 5 was not started.**

### Currency-format re-QA (CRN-2026-000031)

Live Chromium after grouping in `DisplayTime::formatMoney`. No POST against frozen INV-119 / CH-481 / CRN-031.

| Surface | Desktop 1440 | Mobile 390×844 |
|---|---|---|
| INV-SCR-010 list | **₹75,089.00** on CRN-2026-000031; not `₹75089.00` | Same on the card; no clip/overflow |
| INV-SCR-011 review | Total **₹75,089.00**; taxable **₹63,634.50**; discount **−₹422.10**; round-off **+₹0.28** | Same; calculation summary fits; `overflowX = 0` |
| INV-SCR-011 create | Remaining invoice amounts grouped (`₹126,000.00`, `₹63,000.00`); inputs stay ungrouped DECIMAL | Same |
| INV-SCR-012 IRN | Identifiers only; no ungrouped five-digit Rupee | Same |
| INV-SCR-004 / invoice review | INV-2026-000119 **₹177,592.00**; not `₹177592.00` | Same on the card |

Required formatter samples (unit + live HTML): ₹75,089.00, ₹177,592.00, ₹63,634.50, ₹5,727.11 (unit; C-DAT-12 shows combined GST ₹11,454.22), ₹0.00, +₹0.28.

---

## 17. Full test and assertion counts

`vendor/bin/phpunit --no-coverage` after the currency-format correction:

**233 tests, 7,234 assertions, 0 failures.**

Checkpoint H was **229 tests / 7,191 assertions**. This correction adds `DisplayTimeMoneyTest` (3 tests) and `GoldenCreditNoteTest::testGoldenDocumentsRenderGroupedInrOnCreditNoteAndInvoicePages` (1 test), plus HTML grouping asserts on INV-SCR-010 / INV-SCR-004 list frames.

Phase 0–3 floor remains **190 tests / 4,807 assertions**. Identifiers, quantities, and percentages are unchanged. Frozen header `INV-2026-000119.grand_total` remains DECIMAL `177592.00`; `CRN-2026-000031.grand_total` remains DECIMAL `75089.00`.

---

## 18. Phase 0–3 frozen-regression matrix

| Frozen item | Status |
|---|---|
| Ledger M30 Chakan | **116.000 MT** (`Phase4SchemaTest` + ledger query) |
| Allocation | 18.000 + 12.000 = 30.000 MT (Phase 0–3 tests remain) |
| LOT-2026-00230 remaining | **13.000 MT** |
| Disposition | 3.000 + 7.000 + 2.000 = 12.000 MT |
| WST-2026-000067 | Approved |
| INV-2026-000119 | **₹177,592.00** |
| 110 Phase 3 frames | Unchanged catalog; Phase 3 HTTP tests remain |
| Phase 3 mobile shell | Unchanged `c_nav_01_shell` |
| `INWARD.REVERSE` | Store Keeper + Super Admin |
| `INWARD.APPROVE` | Absent from catalogue |
| Disposition reversal | No reverse route |

---

## 19. Commit hash for every checkpoint

| Checkpoint | Hash | Subject |
|---|---|---|
| Phase 3 final tag `phase-3-final-approved` | `25ef02a` | Correct Phase 3 mobile drawer shell and reverse POST 403 |
| Checkpoint 0 (starting HEAD / approved plan) | `59fd273` | Correct the Phase 4 plan with Business Owner decisions D1–D12 |
| **A** | `e6103a9` | Additive schema and authorised permissions |
| **B** | `999f0d8` | Unbilled selection and later/bulk invoices |
| **C** | `003bb55` | Invoice number reservation |
| **D** | `5a3c9c8` | INV-SCR-004 integration |
| **E** | `b04953d` | Credit Note create/review/approval and G1 |
| **F** | `13ab3fe` | Manual Credit Note IRN |
| **G** | `a925e41` | Golden `CRN-2026-000031` = ₹75,089.00 |
| **H** | `e3643c3cd0aa31027618c9c552097e8f443cd361` | 77-frame coverage, mobile QA, closeout |
| **H hash pointer** | `2b8c060` | Record the Checkpoint H closeout commit hash |
| **Live-browser QA correction** | `08f072f` | `a.btn` 44×44 touch height; live Chromium evidence in §16 |
| **Currency-format correction** | *(this commit)* | Grouped INR via `DisplayTime::formatMoney`; CRN-031 **₹75,089.00**; INV-119 **₹177,592.00**; Phase 5 not started |

---

## 20. Confirmation that Phase 5 was not started

No Phase 5 plan, Screen IDs, routes, migrations, or seeders were added. Credit Note cancellation remains deferred (D4). Automatic IRN, Alankit, IRN queue/job/retry, and Credit Note number reuse were not implemented.

Mandatory matrix 1–28 is covered by `CreditNoteG1Test`, `InvoiceReservationTest`, `LaterInvoiceAndUnbilledTest`, `GoldenCreditNoteTest`, `Phase4AuthorizationHttpTest`, `Phase4FrameHttpTest`, `Phase4MobileFrameHttpTest`, `Phase4SchemaTest`, `ProductionRouteAbsenceTest`, and the surviving Phase 0–3 suite.
