# RESUME — Permissions Screen Redesign + Enforcement Hardening — ✅ COMPLETE

**Status:** All 11 WPs done + 2 review-fix rounds. Deployed to `/app` (owner tests live). **Awaiting owner `/fullpush`** — no merge to main was performed.
**Branch:** `hazemdev2` (both repos). **Dev DB:** `moonui2_dev_be` (forward-only migrations; never wiped).
**Ledger:** `LEDGER.md` — the full blow-by-blow: every WP, every finding, every deviation, every correction.

## What shipped

**1. The permissions screen (`/core/roles`) was rebuilt.** A flat list of ~1000 identical checkboxes → a **resource × action matrix**: one row per resource, columns for View/Create/Edit/Delete/Approve/Post. A module drops from ~50 scrolling rows to ~9. Every permission now carries an **info tooltip** explaining what granting it actually does; **destructive and ledger-committing actions are red**; ready-made templates (Viewer / Data entry / Approver / Full); cross-module search; permissions that exist in the API but have no screen are **labelled as such** instead of silently doing nothing.

**2. A BE `PermissionCatalog`** (`Modules/Core/app/Support/PermissionCatalog.php`, generalising `LisScreenCatalog`) gives every permission a bilingual label + description + `danger` flag, served through `GET /core/permissions` (backward-compatible). A CI test keeps every catalog key honest against the seeder — a typo'd key fails the build.

**3. Action-level enforcement, app-wide.** ~523 gate strings across Accounting → Sales → Purchases → Inventory → Production → HR → LIS/Core/POS/Clinic, **each taken from the BE controller's `middleware()`**, never from the button's name. **Zero dead gates** (verified app-wide). New `appCanAll` (ALL-OF) directive added once in the shared layer for chained actions.

**4. "Every setting carries its own permission."** `setting_definitions.permission`, enforced per-setting on the write path. Module settings are gated by a per-MODULE rule, so **a setting added tomorrow is gated automatically** instead of silently shipping under the broad core gate. `crm.settings.*` was **deleted** rather than shipped dead (CRM has no settings).

**5. The owner's original ask:** removing `sales.orders.create` from a user now **hides** the blue-arrow "convert quotation → sales order" button (`quotations.component.html:146`). The BE already returned 403; the gap was purely FE. All 5 convert/create buttons are gated on the **target document's** permission.

## Security holes found and closed (none of these were the original ask)

| Hole | Impact |
|---|---|
| `POST /hr/dev/reset-data` — **no permission middleware at all** | Truncates payrolls, adjustments, loans, EOS. Guarded only by `env != production`; this host is `local`. Any authenticated user could destroy every payroll record. |
| Payroll adjustment `payOut` on the **data-entry** permission | A "payroll data entry" role built deliberately without `.pay` could create an adjustment **and disburse it**. |
| Payroll adjustment `approve`/`reject` — **no middleware** | Any authenticated user could approve a payroll adjustment. |
| LIS `sendOut` — **ungated** | Outsources tests to an external lab → **creates a payable**. Now gated on `lis.samples.send-external`, consistent with its sibling `LabExternalSampleController`. |
| LIS referral `addTest`/`removeTest`/`send` — **ungated** | Changes what the external lab **bills you**. |
| LIS `regenerate-invoice` — **ungated** | Cancels the live lab invoice and rebuilds it, wiping applied insurance/discounts. |
| Setup wizard **swallowed setting-write 403s** | Reported "success" while **zero GL mappings saved** → every later journal entry posts to the wrong account. |
| POS offline sync | Cash collected at the till, the post 403'd → **no journal entry, invoice still unpaid**. Also (pre-existing, unrelated to permissions): **every offline CARD sale would have 422'd** on a dropped `credit_card→card` mapping. |
| Clinic pricing matrix | A view-only user saw a **fully editable price grid**; every keystroke 403'd. Fixed with `[readonly]`, not `*appCan` (hiding the cells would destroy the read view). |
| Cashier could not refund | Stock `cashier` held no `sales.returns.*`. Now granted `.create`+`.view` only — raises a refund, a supervisor approves. Separation of duties kept. |
| `SalesCommissionRuleController::show` | Commission rates/tiers readable by any authenticated user. |

Plus, across the modules: the **submit-for-approval** button was ungated in Sales and in **all four** Purchases documents (approve/reject were gated — same BE middleware); a **privilege leak** let an entry-level lab tech trigger a retest; several buttons were gated on the *wrong* permission — visible to people who'd 403, hidden from the people entitled to them.

## Tests
Core **435 passed / 7 failed** · HRM **239 passed / 0 failed** · `ng build` green · `/app` deployed.
**Zero new failures.** All 7 Core failures are pre-existing with documented root causes (LEDGER → "FINAL TEST RESULTS"); this feature **fixed** 3 others (`ProductApiTest` barcode).
My Phase-A baseline sampled only 6 files and so under-counted the pre-existing failures — corrected in the LEDGER rather than quietly re-based.

## Honest remaining gaps (NOT done — do not assume otherwise)
- **3 dead permissions still exist**: `qms.settings.manage`, `cmms.settings.manage`, `webstore.settings.view` — grantable, enforced by nothing. Same disease cured elsewhere; recommend deleting.
- **3 stale tests** (`SettingsServiceTest` enum, `UserApiTest` create-with-role, `SessionApiTest` login-meta) are trivially fixable and would take Core to green. Left alone as out-of-scope — **offer to the owner**.
- **FE buttons calling BE routes that don't exist** (pre-existing 404s, not permission bugs): HR loans edit/delete/reject, EOS calculate/edit/delete, performance-reviews edit/delete/submit/approve, attendance CRUD, offers edit/delete, training-session delete; purchase order/bill **Duplicate**; inventory adjustment update/delete and opening-balance approve/update/delete. `features/hr/CLAUDE.md` documents several as if they exist — **that doc is wrong**.
- **Permissions with no FE control** (BE-enforced, no button): `hrm.attendance.approve` (corrections can be requested and never approved — a real functional gap), `hrm.leave-requests.cancel`, `hrm.loans.cancel`, `hrm.punch-types.*`, `inventory.reorder.notify`, `production.orders.start`, accounting `zakat.*` / `withholding-tax.*` / `fx-revaluation.*`, receipt/payment-voucher `approve`.
- **Dead catalog entries**: `production.quality.inspect` / `.disposition` — no BE middleware uses them, no FE control exists.
- **Coverage-lint** ships only 13 static dependency rules; it now honestly shows nothing rather than a false "healthy".
- **By-screen mode** is resource-shaped, not screen-shaped (no BE screen catalog outside LIS).
- **~40 LIS master-data create/edit buttons** remain coarse-gated (WP4g's honest deferral list).

## Corrections to my own earlier claims (kept visible on purpose)
- I said `lis.samples.send-external` "existed and was used nowhere". **False** — `LabExternalSampleController:27` gates 5 methods on it. The hole (`LabSampleController::sendOut` ungated) was real; my characterisation was not. I repeated a reviewer's claim without grepping.
- My Phase-A test baseline sampled only 6 files and under-counted pre-existing failures. Corrected, not re-based.
- **15 further pre-existing failures** exist on `hazemdev2` (LIS/Sales: `LabReportPdfParityTest`, `LabRequestApiTest` ×3, `LabResultApiTest` ×2, `LisCatalogReplaceTest` ×2, `CogsAtDeliveryCancelTest` ×6, `SalesReturnApiTest` ×1) — proven pre-existing by stashing the whole changeset and re-running. Not caused by this work.

## Key lesson (carry this forward)
"**Trust the BE middleware, never the button name**" prevented dozens of dead gates — the names lie constantly (`dispose`→`.update`, MPS `approve`→`mrp.run`, payroll `destroy`→`.create`).
**But the middleware itself can be wrong.** `payOut` — which records that cash left the company — sat under the *data-entry* permission, and dutifully aligning the FE to it **widened** a money hole. A money action under a data-entry permission is a **backend bug**, not a naming quirk. Read the middleware to learn *what the button calls*; then ask whether that gate is *right*.

## Next
Owner runs `/fullpush` (sync + deploy). No merge to main was performed by this work.
