# Branch Isolation Security Boundary

Branch isolation is a system-level authorization boundary. It is not optional controller filtering.

## Canonical access rules

- `BranchAccessService` is the single source of truth for user branch access.
- Non-super-administrators may operate only in their assigned branches. A legacy `primary_branch_id` remains accessible even if its pivot row is missing.
- Super-administrators are system-wide.
- `User::accessibleBranchIds()` and `User::canAccessBranch()` are compatibility wrappers only and must delegate to `BranchAccessService`.
- Branch filters may narrow access but must never expand it.
- Parent-owned child records inherit the parent branch security boundary.
- Reports, dashboards, searches, exports, and aggregates must establish branch scope before aggregation or export.

## Background and non-HTTP execution

Normal branch-owned model queries fail closed when there is no authenticated user and no explicit `BranchContext`.

Trusted background work must opt in explicitly using one of:

- `BranchContext::forUser()`
- `BranchContext::forBranch()` / `forBranches()`
- `BranchContext::systemWide()` only for reviewed system-wide operations

A missing actor is never an implicit system-wide bypass.

## Approved `BranchScope` bypasses

Direct `withoutGlobalScope(BranchScope::class)` lookups are exceptional. They are allowed only when the code immediately performs explicit branch authorization and the bypass is needed to preserve deliberate authorization semantics such as returning 403 rather than hiding a submitted foreign key as 404.

The reviewed controller locations are:

- `app/Http/Controllers/ExpenseController.php` — submitted paid-from account is explicitly branch-authorized and must satisfy `SameBranchAs` with the selected expense branch.
- `app/Http/Controllers/FinancialTransferController.php` — both source and destination accounts are explicitly checked against the actor's accessible branches before the transfer service runs.
- `app/Http/Controllers/LoanDisbursementController.php` — submitted disbursement and optional processing-fee accounts are explicitly branch-authorized and must satisfy `SameBranchAs` with the loan branch.
- `app/Http/Controllers/LoanPaymentController.php` — the submitted receiving account is pre-authorized and must satisfy `SameBranchAs` with the loan branch.

The bypass inventory test must detect both imported `BranchScope::class` references and fully-qualified class references. Any new bypass must be reviewed, documented here, and covered by a branch-isolation regression before merge.

## Staff assignment

System-wide access does not imply employment in every branch. Branch staff/custodian/receiver validation must use actual assignment (`assignedBranchIds` / `isAssignedToBranch`) rather than general accessible scope.

## Cleanup rule

Legacy controller checks may be removed only after the same boundary is enforced by the canonical service, model scope/context, request validation, or parent-derived scope and regression tests remain green. Middleware remains defense in depth.
