fix(branch): the catalogue belongs to a branch too, and events belong to none
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:
Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
This diff is collapsed.
Please register or sign in to comment