Add invite-only group classes
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m49s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m40s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m49s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m40s
CI / Build Plugin Zip (pull_request) Skipped
Group classes can now be marked invite-only (us_offerings.access_mode). Invite-only classes are hidden from the public catalog and reachable only when the instructor lets someone in via one of three paths, managed from My Lessons -> My Group Classes: - Add students directly: enrols them now with a pending payment. - Make available: grants registered students access to self-enrol through the normal paid flow (multi-select, emailed a notice). - Invite by email: tokenised registration invite tied to the class for a non-account address; after they register the class becomes enrollable. Reuses an existing pending invite instead of sending a second link. New us_group_access table records grants; GET /offerings merges granted invite-only classes for the caller; enrolment requires a grant (403 invite_required) and flips it to enrolled on success. composer test (487), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
## 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).
|
||||
|
||||
A group class can be marked **invite-only** (`us_offerings.access_mode = invite_only`, see `offerings.md`). Invite-only classes are hidden from the public catalog — they never appear in the student booking/group-class list — and can only be enrolled in by students the instructor has let in. See **Invite-only access** below.
|
||||
|
||||
## Data Model — `{prefix}us_group_enrollments`
|
||||
|
||||
| Column | Type | Notes |
|
||||
@@ -53,14 +55,62 @@ 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.
|
||||
|
||||
`GET /offerings` (the catalog that feeds the group-class list) returns public
|
||||
offerings **plus** any invite-only offerings the caller has an access grant for, so a
|
||||
granted student sees the private class alongside public ones. Ungranted students never
|
||||
receive it. Enrolling in an invite-only class requires a grant: `POST /enrollments`
|
||||
rejects an ungranted student with `403 invite_required`, and a successful enrolment
|
||||
flips their grant from `invited` to `enrolled`.
|
||||
|
||||
## Invite-only access
|
||||
|
||||
Access to an invite-only class is recorded in `{prefix}us_group_access` — a grant per
|
||||
person, separate from the enrolment itself. The instructor manages access from
|
||||
**My Lessons → My Group Classes**, which renders three controls under each invite-only
|
||||
class:
|
||||
|
||||
1. **Add students directly** — the selected registered students are enrolled immediately
|
||||
(`status = active`) with a **pending payment** at the class price (comp students are
|
||||
settled at once by `PaymentService`). No access grant is needed — this writes straight
|
||||
to `us_group_enrollments` + `us_payments`.
|
||||
2. **Make available** — the selected registered students get an `invited` grant so the
|
||||
class appears in their own group-class list; they then self-enrol through the normal
|
||||
paid flow. Each is emailed a "you've been added" notice.
|
||||
3. **Invite by email** — for an address with no account yet: a tokenised personal invite
|
||||
(`us_invites`, carrying `offering_id`) is created and the registration link emailed,
|
||||
alongside an `invited` grant keyed by `email` + `invite_id`. If the address already has
|
||||
a **pending** invite, the grant is attached to that invite and **no second link is
|
||||
sent**. An address that already has an account is treated as **Make available** instead.
|
||||
|
||||
When an email-invited person completes registration, `RegistrationPage` links their new
|
||||
account to the grant (`GroupAccessRepository::linkStudentByEmail`), so the invite-only
|
||||
class becomes enrollable for them — they choose whether to enrol.
|
||||
|
||||
### Data Model — `{prefix}us_group_access`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---------------|-----------------|-----------------------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `offering_id` | BIGINT UNSIGNED | FK → `us_offerings.id` (an invite-only group class) |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID; NULL until an email invitee registers |
|
||||
| `email` | VARCHAR(191) | Email-invite grants only; used to link the account once it registers |
|
||||
| `invite_id` | BIGINT UNSIGNED | FK → `us_invites.id` for email-invite grants; NULL otherwise |
|
||||
| `status` | VARCHAR(20) | `invited` / `enrolled` / `revoked` |
|
||||
| `invited_by` | BIGINT UNSIGNED | Instructor who granted access |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
## 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
|
||||
per-class roster of enrolled students with enrolment and payment status. Invite-only
|
||||
classes additionally list who has been invited but not yet enrolled and carry the
|
||||
add/make-available/invite-by-email controls (nonce-checked `usc_action` POSTs, scoped to
|
||||
the owning instructor)
|
||||
|
||||
## Implementation
|
||||
- Repository: `Unsupervised\Schedular\GroupClass\EnrollmentRepository` (`countActiveForOffering`/`hasActiveEnrollment` enforce capacity and prevent duplicates)
|
||||
- Access grants: `Unsupervised\Schedular\GroupClass\GroupAccess` + `GroupAccessRepository` (`hasGrant`, `findGrantedOfferingIds`, `markEnrolled`, `linkStudentByEmail`)
|
||||
- Model: `Unsupervised\Schedular\GroupClass\Enrollment`
|
||||
- Admin controller: `Unsupervised\Schedular\GroupClass\GroupClassController` — `renderPage` (studio admin, `view_all_lessons`) and `renderInstructorPage` (instructor, `view_own_lessons`)
|
||||
- REST endpoint: `Unsupervised\Schedular\GroupClass\EnrollmentEndpoint`
|
||||
@@ -73,7 +123,11 @@ instructor's group classes if the caller has `view_own_lessons` on those offerin
|
||||
> for the card/e-transfer/comp flows.
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/GroupClass/GroupClassControllerTest.php`
|
||||
- `tests/Unit/GroupClass/GroupClassControllerTest.php` (roster + add/make-available/invite actions)
|
||||
- `tests/Unit/GroupClass/EnrollmentTest.php`
|
||||
- `tests/Unit/GroupClass/EnrollmentRepositoryTest.php`
|
||||
- `tests/Unit/GroupClass/EnrollmentEndpointTest.php` (invite-only gating)
|
||||
- `tests/Unit/GroupClass/GroupAccessTest.php`
|
||||
- `tests/Unit/GroupClass/GroupAccessRepositoryTest.php`
|
||||
- `tests/Unit/GroupClass/GroupClassPageTest.php`
|
||||
- `tests/Unit/Offering/OfferingEndpointTest.php` (catalog merges granted invite-only classes)
|
||||
|
||||
Reference in New Issue
Block a user