fix(branch): split a programme two branches share, and give operators a way to check a client
Everything so far was validated against one client database. The tenants are not
all shaped like OC-Sport, every one of them runs `migrate --force` on its next
container start, and there is no staging in between — so the risk worth chasing
was never OC-Sport, it was the tenant I cannot see.
Reproduced by building one. On a database where a programme's groups run at two
branches, 2026_09_13_000002 declines to guess and falls through to the
main-branch fallback. The group at the other branch keeps pointing at a
programme that branch can no longer see: its name renders blank, and
PricingService cannot find a base price for it, so the enrolment cannot be
billed at all. `لا يوجد سعر محدد`, on a group that worked the day before. Silent,
and caused by the migration rather than found by it.
- 2026_09_13_000003 replicates instead of picking a winner. The programme keeps
its identity where it was pinned, every other branch using it gets a copy of
its own, and that branch's groups, enrolments, active prices and product
bundles are repointed at the copy. Nothing is deleted and nothing changes
branch. Two branches running "فريق 2018" now have two rows that can diverge,
which is the point — the same answer the product already gives for groups.
Verified on a constructed tenant carrying the fault, and a no-op on OC-Sport.
- `php artisan branch:audit` reports what is silent in the UI: strictly-scoped
rows with no branch (not a leak — a disappearance, present in SQL and on no
screen), children in a different branch from their parent, and programmes
with live enrolments and no active price. Exits non-zero so it can gate a
deploy check. On OC-Sport it finds one genuine pre-existing problem —
programme #32 has six active enrolments and no price at all — and no branch
integrity faults.
- BranchValidationRulesTest closes a gap in the suite itself: every other
branch test needs a restored Postgres tenant and skips without one, so on an
ordinary `php artisan test` none of them run. This one reads source, so it
runs everywhere — banning a raw `exists:` rule on a branch-owned table (they
compile to a raw query that accepts any id in the academy, and the property
feeding one is usually browser-settable), and failing when a model carries
branch_id in $fillable without declaring how it is scoped.
- Event now declares BRANCH_SCOPE_EXEMPT with its reasoning, so that being
academy-wide reads as a decision rather than as a model somebody forgot.
Standard suite: 231 tests, no failures. Against a restored tenant: 28 branch
tests, 23,093 assertions — and the same suite passes against the constructed
tenant that carried the split fault.
Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
Please register or sign in to comment