# Phase 6 plan (IAM screens and Audit completeness) — corrected, decisions approved

**Status:** P6-D1–P6-D12 are **Approved and binding.** **Phase 6 remains unimplemented.** The corrected plan is ready for Business Owner **implementation** approval. **Phase 7 was not started.**  
**Closed baseline:** Phase 5 commit `3d86e279c03fa94a9c76a54db08934338df34b9e`. Suite **263 tests / 9,002 assertions / 0 failures**.  
**Stack (locked):** CodeIgniter 4, PHP 8.2+, MySQL 8 InnoDB, server-rendered Views. No JWT, OAuth, IdP, Shield, React, Next.js, NestJS, PostgreSQL, numeric role hierarchy, or Company Admin.

Do not implement until a separate implementation authorisation is given. This turn is documentation only.

---

## Binding decisions (P6-D1–P6-D12) — Approved

| ID | Title | Binding outcome | Status |
|---|---|---|---|
| **P6-D1** | Anti-escalation | Hybrid **C** (§7). Active-role subset; Company/Plant ⊆ editor; Super Admin and SYSTEM protected; ungrantable options visible+disabled; unauthorised POST **403**. | **Approved** |
| **P6-D2** | New-user credentials | Cryptographic one-time password; shown once; Argon2id only; 24-hour expiry; first-login password-change gate; no SMTP; regenerate invalidates prior credential and sessions. | **Approved** |
| **P6-D3** | Session invalidation | Exact matrix in §9. `session_version` only. Lock `app_user` by ascending `user_id`. No JWT. | **Approved** |
| **P6-D4** | Self-administration | No self-deactivation. Profile text allowed. Security/self-role changes follow anti-escalation + last Super Admin; no silent replacements. | **Approved** |
| **P6-D5** | Last Super Admin / SYSTEM | ≥1 enabled interactive Super Admin. SYSTEM does not count and is not an IAM option. Recheck under locks before commit. | **Approved** |
| **P6-D6** | Reactivation | In scope under `IAM.MANAGE_USERS`. Does not restore grants. May issue a new D2 credential. | **Approved** |
| **P6-D7** | Revoke sessions | UI + POST under `IAM.MANAGE_USERS`. No new permission code. CSRF. 403 if unauthorised. | **Approved** |
| **P6-D8** | Role identity | Immutable `role_id`. No `role_code`. Unique `role_name`. Rename does not rewrite historical audit JSON. | **Approved** |
| **P6-D9** | Role deactivation | Additive `is_active` + `row_version` (absent today). No DELETE. Super Admin/SYSTEM never deactivated. Not while in use as `active_role_id`. | **Approved** |
| **P6-D10** | Audit export | **Preserve Phase 1 `AUDIT.EXPORT` and `POST /audit/export`.** Independent of `REPORT.EXPORT`. Seed matrix unchanged. Unauthorised POST **403**. | **Approved** |
| **P6-D11** | Audit retention | Append-only. No Phase 6 purge. No UPDATE/DELETE service. Future retention needs a separate BO decision. | **Approved** |
| **P6-D12** | Frames | **32 new IAM frames FR-0569–FR-0600** (26 D + 6 M + 0 T). AUD FR-0601–FR-0606 Phase 1 only; preserve three AUD aliases; no duplicates. | **Approved** |

---

## 1. Objective and non-goals

### In scope

- **IAM-SCR-001** User Access Assignment (32-frame share)
- **IAM-SCR-002** Role Permission Matrix (32-frame share)
- **AUD-SCR-001** completeness/integration of the **existing six** Phase 1 frames
- Supporting first-login **password-change** flow (P6-D2). This is **not** a 33rd IAM CSV frame and must not be given an invented Frame Name.

### Out of scope

Phase 7; JWT/Shield/SMTP; Company Admin; numeric hierarchy; physical DELETE; rebuilding Phase 0–5 tables; changing seeded permission grants; automatic IRN; valuation; `INWARD.APPROVE`; duplicating AUD frames; production `?frame=`; seed/demo/probe routes; emailing passwords.

### Frozen identities and figures

Five Figma roles; R. Kumar / S. Patil (Store Keeper + Billing Clerk, one active) / A. Deshpande / N. Joshi; Apex RMC + Apex Aggregates; Chakan / Talegaon / Ranjangaon. SYSTEM actor/role preserved. Ledger/inventory **116.000 MT**; allocation **30.000**; LOT-2026-00230 **13.000**; disposition **12.000**; WST-2026-000067 Approved; INV-119 **₹177,592.00**; CRN-031 **₹75,089.00**; `INWARD.REVERSE` Store Keeper + Super Admin only.

---

## 2. Source precedence and source gaps

| Rank | Source |
|---|---|
| 1 | Live schema, seeders, tests, and routes at Phase 5 close `3d86e27` |
| 2 | Approved Phase 0–5 plans (including Phase 1 **Final Decision 2 — `AUDIT.EXPORT`**) |
| 3 | Authoritative CSV frame manifest (session file; gitignored) |
| 4 | Approved Frame-Name Aliases (12 rows) |
| 5 | This corrected Phase 6 plan (P6-D1–P6-D12 binding) |
| 6 | Figma URL (visual; canvas not fetched this turn) |

### Why the earlier “AUDIT.EXPORT absent” assumption is superseded (P6-D10)

The first Phase 6 planning prompt assumed AUD-SCR-001 export was still absent. **Repository evidence takes precedence:**

- `docs/PHASE1_PLAN.md` Final Decision 2 (12 Sep 2026) **added** `AUDIT.EXPORT`.
- `PermissionCatalog` includes `AUDIT.EXPORT`.
- Super Admin receives the **full catalogue** (`SampleIdentities::permissionCodesFor`).
- `POST /audit/export` → `AuditController::export` → `AuditQuery::requestExport` → `AUDIT_EXPORT_REQUESTED`.
- Tests: `AuthAndAuthorisationTest::testAuditExportIsSuperAdminOnlyInSeed`, `Phase1FrameHttpTest` export present/absent, `InvoiceIrnAndAuditTest::testAuditExportWritesEventAndLeavesRowsImmutable`.
- Phase 5 P5-D10 only forbade **creating** `AUDIT.EXPORT` for **RPT-SCR-013**; it did not revoke Phase 1.

Therefore Phase 6 **preserves** `AUDIT.EXPORT` and must not replace it with `REPORT.EXPORT`. The two codes remain independent.

### CSV / Figma availability

Readable this session (gitignored, **not in git**):

- `uploads/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1_4e22.csv`
- `uploads/Approved-Frame-Name-Aliases_61ba.md`

**Preservation (after implementation is authorised, not in this turn):** copy unmodified to:

- `docs/reference/RMC_Crusher_ERP_Phase1_Figma_Frame_Component_Manifest_v1.1.csv`
- `docs/reference/Approved-Frame-Name-Aliases.md`
- SHA-256 in `docs/reference/SOURCE_CHECKSUMS.md`

If policy later forbids committing those files, record original filename, SHA-256, and controlled external location instead. **Do not modify contents when copying.**

### Schema evidence gap

Live `SHOW COLUMNS` was not executed in the planning sandbox. Contract below is from committed migrations (`2026-09-11-180002_CreateIamTables`, `2026-09-12-100001_AlterAppUserForSystemActor`) plus seeders/services. Implementation Checkpoint A must re-verify `information_schema` before issuing ALTER.

### Seed-data gap (email / employee code)

`UserSeeder` / `SampleIdentities::users()` provide `username` and `display_name` only. **No email and no employee_code exist in repository seed data.** Do **not** fabricate production-looking addresses. See §11 backfill.

---

## 3. Exact authoritative frame inventory (P6-D12 Approved)

### New Phase 6 IAM frames — exactly 32 (FR-0569–FR-0600)

26 desktop + 6 mobile + **0 tablet**. CSV direct matches **32/32**. Missing: none. Unexpected: none. IAM aliases: **none**.

| Frame ID | Frame Name | Device | Screen |
|---|---|---|---|
| FR-0569 | `D-IAM-SCR-001-List-Default` | D | IAM-SCR-001 |
| FR-0570 | `D-IAM-SCR-001-List-Filtered` | D | IAM-SCR-001 |
| FR-0571 | `D-IAM-SCR-001-List-Empty` | D | IAM-SCR-001 |
| FR-0572 | `D-IAM-SCR-001-List-Loading` | D | IAM-SCR-001 |
| FR-0573 | `D-IAM-SCR-001-Edit-Default` | D | IAM-SCR-001 |
| FR-0574 | `D-IAM-SCR-001-Edit-PermissionRestricted` | D | IAM-SCR-001 |
| FR-0575 | `D-IAM-SCR-001-Edit-Error` | D | IAM-SCR-001 |
| FR-0576 | `D-IAM-SCR-001-Edit-Draft` | D | IAM-SCR-001 |
| FR-0577 | `D-IAM-SCR-001-Edit-ConcurrentUpdate` | D | IAM-SCR-001 |
| FR-0578 | `D-IAM-SCR-001-Create-Default` | D | IAM-SCR-001 |
| FR-0579 | `M-IAM-SCR-001-Edit-Default` | M | IAM-SCR-001 |
| FR-0580 | `D-IAM-SCR-001-View-Default` | D | IAM-SCR-001 |
| FR-0581 | `D-IAM-SCR-001-View-Cancelled` | D | IAM-SCR-001 |
| FR-0582 | `D-IAM-SCR-001-Create-Loading` | D | IAM-SCR-001 |
| FR-0583 | `M-IAM-SCR-001-Create-Default` | M | IAM-SCR-001 |
| FR-0584 | `M-IAM-SCR-001-Edit-Error` | M | IAM-SCR-001 |
| FR-0585 | `D-IAM-SCR-002-List-Default` | D | IAM-SCR-002 |
| FR-0586 | `D-IAM-SCR-002-List-Loading` | D | IAM-SCR-002 |
| FR-0587 | `D-IAM-SCR-002-List-Empty` | D | IAM-SCR-002 |
| FR-0588 | `D-IAM-SCR-002-Edit-Default` | D | IAM-SCR-002 |
| FR-0589 | `D-IAM-SCR-002-Edit-Filtered` | D | IAM-SCR-002 |
| FR-0590 | `D-IAM-SCR-002-Edit-PermissionRestricted` | D | IAM-SCR-002 |
| FR-0591 | `D-IAM-SCR-002-Edit-Draft` | D | IAM-SCR-002 |
| FR-0592 | `D-IAM-SCR-002-Edit-ConcurrentUpdate` | D | IAM-SCR-002 |
| FR-0593 | `D-IAM-SCR-002-Create-Default` | D | IAM-SCR-002 |
| FR-0594 | `M-IAM-SCR-002-Edit-Default` | M | IAM-SCR-002 |
| FR-0595 | `D-IAM-SCR-002-Edit-Error` | D | IAM-SCR-002 |
| FR-0596 | `D-IAM-SCR-002-View-Default` | D | IAM-SCR-002 |
| FR-0597 | `D-IAM-SCR-002-View-Cancelled` | D | IAM-SCR-002 |
| FR-0598 | `D-IAM-SCR-002-Create-Loading` | D | IAM-SCR-002 |
| FR-0599 | `M-IAM-SCR-002-Create-Default` | M | IAM-SCR-002 |
| FR-0600 | `M-IAM-SCR-002-Edit-Error` | M | IAM-SCR-002 |

Do not add these 32 names to Phase 1 `FrameCatalog::all()` (88 frames). Use a Phase 6 catalog module at implementation time.

### Existing six AUD frames — integrate, do not duplicate

Already in `FrameCatalog::all()`. Regression-test; **do not count as new Phase 6 frames.**

| Frame ID | Canonical name | Approved Figma alias (do not create a second frame) |
|---|---|---|
| FR-0601 | `D-AUD-SCR-001-View-Default` | `D-AUD-SCR-001-Default` |
| FR-0602 | `D-AUD-SCR-001-View-Filtered` | `D-AUD-SCR-001-DetailExpanded` |
| FR-0603 | `D-AUD-SCR-001-View-Empty` | — |
| FR-0604 | `D-AUD-SCR-001-View-Loading` | — |
| FR-0605 | `D-AUD-SCR-001-View-PermissionRestricted` | `D-AUD-SCR-001-ExportRestricted` |
| FR-0606 | `M-AUD-SCR-001-View-Default` | — |

---

## 4. Existing schema / service / route audit

### 4.1 `app_user` (do not add `full_name`)

| Column | Equivalent UI | Phase 6 |
|---|---|---|
| `display_name` VARCHAR(200) | Full name (UI max 120) | **Reuse.** Do not add `full_name`. |
| `username` unique | Login id | Keep; authenticate() key |
| `password_hash` | — | Argon2id; unchanged for existing rows |
| `active_role_id` FK → `role` | Active role | Service must also prove `user_role` |
| `session_version` | — | P6-D3 |
| `is_disabled` | Status Active/Inactive | Keep |
| `is_system` | SYSTEM actor | Protect (P6-D5) |
| timestamps | Created / Last modified | Keep |
| email / employee_code / must_change_password / temp expiry / row_version | — | **Absent → additive** |

MySQL cannot CHECK `active_role_id ∈ user_role` or plant∈company mapping.

### 4.2 `role`

`role_id`, unique case-sensitive `role_name`, `is_system` (five Figma roles **and** SYSTEM are `1` — **not** a privilege level), timestamps. **No** `description`, `is_active`, `role_code`, `row_version`.

### 4.3 Grants, switch, audit, sessions

`user_role`, `user_company_access`, `user_plant_access` (NULL plant_id = all plants in that company), `role_switch_history`, `audit_event` append-only by service convention, `ci_sessions`, `login_attempt`, `idempotent_command` (no IAM types yet).

`AuthService::invalidateSessions`, `SwitchRoleService` (`IAM_ROLE_SWITCH` + history + `session_version++`), `SessionVersionFilter` (`AUTH_SESSION_REJECTED` → login redirect / AJAX **401**).

`AuditController::export` without `AUDIT.EXPORT` currently **redirects** with flash — completeness must make unauthorised POST **HTTP 403**.

### 4.4 AUDIT.EXPORT seed-grant matrix (live catalogue + `SampleIdentities` + tests)

`AUDIT.EXPORT` is in `PermissionCatalog`. Super Admin is granted **every** catalogue code. Other roles list permissions explicitly; only Accounts Manager includes any `AUDIT.*`, and that list is **`AUDIT.VIEW` only**.

| Role | `AUDIT.VIEW` | `AUDIT.EXPORT` | `REPORT.EXPORT` | `IAM.MANAGE_USERS` | `IAM.MANAGE_ROLES` | `IAM.VIEW` | `IAM.SWITCH_ROLE` |
|---|---|---|---|---|---|---|---|
| Super Admin | Yes (full catalogue) | **Yes** | Yes | Yes | Yes | Yes | Yes |
| Plant Manager | No | **No** | Yes | No | No | No | No |
| Store Keeper | No | **No** | No | No | No | No | Yes |
| Billing Clerk | No | **No** | No | No | No | No | Yes |
| Accounts Manager | **Yes** | **No** | Yes | No | No | No | No |
| SYSTEM | No | **No** | No | No | No | No | No |

Proven by `AuthAndAuthorisationTest::testAuditExportIsSuperAdminOnlyInSeed`. **Do not grant `AUDIT.EXPORT` to additional roles.** RPT-SCR-013 continues to use `REPORT.EXPORT` only.

---

## 5. IAM-SCR-001 — User Access Assignment

Fields: Full name → `display_name` (required, max 120); Email → additive unique lowercase; Employee code → additive, unique when supplied, NULL allowed; Status → `is_disabled`; Created/Last modified read-only; Roles multi-select min 1; Active role exactly one of those; Companies min 1 for enabled operational users; Plants ⊆ selected companies ∩ editor plants ∩ `company_plant`.

Exact active-role copy:

> A user may hold multiple roles but operates under exactly one active role at a time. Permissions are evaluated only against the active role.

Ungrantable Super Admin / SYSTEM / out-of-scope Company or Plant: **visible and disabled**.

Permission reason:

> Disabled — Permissions above your administrative level. A role cannot grant permissions it does not already hold.

Scope reason:

> Disabled — You cannot grant Company or Plant access outside your own administrative scope.

No Super Admin checkbox. No permission union. No physical DELETE. SYSTEM omitted from operational pickers (not an interactive administrative option).

S. Patil remains the multi-role sample (Store Keeper + Billing Clerk, one active).

If the current active role is removed, the **same POST** must set another assigned role or the mutation is **rejected**. Same for current Company/Plant context. **Do not silently pick an arbitrary replacement.**

One-time password: shown **only** on the successful create response. Leaving or refreshing that response must **not** redisplay it (flash-once / consume token). Regeneration (P6-D6) uses the same D2 flow and invalidates the previous hash and all sessions.

---

## 6. IAM-SCR-002 — Role Permission Matrix

Role name required max 60; identifier = read-only `role_id` (P6-D8); description additive; status additive `is_active`; search / expand / collapse; **Billing Clerk** never Billing Manager.

Seven groups (each permission in exactly one):

1. Masters — COMPANY, PLANT, CLIENT, CLIENT_SITE, VENDOR, PRODUCT, RAW_MATERIAL, UNIT, RECIPE, VEHICLE, RATE, CHARGE, STATUTORY  
2. Material — INWARD, OPENING_STOCK, WASTAGE, DISPATCH  
3. Challans — CHALLAN  
4. Invoices — INVOICE except `INVOICE.RECORD_IRN_DETAILS`  
5. Credit Notes and IRN — `CREDIT_NOTE.*` + `INVOICE.RECORD_IRN_DETAILS`  
6. Reports — REPORT, DASHBOARD  
7. Administration — IAM, AUDIT  

Collapsed groups show selected count. Disabled permissions use the permission reason in §5. Duplicate `role_name` → Edit-Error.

SYSTEM role: not editable, not deactivatable, not assignable in UI.

Before deactivating a custom role: report **assigned-but-inactive** holders. If it is `active_role_id` for any **enabled** user → reject. If allowed, hide from future selection; do **not** auto-migrate users; keep historical `role_id` references.

---

## 7. Formal anti-escalation (P6-D1 Approved — Hybrid C)

1. Evaluation uses **only** the editor’s active role.  
2. Editor may grant only permissions that active role holds.  
3. Inactive assigned roles do **not** widen authority.  
4. Target Company grants ⊆ editor’s Company grants.  
5. Target Plant grants ⊆ editor’s Plant grants **and** the target’s selected Companies **and** active `company_plant` mappings.  
6. Super Admin role is explicitly protected (assignable only by an editor whose **active** role is Super Admin).  
7. SYSTEM role/user is protected; never an interactive administrative option; never interactive login.  
8. Do **not** infer hierarchy from `role_id`, `is_system`, or role name.  
9. No numeric level column.  
10. No Company Admin.  
11. Ungrantable options remain visible and disabled with the reasons in §5.  
12. Direct unauthorised POST → **403**.

---

## 8. New-user credentials (P6-D2 Approved)

1. Cryptographically secure one-time temporary password (`random_bytes` / equivalent; not predictable).  
2. Display **exactly once** immediately after successful create.  
3. Never email plaintext. No SMTP dependency.  
4. Never persist plaintext.  
5. Store **only** Argon2id (`PasswordHasher`).  
6. Require password change at **first successful login**.  
7. Temporary credentials expire **24 hours** (`temporary_password_expires_at_utc`).  
8. Until changed, permit **only** authentication, password-change screen, and logout.  
9. No ERP module access before password change.  
10. Regenerating a temporary credential invalidates the previous hash **and all sessions** (`session_version++`).  
11. Never place password, hash, reset token, session ID, or CSRF token in logs or audit JSON.  
12. Existing seeded users: `must_change_password = 0`, expiry NULL, **hashes unchanged**.  
13. Additive columns only as §11.  
14. Login remains `username`. Email is not a login alias in Phase 6.

Supporting routes (not CSV frames): `GET/POST` password-change under the authenticated group, plus a filter that blocks all other routes while `must_change_password = 1` or while a still-valid temporary credential has not been replaced. Expired temporary credentials fail authentication with the generic login error.

The one-time value must not redisplay after navigation or refresh.

---

## 9. Session invalidation matrix (P6-D3 Approved)

Mechanism: increment `app_user.session_version`. Next authenticated request: destroy stale session; browser → login; API-style → **401**. Permission/scope denial **without** invalidation remains **403**. No JWT.

Lock affected `app_user` rows in **ascending `user_id`**. Complete the authorised transaction first. The current admin request may finish (including in-request cookie refresh **only** for active-role switch, which already does so). Other invalidations take effect on the **next** request.

| Change | Increment `session_version` |
|---|---|
| User deactivated | **Yes** |
| User reactivated | **Yes** |
| Assigned role added | **Yes** |
| Assigned role removed | **Yes** |
| Active role removed | Mutation must **first** select a valid replacement active role or be **rejected**; then **Yes** |
| Company grant added | **Yes** |
| Company grant removed | **Yes** |
| Plant grant added | **Yes** |
| Plant grant removed | **Yes** |
| Permission added to a role | **Yes**, for **enabled** users whose `active_role_id` is that role |
| Permission removed from a role | **Yes**, for **enabled** users whose `active_role_id` is that role |
| Role deactivated | **Not allowed** while it is `active_role_id` for any enabled user |
| Password changed / reset / temp regenerated | **Yes** |
| Forced session revocation | **Yes** |
| Active-role switch | **Yes** (existing Phase 0: history + `IAM_ROLE_SWITCH` + cookie refresh) |
| Non-security profile text only (`display_name`, email, employee_code) | **No**, unless credentials or access also changed |

Active-role switches continue to write `role_switch_history` and audit. Other access changes write audit **only** — never fake switch-history rows.

If the editor’s own active role’s permissions change: commit, bump including the editor, **do not** silently keep the old cookie valid; next request reauthenticates (P6-D4).

---

## 10. Self-administration (P6-D4) and last Super Admin / SYSTEM (P6-D5)

- No administrator may deactivate their **currently authenticated** user.  
- Own non-security profile fields may be edited.  
- Own active-role permission edits must satisfy Hybrid C, must not remove the last viable Super Admin administration path, invalidate affected sessions including the editor after commit; current request may finish; next request reauthenticates.  
- Editor cannot remove their own active role unless another assigned role is selected **atomically**.  
- Editor cannot remove current Company/Plant context without atomically selecting another **valid** context.  
- **No arbitrary silent replacement.**

P6-D5:

- ≥1 enabled (`is_disabled=0`), interactive (`is_system=0`) user assigned to the **active Super Admin** role.  
- SYSTEM does **not** count.  
- The final such Super Admin cannot be deactivated, stripped of Super Admin, or left without Company/Plant access required for administration.  
- Super Admin role cannot be deactivated.  
- SYSTEM role and SYSTEM actor cannot be edited, deactivated, assigned through the UI, or used for interactive login.  
- Lock and **recheck the invariant immediately before commit** (not only a pre-transaction count).

---

## 11. Additive schema proposal and backfill risks

Verify with `information_schema` at Checkpoint A. Add **only** what is absent. **No table rebuild. No Phase 0–5 FK/index removal.** `down()` drops only Phase 6 additions. Existing `role_id` / `user_id` / password hashes unchanged.

### `app_user` — add

| Column | Why |
|---|---|
| `email` VARCHAR(254) NULL, UNIQUE | Figma identity. Normalise **lowercase** before uniqueness. Multiple NULL allowed. |
| `employee_code` VARCHAR(20) NULL, UNIQUE | Optional. Multiple NULL allowed. Unique when supplied. |
| `must_change_password` TINYINT(1) NOT NULL DEFAULT 0 + CHECK | P6-D2 |
| `temporary_password_expires_at_utc` DATETIME(6) NULL | 24-hour temp expiry |
| `row_version` INT UNSIGNED NOT NULL DEFAULT 1 | Concurrency |

**Do not add `full_name`.** Use existing `display_name`.

### `role` — add

| Column | Why |
|---|---|
| `description` TEXT NULL | Figma |
| `is_active` TINYINT(1) NOT NULL DEFAULT 1 + CHECK | P6-D9 |
| `row_version` INT UNSIGNED NOT NULL DEFAULT 1 | Concurrency |

**Do not add `role_code`.**

Optional: widen `idempotent_command.chk_idem_type` with `IAM_USER_SAVE`, `IAM_ROLE_SAVE`.

### Backfill (no invented production identities)

| Field | Existing four demo users | SYSTEM |
|---|---|---|
| Full name | Already `display_name`: R. Kumar, S. Patil, A. Deshpande, N. Joshi | `SYSTEM` |
| Email | **No seed value.** Leave **NULL**. **Migration dependency:** do not invent `@` addresses. New IAM creates require email. Editing an existing user in IAM-SCR-001 requires supplying email from then on. | NULL |
| Employee code | NULL | NULL |
| `must_change_password` | **0** (demo passwords already in `UserSeeder`; hashes unchanged) | 0 |
| Temp expiry | NULL | NULL |
| `row_version` | 1 | 1 |
| `role.is_active` | 1 | 1 |
| `role.description` | NULL | NULL |

**Risk:** unique email is required for **new** users; existing demo logins continue via `username` with NULL email until an administrator saves an email. If BO later supplies explicit demo emails, they may be added in a **non-production** seeder only (`DemoSeedingGuard`), never as fabricated production identities in the migration.

---

## 12. Reactivation (P6-D6) and revoke sessions (P6-D7)

Reactivation: `IAM.MANAGE_USERS`; target must already have valid role + Company + Plant grants (do **not** silently restore removed grants); `session_version++`; immutable audit; do not reuse or reveal an old temporary password; if the user cannot authenticate, issue a **new** D2 one-time credential (once, on that success response).

Revoke sessions: existing `IAM.MANAGE_USERS`; increment `session_version` only; **do not** change roles, permissions, or status; CSRF; audit without session IDs; unauthorised POST **403**. Reuse `AuthService::invalidateSessions(..., disableUser: false)` or equivalent.

---

## 13. AUD-SCR-001 completeness (export preserved)

Eight columns: Timestamp, User, Active Role, Action, Entity, Reference, Company, Plant.  
Eight filters: Date Range (Asia/Kolkata half-open UTC, From ≯ To), User, Active Role, Action, Entity, Reference, Company, Plant.

Newest first; immutable; no edit/delete controls or routes; row expand before/after omitting unchanged keys; stored UTC + local Asia/Kolkata; Role Switch shows Previous and New Active Role from `IAM_ROLE_SWITCH` / history; exact empty copy **`No audit events match the selected filters.`**; inaccessible Company/Plant filter → **403** not empty 200; production ignores `?frame=`; mobile event cards.

Export: only if active role has `AUDIT.EXPORT`; PermissionRestricted **removes** Export; unauthorised POST **403**; same eight filters + current scope; writes `AUDIT_EXPORT_REQUESTED` (already exists); never `REPORT.EXPORT`.

---

## 14. Route / permission / transaction matrix

Existing codes only. CSRF on POST. `FOR UPDATE` on `app_user` ascending id, then `role`. `row_version` on user/role writes.

| Method | Path | Permission | Notes | Audit |
|---|---|---|---|---|
| GET | `/iam/users` | `IAM.VIEW` | Scope overlap | — |
| GET | `/iam/users/create` | `IAM.MANAGE_USERS` | | — |
| POST | `/iam/users` | `IAM.MANAGE_USERS` | D2 one-time password flash-once | `IAM_USER_CREATED` |
| GET | `/iam/users/{id}` | `IAM.VIEW` | Hide SYSTEM or 404 | — |
| GET | `/iam/users/{id}/edit` | `IAM.MANAGE_USERS` | | — |
| POST | `/iam/users/{id}` | `IAM.MANAGE_USERS` | `row_version`; no silent replacements | UPDATED / ROLES / COMPANIES / PLANTS_CHANGED |
| POST | `/iam/users/{id}/deactivate` | `IAM.MANAGE_USERS` | Not self; not last SA | `AUTH_USER_DISABLED` (existing name; do not synonym) |
| POST | `/iam/users/{id}/reactivate` | `IAM.MANAGE_USERS` | Grants must already be valid | `IAM_USER_REACTIVATED` |
| POST | `/iam/users/{id}/revoke-sessions` | `IAM.MANAGE_USERS` | Status/roles unchanged | `AUTH_SESSIONS_REVOKED` |
| POST | `/iam/users/{id}/temporary-password` | `IAM.MANAGE_USERS` | New D2 credential; invalidate sessions | no secrets in JSON |
| GET | `/iam/roles` | `IAM.VIEW` | SYSTEM not a picker | — |
| GET | `/iam/roles/create` | `IAM.MANAGE_ROLES` | | — |
| POST | `/iam/roles` | `IAM.MANAGE_ROLES` | Subset | `IAM_ROLE_CREATED` |
| GET | `/iam/roles/{id}` | `IAM.VIEW` | | — |
| GET | `/iam/roles/{id}/edit` | `IAM.MANAGE_ROLES` | Not SYSTEM | — |
| POST | `/iam/roles/{id}` | `IAM.MANAGE_ROLES` | `row_version` | UPDATED / PERMISSIONS_CHANGED |
| POST | `/iam/roles/{id}/deactivate` | `IAM.MANAGE_ROLES` | P6-D9 | `IAM_ROLE_DEACTIVATED` |
| GET/POST | password-change | authenticated + D2 gate | Not a CSV frame | password change audit **without** secrets |
| GET | `/audit` | `AUDIT.VIEW` | 8 filters; scope 403 | — |
| POST | `/audit/export` | `AUDIT.EXPORT` | Same filters/scope; **403** if missing | `AUDIT_EXPORT_REQUESTED` |
| POST | `/session/switch-role` | held role | Existing | `IAM_ROLE_SWITCH` + history |

No DELETE routes. Unauthorised POST → 403 even if the control is absent.

---

## 15. Audit-event catalogue

Reuse: `IAM_ROLE_SWITCH`, `AUTH_SESSION_REJECTED`, `AUTH_SESSIONS_REVOKED`, `AUTH_USER_DISABLED`, `AUTH_LOGIN`, `AUTH_LOGOUT`, `AUTH_LOGIN_FAILURE`, `AUTH_LOGIN_THROTTLED`, `AUDIT_EXPORT_REQUESTED`.

Add (no synonyms for the above): `IAM_USER_CREATED`, `IAM_USER_UPDATED`, `IAM_USER_REACTIVATED`, `IAM_USER_ROLES_CHANGED`, `IAM_USER_COMPANIES_CHANGED`, `IAM_USER_PLANTS_CHANGED`, `IAM_ROLE_CREATED`, `IAM_ROLE_UPDATED`, `IAM_ROLE_PERMISSIONS_CHANGED`, `IAM_ROLE_DEACTIVATED`.

Changed fields only. No passwords, hashes, temporary credentials, reset tokens, session IDs, CSRF tokens. Role rename must not rewrite historical JSON; snapshots keep the name captured at event time.

---

## 16. Responsive plan

Six mobile IAM frames: 390×844 shell, 44×44 hamburger, closed drawer, no stacked sidebar, no horizontal overflow, sticky footer only when an action exists. Multi-select sheets; wrapping chips; disabled reasons readable. IAM-002: full-width accordions with collapsed selected counts. AUD mobile: cards. No tablet Phase 6 frames.

---

## 17. Sample data / frozen regression

Do not change seed permission matrix, five Figma roles, SYSTEM, demo usernames, or frozen inventory/financial figures. IAM UI **reads** S. Patil as Edit-Default.

---

## 18. Security and privacy

Hybrid C; Argon2id; CSRF; first-login gate; 24h temp expiry; flash-once password; `session_version`; 401 vs 403 as §9; append-only audit; no SMTP.

---

## 19. Automated-test plan

Preserve Phase 5 baseline **263 / 9,002 / 0**. Do not invent a final Phase 6 count.

Keep the original plan’s tests 1–52, **plus**:

1. One-time password shown only once (refresh does not redisplay).  
2. Only Argon2id hash stored.  
3. Temporary password expires after 24 hours.  
4. First login permits only password change / logout.  
5. Password change clears `must_change_password`.  
6. Temp credential regeneration invalidates the previous credential.  
7. Secrets never appear in audit JSON.  
8. Permission changes invalidate users currently active under that role.  
9. Inactive assigned role does not widen administration.  
10. Self security change completes and invalidates the editor’s next request.  
11. No arbitrary replacement active role or scope.  
12. Last interactive Super Admin protection is concurrency-safe (two concurrent deactivations).  
13. SYSTEM cannot be edited or used interactively.  
14. Reactivation does not silently restore grants.  
15. Revoke sessions changes no business access data.  
16. `AUDIT.EXPORT` and `REPORT.EXPORT` remain independent.  
17. Existing `AUDIT.EXPORT` seed grants unchanged.  
18. Audit export uses all eight filters and current scope.  
19. Audit export writes its own immutable audit event.  
20. Existing six AUD frames are not duplicated.  
21. Exactly 32 new IAM frames are covered.  
22. Phase 0–5 suite remains green.  
23. Frozen inventory and financial reconciliations unchanged.

---

## 20. Proposed checkpoints (not executed)

| CP | Scope |
|---|---|
| **0** | Implementation authorisation of this corrected plan; copy CSV + aliases + checksums into `docs/reference/` |
| **A** | Additive migration + `information_schema` verify + rollback; no invented emails |
| **B** | Domain services, Hybrid C, D2 hasher/expiry/gate, last Super Admin under locks |
| **C** | IAM-SCR-001 desktop |
| **D** | IAM-SCR-001 mobile |
| **E** | IAM-SCR-002 desktop |
| **F** | IAM-SCR-002 mobile |
| **G** | AUD completeness + export 403 + eight filters |
| **H** | Session matrix, first-login gate, concurrency, SYSTEM |
| **I** | 32 frames + six AUD regression + Phase 0–5 green |

Tests before each commit. No next CP until the current one passes. No Phase 7.

---

## 21. Entry / exit

**Implementation entry:** this corrected plan authorised; baseline still green; CSV 32 names; source files preserved or checksummed.  
**Exit:** 32 IAM frames; six AUD integrated not duplicated; D1–D11 behaviour; Phase 0–5 green; frozen figures; Phase 7 not started.

---

## 22. Decision register (approved wording)

All P6-D1–P6-D12 are **Approved** as written in this document. The previous “AUDIT.EXPORT absent” planning assumption is **withdrawn** in favour of Phase 1 repository evidence (P6-D10). Email backfill remains a **migration dependency** (no fabricated addresses).

---

## 23. Phase 7

Phase 7 was **not** started.

---

**Phase 6 remains unimplemented. The corrected plan is ready for Business Owner implementation approval. Phase 7 was not started.**
