Daily programming — QA plan
Pre-requisites
- Pro-tier org with:
- Schedule-mode program “Open Box” containing two class types: “WOD” and “Open Gym”.
- Library workouts: “Fran”, “Easy Row”, “Skills Day”.
- Personas: Coach Casey, Owner Olga, Member Mia.
- Mia has a booking on the WOD session 2026-06-03.
- Org timezone
Asia/Jerusalem.
Test scenarios
Set / replace
-
Set a slot with one workout
- Trigger: Casey
PUT /daily-programming{ classTypeId:<wod>, date:'2026-06-03', workouts:[{workoutId:<fran>, sortOrder:0}] }. - Expected: 1 row in
daily_programmingwithstatus='draft',created_by=Casey. Response is the grouped slot with that one workout.
- Trigger: Casey
-
Set two workouts on the same slot
- Trigger: same
PUTwith two workouts at sortOrder 0 and 1. - Expected: 2 rows. UNIQUE constraint not violated (different
workout_id).
- Trigger: same
-
Replace existing slot
- Trigger: after step 2,
PUTagain with one different workout. - Expected: the previous 2 rows are physically deleted; only the new 1 row remains.
- Trigger: after step 2,
-
Set with empty workouts[]
- Trigger:
PUT{ ..., workouts: [] }. - Expected: existing rows deleted; slot effectively empty.
- Trigger:
-
Set with cross-org workoutId
- Trigger: workoutId from a different org.
- Expected: 400 “One or more workouts not found in this organization”.
-
Set with soft-deleted workoutId
- Trigger: workoutId of a workout with
deleted_atset. - Expected: 400.
- Trigger: workoutId of a workout with
-
Set with cross-org classTypeId
- Trigger: classTypeId from another org.
- Expected: 400 “Class type not found in this organization”.
-
Duplicate workoutId in the same payload
- Trigger:
workouts:[{wid:X, sortOrder:0}, {wid:X, sortOrder:1}]. - Expected: UNIQUE
(class_type_id, date, workout_id)rejects the second insert. TODO: verify whether service deduplicates first or the DB raises.
- Trigger:
Status
-
Publish a slot
- Trigger:
PATCH /daily-programming/status{ classTypeId:<wod>, date:'2026-06-03', status:'published' }. - Expected: all rows for the slot become
published.
- Trigger:
-
Unpublish back to draft
- Trigger: same with
status:'draft'. - Expected: all rows back to
draft.
- Trigger: same with
-
Status flip on empty slot
- Trigger:
PATCH /statuson a slot with no rows. - Expected: 200, no rows updated, response shows empty slot.
- Trigger:
Reads
-
Week read as coach
- Trigger:
GET /daily-programming?weekStart=2026-06-01. - Expected: rows for the whole week, including drafts. Grouped per (classType, date).
- Trigger:
-
Week read as member with drafts present
- Trigger: same as 12 but as Mia.
- Expected: drafts filtered out; only
publishedrows visible.
-
Date read with explicit classType filter
- Trigger:
GET /daily-programming?date=2026-06-03&classTypeId=<wod>(passed via servicefindByDatewith classTypeId). - Note: the HTTP
GET /does NOT acceptclassTypeId; the route usesfindByDate(orgId, clerkId, date). The classTypeId-scoped read is internal. TODO: verify which is preferred.
- Trigger:
-
Week read returns no rows for empty week
- Trigger: a week with no programming.
- Expected:
[].
-
Member sees workout sections
- Trigger: Mia
GET /daily-programming?date=2026-06-03for a slot that’s published. - Expected: response carries the workout with sections + movements + exercise metadata.
- Trigger: Mia
Remove
-
Remove a slot
- Trigger:
DELETE /daily-programming?classTypeId=<wod>&date=2026-06-03. - Expected: rows physically deleted. Subsequent GET returns the slot absent (no draft tombstone).
- Trigger:
-
Remove a non-existent slot
- Trigger: same on an empty slot.
- Expected: 200, no-op.
Template apply integration
- Apply schedule template writes daily-programming
- Trigger:
POST /program-templates/:id/applywith modeschedule, startDate. - Expected: new
daily_programmingrows (status='draft') for each cell that lands on an active day. Coach must publish manually.
- Trigger:
Permissions
-
Member tries to PUT
- Trigger: Mia
PUT /daily-programming. - Expected: 403.
- Trigger: Mia
-
Member tries to PATCH status
- Trigger: Mia
PATCH /daily-programming/status. - Expected: 403.
- Trigger: Mia
-
Cross-org read
- Trigger: Coach Bob (org B) calls org A’s endpoint.
- Expected: 403 from
requireMembership.
Class-session integration
-
Class session card shows the day’s published workouts
- Trigger: Mia opens the WOD session for 2026-06-03 on her schedule.
- Expected: the session detail loads via
ClassSessionsService.findByDateRangeand renders Fran. If the slot isdraft, Mia sees no workout.
-
Class session card shows nothing in draft
- Trigger: same after unpublishing.
- Expected: workout disappears from member’s view.
i18n
-
Whiteboard in Hebrew
- Trigger: Mia in
heviews the WOD board. - Expected: section labels, movement names (canonical with Hebrew override), and time-cap labels all RTL/translated.
- Trigger: Mia in
-
Coach UI in Russian
- Trigger: Casey in
rusets the day’s programming. - Expected: status chips (“Draft”/“Published”) and form labels translated.
- Trigger: Casey in
What “broken” looks like
- A previously-removed workout reappears after a status flip (because
PATCH /statusdoesn’t restore physical deletes — but if a bug re-inserted rows on flip, would show this symptom). - Member sees
draftrows (member visibility filter regressed). - Two coaches editing the same slot simultaneously each end up with their own row set (race in the delete-then-insert transaction).
- Cross-org workout slips into the slot (
workoutIdorg validation missing). - Daily programming for a deleted class type persists and renders ghost entries (the class type FK is set NULL’d? — verify CASCADE behavior; current schema does not specify ON DELETE on
class_type_id). - Member sees programming for a class type that doesn’t belong to their org (org scoping via
getOrgClassTypeIdsregressed). - Status enum drift — service accepts arbitrary strings instead of
draft/published(currently theIsInvalidator on the DTO enforces this).