CI / Tests (PHP 8.2) (pull_request) Successful in 38s
CI / Tests (PHP 8.1) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 1s
CI / Coding Standards (pull_request) Successful in 2m49s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
The README still listed Payments as partial with the Stripe card charge pending, and group-classes.md still described the pre-#7 payment seam. Both are behind the code: StripeGateway/PaymentEndpoint ship the live card charge, and enrolments create and link payments via PaymentService. Fixes #73 Co-Authored-By: Claude Fable 5 <[email protected]>
80 lines
5.0 KiB
Markdown
80 lines
5.0 KiB
Markdown
# Feature: Group Classes
|
|
|
|
## Overview
|
|
Students enrol in a group class — an offering of kind `group_class` — as a commitment for the year. Enrolment is capacity-enforced and billed full-term upfront. Registration reuses the same flow as private lessons (intake questions + policy acceptance + payment).
|
|
|
|
## Data Model — `{prefix}us_group_enrollments`
|
|
|
|
| Column | Type | Notes |
|
|
|----------------|------------------|-------------------------------------------------------------|
|
|
| `id` | BIGINT UNSIGNED | Primary key |
|
|
| `offering_id` | BIGINT UNSIGNED | FK → `us_offerings.id` (kind = `group_class`) |
|
|
| `student_id` | BIGINT UNSIGNED | WordPress user ID |
|
|
| `instructor_id`| BIGINT UNSIGNED | WordPress user ID (denormalised from the offering) |
|
|
| `status` | VARCHAR(20) | `active` / `cancelled` / `completed` |
|
|
| `payment_id` | BIGINT UNSIGNED | Nullable FK → `us_payments.id` |
|
|
| `enrolled_at` | DATETIME | Insertion time |
|
|
|
|
## Class Dates
|
|
A group class offering carries `term_start`/`term_end` (see `offerings.md`):
|
|
one-off classes end the day they start; weekly classes run a set number of
|
|
sessions. The class card on the enrolment page shows the date or date range
|
|
with the session count.
|
|
|
|
## Enrolment Flow
|
|
The class list is loaded together with the student's own enrolments
|
|
(`GET /enrollments`); a class the student already has an `active` enrolment in
|
|
shows "You are enrolled in this class." instead of the Enrol button (the
|
|
server would reject the duplicate with `409 already_enrolled` regardless — a
|
|
cancelled enrolment does not block re-enrolling).
|
|
|
|
1. Student opens a group class from the offering catalog.
|
|
2. Student answers the offering's questions (`GET /offerings/{id}/questions`).
|
|
3. Student accepts the current published policy versions (`GET /policies`) — required to continue.
|
|
4. Full-term payment is taken per the student's billing method (card by default; `pending` for e-transfer; skipped for comp). See `payments.md`.
|
|
5. `POST /enrollments` creates the enrolment (`status = active`), records answers and policy acceptances, and links the payment — but only if the offering's `capacity` has not been reached.
|
|
6. On successful payment (or comp) a receipt is emailed.
|
|
|
|
Capacity is enforced at enrolment time by counting `active` rows for the offering;
|
|
a class at capacity rejects further enrolments.
|
|
|
|
## 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` |
|
|
|
|
`POST /enrollments` body: `offering_id`, `answers[]` (`question_id` → value),
|
|
`accepted_policy_version_ids[]`, and payment data (see `payments.md`). The
|
|
response includes `id`, `status`, and `payment` — a `{id, method, status}`
|
|
summary, or `null` when the class is free (the front end then skips the
|
|
payment step).
|
|
|
|
`GET /enrollments` returns the caller's own enrolments, or all enrolments for the
|
|
instructor's group classes if the caller has `view_own_lessons` on those offerings.
|
|
|
|
## Admin Interface
|
|
- **Group Classes** (`manage_options` / studio admin): all enrolments across instructors
|
|
- Instructors see enrolments for their own group classes under **My Lessons**
|
|
|
|
## Implementation
|
|
- Repository: `Unsupervised\Schedular\GroupClass\EnrollmentRepository` (`countActiveForOffering`/`hasActiveEnrollment` enforce capacity and prevent duplicates)
|
|
- Model: `Unsupervised\Schedular\GroupClass\Enrollment`
|
|
- Admin controller: `Unsupervised\Schedular\GroupClass\GroupClassController` (gated on `view_all_lessons`)
|
|
- REST endpoint: `Unsupervised\Schedular\GroupClass\EnrollmentEndpoint`
|
|
- Frontend: `Unsupervised\Schedular\GroupClass\GroupClassPage` (`[us_group_classes]` shortcode; `offering="…"` restricts it to a single class for embedding on a dedicated page — the block equivalent is the `offeringId` attribute)
|
|
- Reuses `Registration\RegistrationGate` (intake answers + booking-scoped policy acceptance, type `enrollment`)
|
|
|
|
> **Payment:** a priced enrolment creates a payment via `Payment\PaymentService`
|
|
> (`registration_type = enrollment`) and links it as `payment_id`; unpriced
|
|
> enrolments return `payment: null` and skip the payment step. See `payments.md`
|
|
> for the card/e-transfer/comp flows. Instructor-specific enrolment views (the
|
|
> spec's "under My Lessons") are a follow-up (#71) — this iteration ships the
|
|
> studio-admin **Group Classes** page (`view_all_lessons`) plus
|
|
> per-student/per-instructor REST queries.
|
|
|
|
## Tests
|
|
- `tests/Unit/GroupClass/EnrollmentTest.php`
|
|
- `tests/Unit/GroupClass/EnrollmentRepositoryTest.php`
|
|
- `tests/Unit/GroupClass/GroupClassPageTest.php`
|