• Mahmoud Aglan's avatar
    feat(settlements): let the desk put a player back in a programme · 2b6e6997
    Mahmoud Aglan authored
    The console could tell an operator that a player had no enrolment and nothing
    in the system was billing him. It could not do anything about it: all ten
    settlement actions move money on an account that already exists, so the one
    account that had stopped existing was the one the wizard could not fix. The
    operator read the diagnosis and had no button.
    
    `restore_enrollment` is that button. It delegates to EnrollmentService rather
    than inserting a row, so a restored enrolment passes the same checks a new one
    does — capacity, group status, one-group-per-programme, the branch rule — and
    lands identical to every other row in the table. A repair that produces a
    slightly different shape of row is a second bug waiting.
    
    It raises no invoice. `next_billing_date` comes out as the 1st of next month,
    so the cycle resumes on its own and the current month stays a deliberate act:
    the operator adds `bill_month` for it if it is owed, with the amount in front
    of them. Restoring a subscription and charging for it are two decisions and the
    second is not ours to assume.
    
    The group is suggested, not chosen. The enrolment that knew the programme is
    gone; the only surviving record is the name on the player's last subscription
    invoice, and that programme was deleted and recreated under a new one — so it
    cannot be looked up, only matched. Matching is on the years in the name,
    because these programmes are birth-year cohorts: «اكاديمية 2017 -2018» and
    «أكاديمية (2017-2018)» share nothing as strings and are obviously one cohort to
    a human. Cohorts written in two digits match nothing and a retired squad has no
    successor, which is exactly where the person at the desk knows and this screen
    does not — so the suggestion is labelled as a guess and the full list is always
    there.
    
    Two bugs found while proving it works, both real rather than test-only:
    
    - EnrollmentService left `academy_id` to BelongsToAcademy, which fills it from
      the `current_academy` container binding — bound only inside a web request. An
      enrolment created from a command, a queued job or a service died on a NOT
      NULL violation. It is now stated from the participant, which is true under
      every caller and which the trait leaves alone.
    - The group list sorted with `sortBy([fn, fn])`. sortBy reads an array as
      [column, direction] pairs, so it sorted by neither and buried the suggestion
      mid-list, where an operator in a hurry never sees it.
    
    Verified end to end against the restored tenant: stage the restore, apply it,
    and the player is enrolled in the suggested group with next month's billing
    date and not one new invoice. 498 tests, 389 pass / 109 skip on Postgres and
    313 / 185 on SQLite.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    2b6e6997
Name
Last commit
Last update
..
Console/Commands Loading commit data...
Domain Loading commit data...
Helpers Loading commit data...
Http Loading commit data...
Livewire Loading commit data...
Models Loading commit data...
Providers Loading commit data...