# Phase 2 — Spatie Model & Gate Integration

## Status

Phase 2 changes the runtime authorization engine from the legacy custom role/permission relations to Spatie Laravel Permission while retaining the legacy pivots as temporary comparison mirrors.

Branch isolation remains independent and unchanged. Spatie Teams and wildcard permissions remain disabled.

## Runtime source of truth

After this phase:

- `model_has_roles` is the canonical user-role assignment table.
- `role_has_permissions` is the canonical role-permission assignment table.
- `model_has_permissions` must remain empty for normal system operation.
- `role_user` and `permission_role` are compatibility mirrors only.
- `User` uses `Spatie\Permission\Traits\HasRoles`.
- `App\Models\Role` extends Spatie's Role model while preserving `display_name`, `description`, and `is_system`.
- `App\Models\Permission` extends Spatie's Permission model while preserving `display_name`, `group`, and `description`.
- Laravel `can()`, `cannot()`, policies and `@can` are the canonical permission-check API.
- The temporary `User::hasPermission()` wrapper delegates to Laravel `can()` so old call sites can be migrated incrementally.

## Super Administrator

`AppServiceProvider` registers a global `Gate::before` rule for the `super-administrator` role. It returns `true` only for that role and `null` otherwise so normal Gate/policy evaluation continues for every other user.

The super-administrator role therefore does not need every permission row attached to it.

## Cutover safety

`2026_08_14_140000_refresh_spatie_bridge_for_runtime_cutover.php` refreshes the Spatie pivots from the latest legacy assignments during deployment. This closes the window where Phase 1 may have been running while administrators continued changing legacy role assignments.

The migration is data-only and intentionally has a no-op `down()` method. Legacy pivots remain present, so rolling application code back does not require destructive data reversal.

## Assignment writes

Runtime role/permission writes go through `AuthorizationAssignmentService`:

- `syncUserRoleIds()` uses Spatie `syncRoles()` and mirrors the affected user to `role_user`.
- `assignUserRole()` uses Spatie `assignRole()` and mirrors the affected user.
- `syncRolePermissionIds()` uses Spatie `syncPermissions()` and mirrors the affected role to `permission_role`.
- `giveRolePermissionNames()` uses Spatie `givePermissionTo()` and mirrors the affected role.

Spatie write + legacy mirror happen inside one database transaction.

The legacy mirrors exist only to support equivalence verification and rollback confidence. New application authorization code must not read them directly.

## Seeder behavior

Role and permission seeders now use Spatie APIs. At the end of `DatabaseSeeder`, `SpatiePermissionBridgeSeeder` mirrors canonical Spatie assignments back into the legacy comparison pivots and requires exact parity.

## Bridge command

Verification only:

```bash
php artisan authorization:spatie-bridge
php artisan authorization:spatie-bridge --json
```

Canonical Spatie -> legacy comparison mirror repair:

```bash
php artisan authorization:spatie-bridge --sync-to-legacy --json
```

Emergency/recovery legacy -> Spatie repair retained from Phase 1:

```bash
php artisan authorization:spatie-bridge --sync --json
```

Do not use legacy -> Spatie as a normal Phase 2 workflow. Spatie is now the runtime source of truth.

## Equivalence guarantees

Automated tests verify:

1. Spatie Gate registration is enabled.
2. `User` uses `HasRoles` and no longer declares a conflicting `roles()` method.
3. custom Role and Permission models extend Spatie models.
4. all seeded role/permission and user/role pivots are in exact legacy/Spatie parity.
5. every seeded permission produces the same allow/deny result for every system role as the frozen legacy permission matrix.
6. super-administrator receives the global Gate bypass, including for abilities without a permission row.
7. the temporary `hasPermission()` wrapper returns the same result as `can()`.
8. runtime assignment writes update Spatie first and preserve legacy mirror parity.
9. both bridge repair directions are tested.
10. existing branch-isolation and application regression tests remain green.

## Rollback

If Phase 2 must be rolled back before later RBAC phases:

1. run `php artisan authorization:spatie-bridge --sync-to-legacy --json` and require `status=pass`;
2. deploy the last Phase 1 application revision;
3. keep the Spatie tables and `guard_name` columns in place;
4. do not drop or rewrite role/permission IDs;
5. verify the Phase 1 authorization inventory and branch-isolation suite.

This rollback works because every Phase 2 runtime assignment is mirrored transactionally into the Phase 1 legacy pivots.

## Not part of Phase 2

The following remain for later phases:

- replacing the custom `permission:` middleware alias with Laravel `can:`/Spatie middleware everywhere;
- centralizing all permission definitions into `PermissionCatalog` and `DefaultRoleMatrix`;
- granular role-management privilege boundaries and custom-role UX;
- replacing broad legacy permissions;
- final removal of `hasPermission()` compatibility calls;
- dropping `role_user` and `permission_role` after full production validation.
