Daily programming — behavior spec
State machine (per slot = (class_type_id, date))
empty
│ PUT /daily-programming (status default 'draft')
▼
draft ─── PATCH /status status='published' ──> published
▲ │
└──────── PATCH /status status='draft' ──────────┘
│
└── DELETE /daily-programming (any state) ──> empty (physical delete)Members see only published rows. Coaches see both states in coach contexts (getCoachOverview etc. — TODO: verify this domain has a coach-facing aggregation; current API exposes the same read endpoint with role-aware filtering).
Invariants
- Unique row per (class_type, date, workout). DB UNIQUE constraint
(class_type_id, date, workout_id). - Whole-day replace.
PUT /daily-programmingalways deletes-then-inserts inside a transaction. There is no per-row patch. - Class type must belong to the calling org.
verifyClassTypeBelongsToOrgjoinsclass_types → programs.organization_id. - Workouts must belong to the calling org. All
workoutIds in the payload are validated together againstworkouts.organizationId = orgId AND deleted_at IS NULL. - Status flip applies to all rows for the slot. A single UPDATE keyed by
(class_type_id, date)mutates every workout in that slot together. - Member visibility. Members never see
draft. The service filters JS-side after the read (TODO: verify whether moving to SQL-side filter is on the roadmap). - No soft delete.
DELETEphysically removes rows.
Golden path — coach publishes today’s WOD
- Coach navigates to schedule’s daily programming surface (TODO: verify exact UI route).
- UI calls
PUT /daily-programming{ classTypeId:<wod>, date:'2026-06-03', status:'draft', workouts:[{workoutId:<fran>, sortOrder:0}, {workoutId:<accessory>, sortOrder:1}] }. - Service validates: class type belongs to org, both workouts belong to org. Opens transaction.
- DELETE existing rows for
(wod, 2026-06-03). INSERT new two rows withcreated_by = currentUserId. - Service returns
findByDatefor the slot. - Coach flips to published via
PATCH /daily-programming/status{ classTypeId:<wod>, date:'2026-06-03', status:'published' }. - Members booked into a WOD session that day now see Fran + accessory on their session card.
Golden path — applying a schedule template
- Coach applies a schedule template starting 2026-06-01.
program-templates.service.ts::applyToScheduleinserts bothclass_sessions(recurrence × duration) anddaily_programming(one row per cell that lands on an active day), allstatus='draft'.- Coach publishes daily programming per-slot via
PATCH /status(or by re-PUTting withstatus:'published').
Edge cases & error states
| Trigger | Handling |
|---|---|
classTypeId from another org | 400 “Class type not found in this organization”. |
workoutId from another org | 400 “One or more workouts not found in this organization”. |
workoutId of a soft-deleted workout | 400 (workout lookup filters deleted_at IS NULL). |
Empty workouts: [] | Allowed — effectively clears the slot. |
setStatus for a slot with no rows | UPDATE matches 0 rows; returns empty slot. |
remove for a non-existent slot | DELETE matches 0 rows; returns 200. |
GET with neither weekStart nor date | Returns []. |
| Member calls a write endpoint | 403 from role gate. |
Side effects
- No push notification on publish — members must visit the schedule/class card to see updates.
- No audit log entry for daily programming edits.
- Schedule template apply is the bulk writer; it writes daily programming in the same transaction as
class_sessions.
Permissions
| Action | Required role |
|---|---|
| Read (week / date) | Any member of the org. Members filtered to published rows. |
| Set (PUT) | owner, admin, coach |
| Set status (PATCH) | owner, admin, coach |
| Remove (DELETE) | owner, admin, coach |