# WP4 — Action-level enforcement hardening (FE) — METHOD (applies to WP4a…WP4g)

One module per sub-WP, in owner order: **WP4a Accounting → WP4b Sales → WP4c Purchases → WP4d Inventory → WP4e Production → WP4f HR → WP4g rest (LIS/core/pos/clinic)**. Each dispatch names ONE module.

## Goal (per module)
Close the "granted but the button still shows" gap: where a resource-level gate (`sales.invoices`) leaves action buttons (delete/approve/post/convert/…) ungated, add the **full action-string** `*appCan` to each action control so removing the action permission actually hides the button. **Priority within a module: danger/financial actions FIRST** — delete, approve, post, convert, cancel, void — then the rest. This is incremental: **log what remains coarse; never claim full coverage you didn't do.**

## ⚠️ CRITICAL METHOD FINDING (from WP4a — do NOT skip)
**The permission a button needs is declared in the BE CONTROLLER MIDDLEWARE, not derivable from the action's name.** Deriving gates from the catalog/naming convention alone produces DEAD GATES (a gate on a permission that doesn't exist hides the button from everyone — the `hrm.training` bug class). Real examples found in Accounting:

| Button | Naive guess (WRONG) | Actual BE requirement |
|---|---|---|
| Fixed asset dispose/sell | `.dispose` (doesn't exist) | `accounting.fixed-assets.update` |
| Budget approve | `.approve` (doesn't exist) | `accounting.budgets.update` |
| Bank-rec auto-match/complete/approve | `.approve` (doesn't exist) | `accounting.bank-reconciliations.create` |
| Allocation-rule / recurring-entry execute | `.execute` (doesn't exist) | `<resource>.create` |
| Year-end reopen | `.reopen` (doesn't exist) | `accounting.year-end-closing.execute` |
| Fiscal-year delete | `.delete` (doesn't exist) | `accounting.fiscal-years.create` |

**Therefore: read `Modules/<Module>/app/Http/Controllers/*` `middleware()` for the method the button calls, and use THAT permission string.** Then verify every gate string exists in the seeder catalog (WP4a verified with a `comm` diff of extracted gates vs extracted catalog → empty = zero dead gates). Also watch for gates that are PRESENT BUT WRONG (button shows, user gets 403) — WP4a found 2.

## Method
1. **List the module's action permissions** from `RolePermissionSeeder::permissions()` (BE) — the `module.resource.action` keys — but treat the **controller middleware as the source of truth** for what each button requires (see above).
2. **Find the action controls** in the module's feature components (`src/app/features/<module>/**/*.html` + `.ts`) — buttons/menuitems that call the action (delete/approve/post/convert/cancel/confirm/etc.).
3. **Gate each** with `*appCan="'<module>.<resource>.<action>'"` (structural directive, prefix-aware via `hasAnyPermission`). Convert buttons: the TARGET document's `.create` (per `convert-buttons-permission-analysis.html` — e.g. quotation→order blue arrow gets `*appCan="'sales.orders.create'"`).
4. **Danger/financial first**, then remaining actions. Where a control is shared across docs, gate in the shared component (one fix, not per-screen — house rule).
5. **Verify** the exact permission string exists in the catalog (no typos → dead gate, the `hrm.training` class of bug).
6. **Log** any action left coarse (e.g. too entangled to gate safely this pass) into the LEDGER Deferrals + a short note in `RESUME.md` — so coverage is honest.

## Reference
- Convert-button specifics + the 6 known ungated convert/create buttons: `knowledge-base/plans/convert-buttons-permission-analysis.html`.
- `*appCan` directive: `src/app/shared/directives/can.directive.ts`; matcher: `src/app/core/services/permission.service.ts:18-22` (dot-boundary — the full action string must exist as a real permission).
- Coarse-gate counts per module (from analysis): accounting 87, sales 10, purchases 14, inventory 18, production 23, hrm 37, lis 133, core 39, pos 8.

## Acceptance criteria (per module)
- [ ] Danger/financial action buttons (delete/approve/post/convert/cancel/void) in the module carry a full-action `*appCan`.
- [ ] Each gate string is a real catalog permission (no dead gates).
- [ ] Shared controls gated in the shared layer, not per-screen.
- [ ] Remaining coarse actions LOGGED in Deferrals (honest coverage).
- [ ] `ng build` green.

## Tests
- FE `ng build` green. Manual: with a role missing e.g. `<module>.<resource>.delete`, the delete button is hidden (owner can spot-check on `/app`).

## Flags
- **[FIN]** — Accounting/Sales/Purchases modules touch money → Fable/advisor consult on those (WP4a/b/c). Others [FIN]=no.
- **Migration:** none.

## Out of scope
- Do NOT change BE middleware (already enforces exact perms — the FE gate is what's missing). Do NOT change the catalog (WP1) or the roles screen (WP3). Do NOT gate non-action controls (navigation within a screen). One module per dispatch — do not bleed into another module's files.
