# 03 — Permissions, Roles & Branch Scoping

## Confirmed enforcement mechanism

Real access control in this ERP is **100% Spatie `laravel-permission`** (guard `admin`): routes
in `routes/admin.php` use `->middleware('permission:module.resource.action')`, and
`App\Models\Admin` uses the `HasRoles` trait with `protected $guard_name = 'admin'`. The
`Admin::role()` relation / `App\Models\Roles` / `App\Models\Permissions` (`group_name` column on
`admins`) layer is **vestigial/display-only** — it's written on user create/update but nothing
reads it for authorization. **Do not build on top of `group_name`/`Roles`/`Permissions`; use
Spatie exclusively**, exactly like every other module.

## New permission keys — add to `database/seeders/RolePermissionSeeder.php`

`RolePermissionSeeder.php` uses a **flat array of dot-string keys** (e.g. `'core.branches.view'`),
all created with `Permission::findOrCreate($name, 'admin')`. Add this block:

```
clinic.clinics.view
clinic.clinics.create
clinic.clinics.edit
clinic.clinics.delete
clinic.specialties.view
clinic.specialties.create
clinic.specialties.edit
clinic.specialties.delete
clinic.doctors.view
clinic.doctors.create
clinic.doctors.edit
clinic.doctors.delete
clinic.schedules.view
clinic.schedules.edit
clinic.bookings.view
clinic.bookings.create
clinic.bookings.confirm
clinic.bookings.cancel
clinic.patients.view
clinic.medical_records.view          (Doctor guard only — see note below)
clinic.reports.view
clinic.reviews.view
clinic.offers.view
clinic.offers.create
clinic.offers.edit
clinic.offers.delete
clinic.policies.view
clinic.policies.edit
clinic.staff.view                    (Clinic Admin managing its own Receptionist accounts)
clinic.staff.create
```

Also add matching Arabic display titles to the `arabicPermissionSegments()` array in the same
seeder (existing pattern, e.g. `core.branches` → `'الفروع'`), so the admin UI's permission
picker shows Arabic labels for these too.

> **Note on `clinic.medical_records.view`**: this permission exists for completeness but per the
> brief, precise medical detail (diagnosis/prescription/history) must be restricted to the
> Doctor and the Patient only — Clinic Admin/Receptionist must **not** be grantable this even by
> mistake. Enforce this in code (a `MedicalRecordPolicy`/explicit guard check), not only via the
> permission system, since Super Admin technically has all permissions by default in most Spatie
> setups (`Role::findOrCreate('Super Admin', ...)` typically gets every permission — confirm
> against the existing seeder's Super Admin block and consider explicitly excluding this one, or
> gating the *view* action behind a hard-coded "unless explicitly authorized" check per the
> brief's "Super Admin cannot see medical files without special access" rule).

## New roles — add to the same seeder

```php
Role::findOrCreate('Clinic Admin', $guardName)->givePermissionTo([
    'clinic.clinics.view', 'clinic.clinics.edit',                 // own clinic only, enforced by branch scope below
    'clinic.specialties.view',
    'clinic.doctors.view', 'clinic.doctors.create', 'clinic.doctors.edit', 'clinic.doctors.delete',
    'clinic.schedules.view', 'clinic.schedules.edit',
    'clinic.bookings.view', 'clinic.bookings.create', 'clinic.bookings.confirm', 'clinic.bookings.cancel',
    'clinic.patients.view',
    'clinic.reports.view',
    'clinic.reviews.view',
    'clinic.offers.view', 'clinic.offers.create', 'clinic.offers.edit', 'clinic.offers.delete',
    'clinic.staff.view', 'clinic.staff.create',
]);

Role::findOrCreate('Receptionist', $guardName)->givePermissionTo([
    'clinic.bookings.view', 'clinic.bookings.create', 'clinic.bookings.confirm', 'clinic.bookings.cancel',
    'clinic.patients.view',
]);
```

`Super Admin`'s existing role gets the full `clinic.*` set added to its `givePermissionTo()` call
(same pattern already used for every other module).

## Branch scoping — new design (nothing like this exists yet)

Grepping the whole codebase found **no branch-scoping trait, no global scope, and no
`branch_id` on `admins`** — this has to be built from scratch for the Clinic module.

### 1. Schema

Add nullable `branch_id` to `admins` (see `02-database-schema.md`, "Altered"). `NULL` = sees
everything (Super Admin). A value = restricted to that one branch (Clinic Admin, Receptionist).

### 2. Enforcement — a new trait, applied per-model, not a blanket global scope

Recommendation: a `BelongsToClinicBranch` trait + a `ClinicBranchScope` global scope, added
**only** to the models that are branch-owned (`Doctor` via the pivot — actually scope at the
`DoctorClinic`/`Booking` level since a Doctor itself can belong to multiple branches — `Booking`,
`clinic_doctor_branch`, `Offer` when branch-local). The scope reads
`auth('admin')->user()?->branch_id` and, if not null, adds `where('branch_id', $branchId)` (or
the equivalent join condition for `Doctor`, since a doctor's branch relationship is via the pivot,
not a direct column). If `branch_id` is null (Super Admin), no constraint is added.

```php
// app/Models/Clinic/Concerns/BelongsToClinicBranch.php  (new)
trait BelongsToClinicBranch
{
    protected static function bootBelongsToClinicBranch(): void
    {
        static::addGlobalScope(new \App\Models\Clinic\Scopes\ClinicBranchScope);
    }
}
```

Apply to: `Booking`, `Offer` (only when `branch_id` is set — global offers stay visible to
everyone), and a dedicated query scope (not a global scope, since it's a pivot/derived
relationship) on `Doctor::visibleTo($admin)` for the doctor list screen.

### 3. Write-side enforcement

Every `store()`/`update()` in `Admin\Clinic\*` controllers must also **assign** `branch_id` from
the acting Clinic Admin/Receptionist's own `branch_id` (never trust a hidden form field for it),
and every `edit()`/`show()` must 403 if the resource's `branch_id` doesn't match the acting
admin's `branch_id` (unless they're Super Admin, i.e. `branch_id` is null). This is standard
Laravel authorization — implement as a small Policy class per model
(`BookingPolicy::view/update`) rather than repeating the check inline in every controller method.

### 4. What Clinic Admin/Receptionist must never see, regardless of scope

Per the brief: precise medical detail (diagnosis, prescription, medical/surgical/drug history,
allergies) is for **Doctor and Patient only** — not Clinic Admin, not Receptionist, not (by
default) Super Admin. This is a *field-level* restriction, not a branch-scoping one — solve it by
never exposing those fields/relations in the `Admin\Clinic\*` controllers, services, or views at
all (don't `select()` them, don't `with()` the relation), rather than relying on the view layer to
hide them.
