Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesScheduling BookingsScheduling & Bookings

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

PersonaWhy
OwnerCreates the schedule, publishes/cancels sessions, runs bulk ops, monitors attendance KPIs.
AdminSame capabilities as owner for scheduling (publish/cancel/bulk).
CoachCreates/updates draft sessions, assigns workouts, checks members in, runs the check-in display.
MemberSees 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 (ForbiddenException from ClassSessionsService.publish/unpublish/cancel).
  • Member: sees only status='published' sessions inside schedule-mode programs (or sessions they’re booked into for non-schedule programs). Receives bookingEligibility payload 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

  1. Session CRUD — create, update, set workouts, publish, unpublish, cancel; soft-delete via deletedAt.
  2. Inline workout creation — coaches can create a fresh workout body directly attached to a session in one round-trip (POST /sessions/:id/workouts/inline).
  3. Bulk operations — preview/delete/publish by filter (program, classType, dateFrom/To, days-of-week, start times, status). Single auditLogs row per batch with full payload.
  4. Booking lifecycle — confirmed → attended/no-show/cancelled; or waitlisted → confirmed when capacity opens via auto-promotion.
  5. Capacity, waitlist, overlap, daily/weekly quotas — enforced inside a SERIALIZABLE transaction with 40001 retry (withSerializableRetry).
  6. Credit accounting — deducts one credit on book, refunds on early cancel and waitlist promotion (unlimited subs untouched; staff bypassed).
  7. Cancellation window — org-configurable hours-before-start cutoff; late cancels block unless allowLateCancellation=true (no credit refund either way).
  8. 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).
  9. Notifications — booking confirmation/waitlisted/cancellation/promoted emails (templated, honours classReminder channel opt-out); push for confirmation + T-30min reminder; class-cancelled email blast on session/bulk cancel.
  10. Workout resolution — per-session workouts > org daily-programming entry for the date > none. Surfaced as workoutSource: 'session' | 'daily_programming' | 'none'.
  11. Attendance analytics/analytics/attendance/summary (avg/session, fill%, no-show%) and /analytics/attendance/daily (per-day attended/no-show).
  12. 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 reads latitude/longitude.
  • membershipsbookings.membershipId and class_sessions.coachMembershipId both FK to memberships. Staff bypass eligibility/credit logic via isStaffRole.
  • organizations — org owns the cancellation policy + timezone; all schedule queries push org-scope through classTypes → programs → organizationId.
  • Programs / daily-programmingprogram.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 provides classCredits, 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) using expo-camera, plus the universal-link landing at app/checkin.tsx that auto-fires POST /sessions/:id/self-checkin when the app opens from a deep link. The web shows the QR but doesn’t scan it. See docs/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 a batchId. No “view this batch” UI yet.
  • Schedule program vs feed/coaching/course filtering for member visibility (findTodayWithWorkouts line ~345) — class is hidden from members unless they’re booked OR the program is non-schedule mode.
  • TZ correctness — findTodayWithWorkouts was previously broken by host-TZ drift; fixed via formatInTimeZone + fromZonedTime (date-fns-tz). Still a comment landmine; bulk-delete startTimes filter relies on AT TIME ZONE in SQL.

Known gaps / TODOs

  • Self-checkin GPS requires location.latitude/longitude. If unset, raises BadRequestException. No fallback to org-level coords. (TODO: verify whether the UI prevents this.)
  • selfCheckin GPS path explicitly rejects program.deliveryMode='feed' — members on feed programs can only QR check-in.
  • attendanceSummary and attendanceDaily accept a days query (default 30). No upper bound documented; large windows scan all org sessions.
  • cancellation_review and manual_refund task types exist (see enums.taskType) but are not produced from this module; produced elsewhere on member-initiated cancels.