• Mahmoud Aglan's avatar
    feat(branch): make branch an isolation boundary instead of a filter each screen remembers · d714d239
    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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    d714d239
Name
Last commit
Last update
..
academy-settings.blade.php Loading commit data...
branding-settings.blade.php Loading commit data...
receipt-settings.blade.php Loading commit data...
system-settings-form.blade.php Loading commit data...