-
Mahmoud Aglan authored
Branch was built as a reporting lens. Nine tables carried `branch_id`, the trait that stamped it deliberately added no scope, and each of 212 Livewire components was individually responsible for remembering to filter. Most remembered for their main list query. Almost none remembered for the <select> rendered beside it — so picking a branch narrowed the table and left every dropdown, typeahead, count badge and print template still offering the other branches' groups, players, facilities and trainers. On OC-Sport, where only ZSC is live and six other branches sit half-configured, that surfaced as stray rows turning up in pickers all over the app. An audit of the whole surface found 434 such leaks across 165 files. Per-screen patching would not have held: the next component added would forget again. So branch now works the way academy already does — a global scope on the model, which no screen can route around. - BranchScopeState owns the on/off switch, BranchScope the filter itself. ResolveBranchContext activates enforcement; it stays off for console and queues, guests, member-portal accounts and the routes in config/branch_scope.php. A cron that silently billed one branch, or a parent hidden from their own child, would each be worse than the leak being fixed. - Models fall into four buckets. STRICT (operational records — players, groups, facilities, invoices, payments, attendance) filter `branch_id = :active`. SHARED (catalogue and configuration — products, base prices, pricing rules, programmes, trainers) filter `branch_id = :active OR branch_id IS NULL`, because null there means "every branch uses this row" and hard-filtering it would leave other branches with no active base price — a hard failure that stops a sale, not a tightening. CHILD line items scope through their parent relation rather than growing a denormalised branch_id that drifts. The rest are academy-level and get no trait. - The migration adds branch_id to 22 more tables and backfills every existing row parent-first (a session from its group, an attendance record from the session), falling back to the academy's main branch. Every step is guarded and additive, and it is safe to re-run: verified by rollback and re-migrate against a populated tenant copy. - The residue a scope cannot catch is fixed by hand: `exists:` validation rules run raw SQL and accepted any id in the academy, browser-settable `#[Url] public $branch_id` properties were an authorisation bypass rather than a filter, raw DB::table aggregates bypassed Eloquent entirely, and several figures attributed a row through the wrong table (an invoice's branch read from its payments'). Screens that are cross-branch by design — transfers, executive roll-ups, branch administration — now opt out explicitly and narrowly. Verified against a restored OC-Sport copy: BranchIsolationTest and BranchScopedScreensTest, 13 tests / 22,897 assertions, comparing what Eloquent returns under each branch against what raw SQL says is in it, rendering every staff screen, and proving a branch id in the URL cannot override the selected one and a detail route cannot open another branch's record. Standard suite: 223 tests, no failures. docs/agent-rules/19-branch-isolation.md records the rule and how to add a new branch-owned table. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
d714d239
| Name |
Last commit
|
Last update |
|---|---|---|
| .. | ||
| academy-settings.blade.php | ||
| branding-settings.blade.php | ||
| receipt-settings.blade.php | ||
| system-settings-form.blade.php |