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. Not flag-gated. Space checks then apply to the result when thespacesflag is on.
Golden paths
Owner: add a new class
- Go to
/dashboard/schedule→ “Tracks” / class types tab. - Click “Add class type”, pick parent program from a dropdown, name it, set defaults (60 min, capacity 20, default space, color), toggle
hasWorkoutif it’s a no-program class. 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).