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

Scheduling & Bookings — QA Plan

Pre-requisites

  • Test organization seeded with: 1 owner, 1 admin, 1 coach, 2 members; 1 program (schedule delivery mode); 2 class types; 1 location with lat/lng; 1 plan with 10 credits, maxBookingsPerDay=2, maxBookingsPerWeek=10, allowOverlappingBookings=false; member subscribed to that plan.
  • A second organization for cross-org isolation checks.
  • The org’s timezone set to a non-UTC value (e.g. Asia/Jerusalem) so TZ bugs surface.
  • cancellationWindowHours=2, allowLateCancellation=false for default policy tests.

Golden path scenarios

G1 — Owner creates and publishes a session

StepActionExpected
1Sign in as owner, navigate to /dashboard/schedule.Calendar week loads.
2Open “Create session”, pick class type, location, coach, start/end times, capacity=10, waitlist=5.Form validates.
3Submit with status=draft.Session appears as draft (visible to staff, not members).
4Click “Publish”.Status flips to published; members can now see it on their schedule.

G2 — Member books and is confirmed

StepActionExpected
1Sign in as member with active sub. Open /schedule.Sees the published session with bookingEligibility.status='eligible', plan unblocked.
2Tap “Book”.Booking confirmed. Confirmation email arrives. Push notification arrives.
3Check /bookings/my.Booking listed with status confirmed.
4Reload session.myBookingStatus='confirmed'. Remaining credits decremented by 1.

G3 — Member self-cancels with refund

StepActionExpected
1With G2 booking in place, well before cancellation deadline, tap “Cancel”.Booking row → cancelled, cancelled_at set. Email sent with creditRefunded=true. Remaining credits +1.

G4 — Waitlist auto-promotion

StepActionExpected
1Reduce a session’s capacity to 1 (or fully book it). Have member-A confirmed, member-B waitlisted (waitlist_position=1).
2Member-A cancels.Member-B promoted: status flips to confirmed, waitlist_position=null. “You’re in” email sent.

G5 — Staff manual check-in

StepActionExpected
1Coach opens session detail, sees roster with 5 confirmed members.
2Tap check-in on one member.Booking status → attended; checkin_method='manual'.
3Tap “Undo check-in”.Status → confirmed; checked_in_at and checkin_method cleared.

G6 — Member self-checkin via QR

StepActionExpected
1Staff generates display URL → QR code visible on a screen.
2Member scans within check-in window (default: TODO: verify window in isWithinCheckinWindow).Booking → attended, checkin_method='qr'.
3Scan again with the same QR.selfCheckin finds no confirmed booking (already attended) → 400.

G7 — Member self-checkin via GPS

StepActionExpected
1Location has lat/lng set. Member is physically within GPS_MAX_METERS.
2POST self-checkin with method=gps and current coords.Booking → attended, checkin_method='gps'.
3Repeat from > GPS_MAX_METERS away.400 with message GPS_TOO_FAR (exact string for client i18n).

G8 — Owner bulk-publishes a week of drafts

StepActionExpected
1Owner has 14 draft sessions for next week (twice-daily).
2POST /sessions/bulk-preview with {programId, dateFrom, dateTo, status:'draft'}.Returns {matched:14, withActiveBookings:0, samples:[10 rows]}.
3POST /sessions/bulk-publish with the same filter.Returns {batchId, publishedSessions:14}. All 14 flip to published. Audit log row exists.

G9 — Owner bulk-deletes wrong-day sessions

StepActionExpected
1Owner accidentally creates 8 sessions on Saturday for an org that’s closed Saturdays.
2POST bulk-preview with daysOfWeek:['saturday'].Returns matched count + 10 samples.
3POST bulk-delete with notifyAffectedMembers:true.Sessions soft-deleted, active bookings cancelled, batch audit row written, cancellation emails sent.

Edge cases

E1 — Race for the last slot

StepActionExpected
1Session has capacity=1, 0 booked. Two members tap Book simultaneously.One gets confirmed. The other either gets waitlisted (if waitlist available) or BadRequestException('Session is full'). Both transactions retry on 40001 up to 3 times.

E2 — Booking with no credits

StepActionExpected
1Member’s remaining_credits=0 on a class-pack plan.
2Tap Book.400 ‘No credits remaining’. Booking row not created.

E3 — Daily / weekly quota hit

StepActionExpected
1Member already has 2 bookings today on a plan with maxBookingsPerDay=2.
2Book a 3rd session today.400 ‘Daily booking limit reached (2 per day)’.

E4 — Overlap

StepActionExpected
1Member booked 09:00–10:00. Tries to book 09:30–10:30. Plan disallows overlapping.400 ‘You already have a booking that overlaps…’.

E5 — Late cancel

StepActionExpected
1Session starts in 30 min. Member tries to cancel. Org cancellationWindowHours=2, allowLateCancellation=false.400 with hours-before message.
2Owner flips allowLateCancellation=true. Member retries.Cancel succeeds. No credit refund (verify remaining credits unchanged).

E6 — Cancel after window with allowLateCancellation=true, then promotion

StepActionExpected
1Session full; one waitlisted. Window passed but allowLateCancellation=true. Confirmed member cancels.Booking cancelled with no refund. Waitlisted member auto-promoted with “you’re in” email.

E7 — Cross-org isolation

StepActionExpected
1Owner A signs in. POST /organizations/{B}/sessions/{anything}/publish.403 ‘Not a member of this organization’ (from requireMembership).
2Even with both orgs’ UUIDs known: try to checkIn org-B’s booking via org-A’s session URL.404 — service joins through programs.organization_id and rejects.

E8 — Concurrent waitlist promotion

StepActionExpected
1Session full; 3 waitlisted (positions 1,2,3). Two confirmed members cancel within ms of each other.Two promotions occur (positions 1 and 2). All status updates run inside serializable; no double-promotion of the same row.

E9 — Unpublish guard

StepActionExpected
1Published session with 3 confirmed bookings. POST /:id/unpublish.400 ‘Cannot unpublish a session that has active bookings’.
2Cancel all 3 bookings; retry unpublish.Succeeds; status → draft.

E10 — Coach trying to publish

StepActionExpected
1Coach POST /:id/publish.403 ‘Only owners and admins can publish sessions’.

E11 — Inline workout requires session ownership

StepActionExpected
1Coach in org A POST inline workout on org B’s session.404 ‘Session not found’.

E12 — Bulk-delete excludes booked by default

StepActionExpected
15 sessions match filter; 2 have active bookings. includeBooked not set (default false).preview: matched=3, withActiveBookings=2. Delete: 3 sessions deleted, 2 untouched.
2Re-run with includeBooked:true.5 deleted, bookings cancelled, emails sent.

E13 — TZ correctness on bulk filter startTimes

StepActionExpected
1Org tz=Asia/Jerusalem. Sessions stored at 07:00 local (UTC stored as 04:00 or 05:00 depending on DST). Bulk filter startTimes=['07:00'].Matches the 07:00-local sessions regardless of host TZ or DST.

E14 — Display screen signed URL expires

StepActionExpected
1Use a display URL > TTL hours old.403 ‘Invalid or expired display link’.

Cross-persona

ScenarioSetupExpected
Coach books on their own sessionCoach has membership; books via webStaff bypass: no credit deduction, no quota check. Booking row has subscription_id=NULL.
Member’s two membershipsMember belongs to org A and org B, both with active subs/bookings/my returns rows across both orgs.
Admin cancels then member rebooksAdmin admin-cancels a member’s confirmed bookingMember can re-book the same session (unique constraint fine — cancelled row remains, new row inserted). Verify: TODO — the existing-active-booking check filters by status, but the DB unique is (class_session_id, membership_id) regardless of status. Confirm whether re-book inserts a new row or hits the unique.

i18n

LangCheck
enCancellation deadline error: “Cancellation window has closed. You can cancel up to {N} hour(s) before class start.”
heSame translation present in he.json under member.cancellation.* / schedule.*.
ruSame in ru.json.
AllGPS_TOO_FAR is a stable machine string the client translates to the localized “You are too far from the session location…” key (schedule.checkinPage.tooFar in en.json).
RTLHebrew calendar grid renders right-to-left; cancellation deadline text aligns to start.

Expected vs actual checks

  • DB: after each test, query bookings and class_sessions directly to confirm status, cancelled_at, deleted_at, checked_in_at, checkin_method match expectations.
  • Audit: bulk operations must leave exactly one audit_logs row per batchId with action ∈ {class_session.bulk_delete, class_session.bulk_publish} and the full filter payload.
  • Email: assert SES/mailpit log contains one entry per affected member; subject matches the template (e.g. “Class cancelled: Yoga Flow”).
  • Push: assert a record was scheduled for T-30 min reminder (BullMQ inspector or push service mock).
  • Credits: subscriptions.remaining_credits matches expected after every book/cancel/promotion.