• DevPilot's avatar
    fix(enrollments): let a player be moved back into a group they have left · 5e1d9b4b
    DevPilot authored
    The transfer wizard died on the return leg of every shuffle. transfer()
    always INSERTs the replacement enrolment, but nothing ever deletes the row
    a player leaves — it stays, cancelled — and `enrollments` is unique on
    (participant_id, training_group_id). So moving a child from 2015 A to
    2015 B worked, and moving them back a month later raised a raw
    UniqueConstraintViolationException that the wizard's catch-all reported to
    the desk as "خطأ غير متوقع", with nothing to say what had gone wrong.
    
    On the OC-Sport tenant 94 active players already have a former group that
    is still open and still listed as a destination, so this was one click away
    on any of them.
    
    Revive the existing row rather than loosening the constraint: one player is
    in one group once, which is what the unique key says and what the history
    should read like. The lookup drops global scopes deliberately — the
    constraint is academy-wide and knows nothing about the branch scope, so a
    row this request could not see would still collide — and clears the
    withdrawal that ended the previous spell, or the revived enrolment would
    read as active and withdrawn at once and the renewal command would skip it.
    
    Two neighbours fixed while here, both latent rather than reported:
    
    - transfer() had no "one group per programme" guard, though enroll() has
      one. A transfer into a programme the player was already enrolled in left
      two active enrolments, which GenerateRenewalInvoices bills twice.
    - transfer() left academy_id to BelongsToAcademy, which fills it from a
      container binding that only exists in a web request. Same NOT NULL
      failure enroll() was already fixed for; states it explicitly now.
    
    Transferring into the group the player is already in is now refused
    instead of cancelling and reviving the same row and reporting success.
    
    Verified against a restored oc_sport tenant (the suite's own convention):
    the new test reproduces the violation without the fix and passes with it,
    and the full suite shows the same 6 pre-existing failures before and after
    — no regressions.
    Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
    5e1d9b4b
EnrollmentTransferReturnsToFormerGroupTest.php 7.29 KB