Class Types — Behavior
State
Class types do not have a status machine. They have a single boolean is_active:
| State | Meaning |
|---|---|
is_active=true | Surfaces in findAll, allowed as class_type_id for new sessions. |
is_active=false | Hidden from findAll. Existing class sessions still reference it — soft-deactivate, not cascade. |
DELETE /class-types/:id performs update set is_active=false, never DELETE FROM. The row stays so historical sessions, bookings, and analytics keep their referential integrity.
Invariants
- Program must belong to the same org as the caller’s membership. Create + update both verify
program.organizationId === orgIdbefore mutating (class-types.service.ts:33-42,:115-121). - Class type cannot be reassigned to another program — there is no
programIdfield onUpdateClassTypeDto. - No hard delete — deactivation only. This is load-bearing for the schedule history.
- Org isolation via the program — class types have no direct
organization_idcolumn.findAlljoinsclass_typestoprogramsand filtersprograms.organization_id = orgIdin SQL — it used to fetch every active class type across every tenant and filter in JS, which meant any consumer holding the (unfiltered) intermediate result could see another org’s rows before the org check ran. findAllaccepts an optionalprogramIdfilter, pushed into the same SQL WHERE clause —GET /class-types?programId=returns only that program’s class types, never another program’s (in-org or not).- Default space must belong to the org —
defaultLocationIdon create/update must be a location of the same org; otherwiseBadRequestException('Location not found in this organization').nullclears it. The FK isON DELETE SET NULL, so a hard delete of the location (DB only; the API only deactivates) clears it. - Default space pre-fills, it does not bind — on
POST /sessionswithlocationIdomitted (undefined), the session getsdefaultLocationIdif that space is active. An explicitlocationId(includingnull) wins. Space checks then apply to the result. - Open classes don’t use a plan —
countsTowardQuota=falsemakes every session of the type an open class: any active member books it with or without a plan, the booking carries no subscription (no credit, cap or quota use, no refund), and only the debt block of the billing gate still applies. Defaulttrue. Changing it only affects new bookings; existing ones keep their subscription. Full rules: scheduling-bookings › Open classes.
Golden paths
Owner: add a new class
- With
workout-text-first-authoringon: go to/dashboard/scheduleand open Settings (the cog beside the greeting,schedule-settings-trigger). The sheet holds three views, Class types first, Timetable templates, and Booking Policies (owners and admins; a coach sees the first two).?settings=class-typesopens it on class types; the Workouts page’s empty Plan grid links there. Off: the schedule toolbar’s “This week” menu has a Class types item (schedule-setup-class-types) that opens the same class-types sheet. - Click “Add class type” (the program is the schedule program the sheet was opened for), name it, set defaults (60 min, capacity 20, default space, color), and toggle
hasWorkoutif it’s a no-program class. “Deducts an entry from the member’s plan” (ct-counts-toward-quota) sits under Advanced (ct-advanced), open by default only for a class type that is already open; switch it off for an open class. The class-types table badges such a typeclassTypes.noQuotaBadge(“No entry deducted”). POST /organizations/:orgId/class-typesreturns the new row + program metadata.- The class type appears in the create-session form.
Owner: deactivate
- Open class type; click “Delete”.
DELETE /organizations/:orgId/class-types/:id.- The class type disappears from
findAll. Historical sessions still resolveclassType(relational query returns the row regardless ofis_active).
Edge cases & error states
| Scenario | Behavior |
|---|---|
Create with programId belonging to a different org | BadRequestException('Program not found in this organization') |
Create/update with defaultLocationId not in this org | BadRequestException('Location not found in this organization') |
| Default space later deactivated | defaultLocationId stays set; new sessions with locationId omitted get no space (the prefill skips inactive spaces). |
| Member or coach tries to create/update/delete | ForbiddenException('Only owners and admins can …') |
| Get class type by id from another org | NotFoundException('Class type not found') (post-fetch org check) |
| Update with empty body | Returns the row with updated_at bumped; all field updates are conditional on !== undefined. |
| Deactivate a class type with active future sessions | Sessions remain. UI calendar still shows them. Booking still works. (TODO: confirm whether dashboard surfaces a warning.) |
Side effects
None outside the row mutation itself. No events emitted, no notifications, no cascades.
Permissions
| Action | owner | admin | coach | member |
|---|---|---|---|---|
| List | ✓ | ✓ | ✓ | ✓ |
| Get by id | ✓ | ✓ | ✓ | ✓ |
| Create | ✓ | ✓ | ✗ | ✗ |
| Update | ✓ | ✓ | ✗ | ✗ |
| Deactivate | ✓ | ✓ | ✗ | ✗ |
All endpoints require an active membership in the org; cross-org access fails at requireMembership (403) or the program.organizationId post-check (404).