Add group-class withdrawal deadline and kind-aware offering form
CI / Tests (PHP 8.1) (pull_request) Successful in 44s
CI / Tests (PHP 8.2) (pull_request) Successful in 59s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m53s
CI / PHPStan (pull_request) Successful in 2m55s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped

Group classes now carry an optional per-class withdrawal deadline. Up to
that day a student may withdraw themselves from the class; the withdrawal
frees the seat and voids any pending payment but never issues an account
credit. After the deadline self-withdrawal closes and a studio admin must
withdraw the student by hand (the admin path is never subject to the
deadline). A blank deadline keeps self-withdrawal open indefinitely.

Also make the Add/Edit Offering form show only the fields relevant to the
selected kind: group settings for group classes, weekly reservation for
private lessons. Progressive enhancement — without JS every field renders.

- New nullable us_offerings.withdrawal_deadline column; Offering model gains
  $withdrawalDeadline + isWithdrawalOpen().
- New student endpoint POST /enrollments/{id}/withdraw, gated by the deadline
  (403 withdrawal_closed), ownership-checked, idempotent.
- Front-end group-class page shows a Withdraw button while open.
- No USC_VERSION bump: 1.2.0 is unreleased and accumulates schema changes
  under its section, matching the scheduled-billing and credit features.

Tests: composer test (596), composer lint, composer cs all pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
2026-07-24 15:56:40 -03:00
co-authored by Claude Opus 4.8
parent f552c3952a
commit 2c4b481077
14 changed files with 347 additions and 21 deletions
+15
View File
@@ -22,6 +22,7 @@ An offering is anything a student can register for: a private-lesson type (30 or
| `term_end` | DATE | Group / term offerings — last day; NULL otherwise |
| `class_time` | TIME | Group only — time of day each session starts; NULL otherwise |
| `enrollment_deadline` | DATE | Group only — last day students may enrol; NULL defaults to `term_start` (the first class day) |
| `withdrawal_deadline` | DATE | Group only — last day a student may withdraw themselves; NULL keeps self-withdrawal open indefinitely |
| `schedule_note` | VARCHAR(191) | Group only — human-readable schedule, e.g. "Tuesdays 4:00pm"|
| `cancellation_cutoff_hours` | SMALLINT UNSIGNED | Optional per-offering cancellation cutoff in hours; NULL inherits the studio default (see `cancellation-cutoff.md`) |
| `access_mode` | VARCHAR(20) | `public` (listed in the catalog) or `invite_only` (group classes hidden from the catalog — see `group-classes.md`) |
@@ -68,6 +69,20 @@ against that effective deadline (inclusive — the deadline day is still open).
enrolment endpoint enforces it (`403 enrollment_closed`) and the front-end
group-class list mirrors the same rule; see `group-classes.md`.
## Withdrawal deadline
A group class also carries an optional `withdrawal_deadline` — the last day a
student may withdraw *themselves* from the class. Unlike the enrolment deadline it
has **no implicit default**: `Offering::isWithdrawalOpen($today)` treats an unset
(NULL) deadline as always open, so a class only closes to self-withdrawal once the
instructor sets a date and it passes (comparison is inclusive — the deadline day is
still open). A withdrawal made while open frees the seat and voids any still-pending
payment but **never issues an account credit** (credits are reserved for cancelled
lessons; see `credits.md`). Once the deadline passes the student must contact the
studio, and an admin withdraws them by hand from the student detail page — the admin
path is never subject to the deadline. The student endpoint enforces it
(`403 withdrawal_closed`) and the front-end group-class list mirrors the rule; see
`group-classes.md`.
## Instructor assignment
Every offering has an owning `instructor_id`. A studio admin
(`manage_instructors`) sees an **Instructor** picker on the offering form and may