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]>
5.0 KiB
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).
- Student opens a group class from the offering catalog.
- Student answers the offering's questions (
GET /offerings/{id}/questions). - Student accepts the current published policy versions (
GET /policies) — required to continue. - Full-term payment is taken per the student's billing method (card by default;
pendingfor e-transfer; skipped for comp). Seepayments.md. POST /enrollmentscreates the enrolment (status = active), records answers and policy acceptances, and links the payment — but only if the offering'scapacityhas not been reached.- 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/hasActiveEnrollmentenforce capacity and prevent duplicates) - Model:
Unsupervised\Schedular\GroupClass\Enrollment - Admin controller:
Unsupervised\Schedular\GroupClass\GroupClassController(gated onview_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 theofferingIdattribute) - Reuses
Registration\RegistrationGate(intake answers + booking-scoped policy acceptance, typeenrollment)
Payment: a priced enrolment creates a payment via
Payment\PaymentService(registration_type = enrollment) and links it aspayment_id; unpriced enrolments returnpayment: nulland skip the payment step. Seepayments.mdfor 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.phptests/Unit/GroupClass/EnrollmentRepositoryTest.phptests/Unit/GroupClass/GroupClassPageTest.php