Scheduling & Bookings
What is this
Class sessions are the operational unit of a studio’s calendar: a concrete instance of a class type, at a time and (usually) a location, optionally led by a coach. Members book a session, get confirmed or waitlisted, and either attend, no-show, or cancel. The schedule view, attendance roster, QR/GPS check-in, and the “my upcoming bookings” surface all read from class_sessions + bookings.
This bucket covers everything from creating a single draft session through bulk-deleting a week of accidentally-published slots, the booking transaction (capacity + waitlist + credit deduction + overlap quotas, in a single serializable transaction with retry), and the full check-in machinery (manual, QR token, GPS Haversine, signed display-screen URLs).
Who uses it
| Persona | Why |
|---|---|
| Owner | Creates the schedule, publishes/cancels sessions, runs bulk ops, monitors attendance KPIs. |
| Admin | Same capabilities as owner for scheduling (publish/cancel/bulk). |
| Coach | Creates/updates draft sessions, assigns workouts, checks members in, runs the check-in display. |
| Member | Sees the weekly calendar, books, cancels, self-checks-in via QR/GPS, sees their booking history. |
Persona impact
- Owner: full mutate authority including bulk-delete and bulk-publish; configures org-wide cancellation policy (
cancellationWindowHours,allowLateCancellation) that the cancel flow enforces. - Coach: can create/update/set workouts on sessions, run check-in, but cannot publish, unpublish, or cancel (
ForbiddenExceptionfromClassSessionsService.publish/unpublish/cancel). - Member: sees only
status='published'sessions inside schedule-mode programs (or sessions they’re booked into for non-schedule programs). ReceivesbookingEligibilitypayload per session — staff bypasses eligibility checks entirely. - No payment provider configured → members can book without a plan (
eligibility.status='eligible',plans=[]). With provider configured but no active sub →eligibility.status='no_plan', booking blocked.
High-level capabilities
- Session CRUD — create, update, set workouts, publish, unpublish, cancel; soft-delete via
deletedAt. - Inline workout creation — coaches can create a fresh workout body directly attached to a session in one round-trip (
POST /sessions/:id/workouts/inline). - Bulk operations — preview/delete/publish by filter (program, classType, dateFrom/To, days-of-week, start times, status). Single
auditLogsrow per batch with full payload. - Booking lifecycle — confirmed → attended/no-show/cancelled; or waitlisted → confirmed when capacity opens via auto-promotion.
- Capacity, waitlist, overlap, daily/weekly quotas — enforced inside a
SERIALIZABLEtransaction with40001retry (withSerializableRetry). - Credit accounting — deducts one credit on book, refunds on early cancel and waitlist promotion (unlimited subs untouched; staff bypassed).
- Cancellation window — org-configurable hours-before-start cutoff; late cancels block unless
allowLateCancellation=true(no credit refund either way). - Check-in — staff manual, member-self via QR (HMAC token, 60s TTL), member-self via GPS (Haversine ≤
GPS_MAX_METERS), check-in display screen (signed URL). - Notifications — booking confirmation/waitlisted/cancellation/promoted emails (templated, honours
classReminderchannel opt-out); push for confirmation + T-30min reminder; class-cancelled email blast on session/bulk cancel. - Workout resolution — per-session workouts > org daily-programming entry for the date > none. Surfaced as
workoutSource: 'session' | 'daily_programming' | 'none'. - Attendance analytics —
/analytics/attendance/summary(avg/session, fill%, no-show%) and/analytics/attendance/daily(per-day attended/no-show). - My bookings — paginated, filterable by status range/date range across all the user’s orgs (multi-membership safe).
Relationship to other features
- class-types — every session belongs to a
class_type(mandatory FK); class type provides defaults (defaultCapacity,defaultDurationMin,color). - locations — session’s optional
locationId; GPS check-in readslatitude/longitude. - memberships —
bookings.membershipIdandclass_sessions.coachMembershipIdboth FK tomemberships. Staff bypass eligibility/credit logic viaisStaffRole. - organizations — org owns the cancellation policy + timezone; all schedule queries push org-scope through
classTypes → programs → organizationId. - Programs / daily-programming —
program.deliveryMode='schedule'is the only mode where standalone sessions surface for non-booked members; other delivery modes route through daily programming or feed. - Subscriptions/plans — bookings reference an optional
subscriptionId; plan providesclassCredits,maxBookingsPerDay/Week,allowOverlappingBookings. - Notifications / push — fire-and-forget after-commit; respect
notification-prefs. - Mobile (
fitkit-mobile) — owns the QR-scan check-in surface (app/(tabs)/schedule/scan.tsx) usingexpo-camera, plus the universal-link landing atapp/checkin.tsxthat auto-firesPOST /sessions/:id/self-checkinwhen the app opens from a deep link. The web shows the QR but doesn’t scan it. Seedocs/architecture/mobile.md.
Current status
Shipped and core to the product. Recent operational concerns surfaced in code comments:
- Bulk-delete + bulk-publish (
apps/api/src/class-sessions/class-sessions.service.ts:887–1210) is the newest hot path; audit-logged under abatchId. No “view this batch” UI yet. - Schedule program vs feed/coaching/course filtering for member visibility (
findTodayWithWorkoutsline ~345) — class is hidden from members unless they’re booked OR the program is non-schedule mode. - TZ correctness —
findTodayWithWorkoutswas previously broken by host-TZ drift; fixed viaformatInTimeZone+fromZonedTime(date-fns-tz). Still a comment landmine; bulk-deletestartTimesfilter relies onAT TIME ZONEin SQL.
Known gaps / TODOs
- Self-checkin GPS requires
location.latitude/longitude. If unset, raisesBadRequestException. No fallback to org-level coords. (TODO: verify whether the UI prevents this.) selfCheckinGPS path explicitly rejectsprogram.deliveryMode='feed'— members on feed programs can only QR check-in.attendanceSummaryandattendanceDailyaccept adaysquery (default 30). No upper bound documented; large windows scan all org sessions.cancellation_reviewandmanual_refundtask types exist (seeenums.taskType) but are not produced from this module; produced elsewhere on member-initiated cancels.