# Phase 3 — Canonical Permission Catalog

## Status

Phase 3 centralizes application permission identifiers, permission metadata, and default system-role grants into one authoritative code path.

This phase intentionally preserves the Phase 2 effective access contract. It does not rename permissions, remove legacy broad permissions, alter branch access, or add future permissions such as `dashboard.view`, `search.use`, or granular export capabilities.

## Canonical sources

Authorization definitions now live under `app/Support/Authorization/`:

- `PermissionName.php` — backed enum containing every shipped permission identifier.
- `PermissionCatalog.php` — display label, group/module, description, risk level, deprecation status, and replacement metadata.
- `DefaultRoleMatrix.php` — the ten default system roles and their exact permission grants.

Runtime permission records and default system-role grants are synchronized by `database/seeders/AuthorizationSeeder.php`.

## Permission catalog baseline

The Phase 3 catalog contains 89 existing permissions using the single `web` guard.

Risk metadata uses five values:

- `read`
- `operate`
- `approve`
- `financial`
- `security`

Risk is descriptive metadata in this phase. It does not create an additional authorization decision layer.

## Legacy permissions

The following four permissions remain part of the current access contract and are explicitly marked deprecated in the catalog:

- `customers.manage`
- `expenses.manage`
- `collections.manage`
- `payroll.manage`

Their reasons and replacement permission lists are now owned by `PermissionCatalog`. `config/authorization_inventory.php` derives the Phase 0 legacy registry from the catalog instead of duplicating it.

No legacy permission is removed or granted more widely in this phase.

## Default role matrix

The canonical matrix preserves the exact Phase 2 grants:

| Role | Permission count |
|---|---:|
| `super-administrator` | 0 |
| `branch-administrator` | 82 |
| `branch-manager` | 71 |
| `loan-officer` | 30 |
| `loan-inspector` | 20 |
| `cashier` | 19 |
| `accountant` | 40 |
| `debt-collector` | 18 |
| `hr-payroll-officer` | 8 |
| `auditor` | 19 |

`super-administrator` continues to receive full authorization through the Phase 2 `Gate::before` rule rather than through 89 attached permission rows.

The complete Phase 2 matrix is protected by a reviewed SHA-256 fingerprint in `AuthorizationCatalogTest`. An accidental grant or removal therefore fails CI even when the catalog and seeder are changed together.

## Seeder consolidation

`DatabaseSeeder` now calls only `AuthorizationSeeder` for permission and default-role definitions.

The former independent seeders:

- `RolePermissionSeeder`
- `PayrollPermissionSeeder`
- `CashClosingPermissionSeeder`
- `ManagementWorkPermissionSeeder`

remain temporarily as deprecated compatibility wrappers. They contain no authorization definitions and delegate to the canonical seeder plus the legacy comparison bridge.

`AuthorizationSeeder`:

1. clears the Spatie permission cache;
2. upserts catalog permissions while preserving numeric IDs;
3. creates/updates the ten default system roles;
4. synchronizes each default role to its canonical matrix;
5. leaves custom roles and user-role assignments intact;
6. clears the permission cache again.

It does not delete unknown/custom roles and does not write direct user permissions.

## Legacy bridge

Phase 2 remains unchanged:

- `model_has_roles` and `role_has_permissions` are runtime canonical tables;
- `role_user` and `permission_role` remain temporary rollback/equivalence mirrors;
- `SpatiePermissionBridgeSeeder` runs after `AdminUserSeeder` to refresh legacy mirrors from Spatie.

Branch isolation remains independent from Spatie and this catalog.

## Catalog integrity command

Run:

```bash
php artisan authorization:catalog-check
php artisan authorization:catalog-check --json
```

The command fails when:

- enum and catalog permission sets differ;
- duplicate/invalid catalog data exists;
- a deprecated permission references an unknown replacement;
- a default role references an unknown or duplicate permission;
- a catalog permission is missing after seeding;
- catalog permission metadata/guard differs from the seeded record;
- a default system role is missing or has different grants.

Unknown database permissions are reported as warnings rather than deleted automatically, avoiding destructive behavior during migration.

## CI guarantees

Foundation CI runs the catalog check on:

- SQLite after fresh migration/seeding;
- MySQL after fresh migration/seeding;
- the production-mode release verification database.

Feature tests additionally verify:

1. the enum, catalog and fresh seeded database contain exactly 89 permissions;
2. every `permission:`-protected route references a catalog permission;
3. the Phase 2 default-role matrix fingerprint is unchanged;
4. the legacy permission registry is derived consistently from the catalog;
5. repeated canonical seeding is idempotent;
6. custom roles and user assignments survive repeated seeding;
7. `DatabaseSeeder` has only one authoritative authorization seeder.

## Deployment safety

This phase has no schema migration and no destructive data cleanup.

On an existing database, running the normal deployment seeder path preserves permission/role numeric IDs because records are matched by stable names. Default system-role grants are synchronized to the same Phase 2 matrix, while custom roles and user assignments remain untouched.

## Not part of Phase 3

Later phases still handle:

- new/granular permission names and deliberate replacement of legacy broad capabilities;
- security administration privilege ceilings and custom-role CRUD controls;
- module-by-module route conversion to Laravel `can:`/policies;
- `dashboard.view`, `search.use`, export/download permission splits, and other cross-cutting permissions;
- removal of the temporary `User::hasPermission()` compatibility wrapper;
- eventual removal of legacy comparison pivots after production validation.
