• Mahmoud Aglan's avatar
    fix(branch): split a programme two branches share, and give operators a way to check a client · e22cd9e4
    Mahmoud Aglan authored
    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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    e22cd9e4
BranchValidationRulesTest.php 6.77 KB