Instructors can now see their own group classes under My Lessons → My Group Classes (view_own_lessons): each class shows its active enrolment count against capacity plus a roster of enrolled students with enrolment and payment status. GroupClassController gains renderInstructorPage(), backed by the existing per-instructor enrolment query and a newly injected PaymentRepository for payment status. Wired as a submenu under the existing My Lessons menu, inside the same !view_all_lessons guard so owner-operators don't get a duplicate item. Closes #71 Co-Authored-By: Claude Opus 4.8 <[email protected]>
5.1 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 (
view_all_lessons/ studio admin): all active enrolments across instructors - My Lessons → My Group Classes (
view_own_lessons/ instructor): the instructor's own group classes, each showing its active-enrolment count against capacity and a per-class roster of enrolled students with enrolment and payment status
Implementation
- Repository:
Unsupervised\Schedular\GroupClass\EnrollmentRepository(countActiveForOffering/hasActiveEnrollmentenforce capacity and prevent duplicates) - Model:
Unsupervised\Schedular\GroupClass\Enrollment - Admin controller:
Unsupervised\Schedular\GroupClass\GroupClassController—renderPage(studio admin,view_all_lessons) andrenderInstructorPage(instructor,view_own_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.
Tests
tests/Unit/GroupClass/GroupClassControllerTest.phptests/Unit/GroupClass/EnrollmentTest.phptests/Unit/GroupClass/EnrollmentRepositoryTest.phptests/Unit/GroupClass/GroupClassPageTest.php