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

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:
2026-07-23 13:51:02 -03:00
co-authored by Claude Opus 4.8
parent 25aeba9dc1
commit a281935811
33 changed files with 1598 additions and 38 deletions
+12 -4
View File
@@ -15,11 +15,11 @@ class OfferingRepository {
* Column formats aligned to {@see columns()} (instructor_id, kind, title,
* description, duration_minutes, price, currency, billing_mode, allow_weekly,
* capacity, term_start, term_end, schedule_note, etransfer_email,
* cancellation_cutoff_hours, is_active).
* cancellation_cutoff_hours, access_mode, is_active).
*
* @var list<string>
*/
private const COLUMN_FORMATS = [ '%d', '%s', '%s', '%s', '%d', '%f', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%d' ];
private const COLUMN_FORMATS = [ '%d', '%s', '%s', '%s', '%d', '%f', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%d' ];
public function insert( Offering $offering ): int {
$this->db->insert(
@@ -63,16 +63,19 @@ class OfferingRepository {
'schedule_note' => $offering->scheduleNote,
'etransfer_email' => $offering->etransferEmail,
'cancellation_cutoff_hours' => $offering->cancellationCutoffHours,
'access_mode' => $offering->accessMode,
'is_active' => $offering->isActive ? 1 : 0,
];
}
/**
* Find offerings, optionally filtered by instructor, kind, and active state.
* Find offerings, optionally filtered by instructor, kind, active state, and
* access mode (e.g. `Offering::ACCESS_PUBLIC` to exclude invite-only classes
* from the public catalogue).
*
* @return list<Offering>
*/
public function findAll( int $instructorId = 0, string $kind = '', ?bool $activeOnly = null ): array {
public function findAll( int $instructorId = 0, string $kind = '', ?bool $activeOnly = null, ?string $accessMode = null ): array {
$where = [ '1 = 1' ];
$params = [];
@@ -91,6 +94,11 @@ class OfferingRepository {
$params[] = $activeOnly ? 1 : 0;
}
if ( null !== $accessMode ) {
$where[] = 'access_mode = %s';
$params[] = $accessMode;
}
$whereClause = implode( ' AND ', $where );
$sql = "SELECT * FROM %i WHERE {$whereClause} ORDER BY title ASC";