• Mahmoud Aglan's avatar
    fix(branch): the catalogue belongs to a branch too, and events belong to none · d3cbd5f1
    Mahmoud Aglan authored
    The first pass put programmes, products, prices, promotions, warehouses and
    receipt templates in the *shared* bucket, where `branch_id IS NULL` means "every
    branch uses this row". The reasoning was that a missing base price is a hard
    failure that stops a sale, so hard-filtering the catalogue risked leaving a
    branch unable to sell anything.
    
    That bought safety with the wrong currency. A branch is meant to read as its own
    installation — its own programmes at its own prices, its own products, its own
    stores — and a programme offered at one branch turning up in another branch's
    dropdown is the same bug as a group doing it. The shared bucket just hid it
    behind a plausible-sounding rule.
    
    The data said the caution was unnecessary. Across the live tenant no group
    points at a programme in another branch, no base price prices a programme in
    another branch, and exactly three catalogue rows had no branch at all. The
    catalogue was already per-branch in practice and merely unlabelled.
    
      - Programmes, base prices, pricing rules, promotions, products, product
        categories, kits, warehouses, receipt templates and wallets move to the
        strict bucket. `kits` and `product_categories` gain the column; the rest
        only needed their nulls resolved, from actual usage where a link existed
        and from the main branch otherwise.
    
      - Events go the other way and lose the trait entirely, with their
        registrations. An event is an academy-wide occasion and is genuinely not
        per branch. The column stays — dropping it would be destructive — but
        nothing reads it.
    
      - Only people and the academy calendar stay shared: employees, trainers,
        guardians, holidays. A coach who works two pitches needs one record visible
        from both, not two that drift. Even there the nulls are narrowed — anyone
        who demonstrably belongs to one branch is pinned to it, which in this tenant
        leaves none shared at all.
    
      - Pricing rules are the one model whose branch is genuinely many-valued: the
        wizard targets a list through `pricing_rule_branches` and deliberately
        leaves the legacy column null. A column scope would have hidden every rule
        it has ever created, so PricingRule supplies its own scope reading the pivot
        — and BelongsToBranch now lets a model do that.
    
      - Forms that let a user save a catalogue row with no branch now require one,
        and the "كل الفروع" option is gone from those pickers: on a strictly scoped
        table that choice does not mean every branch, it means none. The setup
        wizard's seeded prices are filed against their programme's branch rather
        than null, so a new academy does not finish setup unable to sell.
    
      - Comments throughout said "SHARED — branch_id NULL means every branch uses
        this row". They now say what is true.
    
    tests/Feature/PricingSurvivesBranchScopeTest.php is the guard on the original
    worry: it prices every live enrolment inside its own branch on each run. It
    passes, and reports the one programme that has no active base price at all — a
    pre-existing gap, unrelated to scoping. Branch suites: 14 tests / 23,060
    assertions against a restored tenant. Standard suite: 224 tests, no failures.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    d3cbd5f1