Let the studio register the students who cannot register themselves
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / Tests (PHP 8.1) (pull_request) Successful in 1m3s
CI / Tests (PHP 8.5) (pull_request) Successful in 2m48s
CI / Tests (PHP 8.3) (pull_request) Successful in 3m24s
CI / Coding Standards & Static Analysis (pull_request) Successful in 8m21s
CI / Build Plugin Zip (pull_request) Skipped

The Book a lesson for a student panel built its picker from the us_student
role but vetted the submission with the book_lesson capability. ChildLoginGate
and RegistrationLoginGate withhold that capability from accounts that keep the
role, so the panel offered every guardian-managed child and every unapproved
signup and then refused them — with a message claiming no student had been
chosen, and a form cleared of all five fields.

Withholding book_lesson stops those accounts registering in their own name. It
was never meant to stop the studio acting for them, which is what the panel is
for, and for a child is the only route to a lesson besides their guardian.

Guard the student role instead, via a new RoleManager::isStudent() shared with
every picker and guard on the staff side so the two cannot drift apart again.
Group enrolment gets the same predicate: addDirect() and grantAccess() vetted
their posted ids not at all, and would enrol an instructor, an administrator,
or an account deleted since the page was drawn — raising a real payment against
them for a priced class.

Keep a refused booking's fields as submitted, reading the form through one
LessonController::submittedBooking() so what gets booked and what is shown
again cannot disagree about a field name. A booking that succeeds still leaves
an empty form, so the next one does not inherit it.

Closes #185

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XunBYk2sFEc1oL14sUiuBU
This commit is contained in:
2026-08-24 18:42:59 -03:00
co-authored by Claude Opus 5
parent 76530878b5
commit 5ce42f0003
12 changed files with 371 additions and 31 deletions
+15 -2
View File
@@ -96,11 +96,13 @@ lesson. The times offered are the open slots of the next eight weeks — every
instructor's on the studio **Scheduler**, only the instructor's own on **My
Lessons**, which `AdminBooking::book()` re-checks rather than trusting the
posted slot id. The result is reported as a notice above the panel saying what
was booked and what it left owing; a refusal reopens the panel with the reason.
was booked and what it left owing; a refusal reopens the panel with the reason
and every field as it was submitted, so only the mistake needs correcting. A
booking that succeeds clears the form, so the next one does not inherit it.
It is the same booking a student makes — `LessonBooker` claims the slot(s),
writes the lesson row(s), and raises the payment exactly as `POST /bookings`
does — and differs in three deliberate ways:
does — and differs in four deliberate ways:
1. **No intake answers or policy acceptances are recorded at booking time.**
Those are the student's to give; staff ticking the boxes for them would be an
@@ -113,6 +115,17 @@ does — and differs in three deliberate ways:
whole series) is `confirmed` at once. Without the tick a pending payment is
raised at the lesson type's price, per-occurrence for a weekly reservation,
and the lesson confirms when it settles like any other.
4. **It can book for a student who cannot book at all.** The guard is
`Auth\RoleManager::isStudent()` — the student *role*, not the `book_lesson`
capability — so it covers a guardian's child and a self-signup still awaiting
approval alike, and is shared with the group-class **Add students directly**
and **Make available** controls so the two paths cannot drift. Both hold the role;
both have `book_lesson` withheld (`Guardian\ChildLoginGate`,
`Auth\RegistrationLoginGate`) so that neither can book in their own name.
That restriction is on them, not on the studio acting for them — and for a
child, whose account is never signed in to, it is the only route to a lesson
besides their guardian's. The picker and the guard therefore accept exactly
the same set, so nothing offered in the panel can be refused as ineligible.
A weekly reservation needs a time that actually repeats: asked for one on a
one-off slot, the form refuses (`not_weekly`) rather than quietly booking a