Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesDaily ProgrammingDaily programming — behavior spec

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-programming always deletes-then-inserts inside a transaction. There is no per-row patch.
  • Class type must belong to the calling org. verifyClassTypeBelongsToOrg joins class_types → programs.organization_id.
  • Workouts must belong to the calling org. All workoutIds in the payload are validated together against workouts.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. DELETE physically removes rows.

Golden path — coach publishes today’s WOD

  1. Coach navigates to schedule’s daily programming surface (TODO: verify exact UI route).
  2. UI calls PUT /daily-programming { classTypeId:<wod>, date:'2026-06-03', status:'draft', workouts:[{workoutId:<fran>, sortOrder:0}, {workoutId:<accessory>, sortOrder:1}] }.
  3. Service validates: class type belongs to org, both workouts belong to org. Opens transaction.
  4. DELETE existing rows for (wod, 2026-06-03). INSERT new two rows with created_by = currentUserId.
  5. Service returns findByDate for the slot.
  6. Coach flips to published via PATCH /daily-programming/status { classTypeId:<wod>, date:'2026-06-03', status:'published' }.
  7. Members booked into a WOD session that day now see Fran + accessory on their session card.

Golden path — applying a schedule template

  1. Coach applies a schedule template starting 2026-06-01.
  2. program-templates.service.ts::applyToSchedule inserts both class_sessions (recurrence × duration) and daily_programming (one row per cell that lands on an active day), all status='draft'.
  3. Coach publishes daily programming per-slot via PATCH /status (or by re-PUTting with status:'published').

Edge cases & error states

TriggerHandling
classTypeId from another org400 “Class type not found in this organization”.
workoutId from another org400 “One or more workouts not found in this organization”.
workoutId of a soft-deleted workout400 (workout lookup filters deleted_at IS NULL).
Empty workouts: []Allowed — effectively clears the slot.
setStatus for a slot with no rowsUPDATE matches 0 rows; returns empty slot.
remove for a non-existent slotDELETE matches 0 rows; returns 200.
GET with neither weekStart nor dateReturns [].
Member calls a write endpoint403 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

ActionRequired 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