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
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:
@@ -62,11 +62,34 @@ deadline (and capacity) so a **late enrolment** can be added after the class has
|
||||
closed. Past the deadline the details page labels these as late enrolments. See
|
||||
**Admin Interface** below.
|
||||
|
||||
## Withdrawal Flow
|
||||
A student may withdraw themselves from a class they are enrolled in through the same
|
||||
group-class page: an active enrolment shows a **Withdraw** button.
|
||||
`POST /enrollments/{id}/withdraw` marks the enrolment `cancelled` (freeing its
|
||||
capacity seat) and voids any still-pending payment. It **never issues an account
|
||||
credit** — a timely withdrawal is a clean exit, not a refund (credits are reserved
|
||||
for cancelled lessons; see `credits.md`).
|
||||
|
||||
Self-withdrawal is bounded by the class's **withdrawal deadline** (the instructor's
|
||||
`withdrawal_deadline`; see `offerings.md`). Unlike the enrolment deadline it has no
|
||||
implicit default — a class with no deadline set stays open to withdrawal for its
|
||||
whole life. Past the deadline `POST /enrollments/{id}/withdraw` rejects the request
|
||||
with `403 withdrawal_closed`, and the class card shows "Withdrawal has closed —
|
||||
contact the studio to withdraw." in place of the Withdraw button. The endpoint also
|
||||
returns `404 not_found` for an unknown enrolment and `403 forbidden` when the
|
||||
enrolment is not the caller's own; a withdrawal of an already-cancelled enrolment is
|
||||
idempotent.
|
||||
|
||||
The deadline only bounds student **self**-withdrawal. A studio admin can withdraw a
|
||||
student at any time from the **student detail page** (`Auth\StudentActions::withdrawEnrollment`),
|
||||
which is never subject to the deadline.
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|----------------------------------------------|----------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/enrollments` | Any logged-in user |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/enrollments` | `book_lesson` |
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|-------------------------------------------------|----------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/enrollments` | Any logged-in user |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/enrollments` | `book_lesson` |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/enrollments/{id}/withdraw` | Owner (the enrolled student) |
|
||||
|
||||
`POST /enrollments` body: `offering_id`, `answers[]` (`question_id` → value),
|
||||
`accepted_policy_version_ids[]`, and payment data (see `payments.md`). The
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user