Skip to content

Session

Purpose

A concrete date+time occurrence of an Activity — the thing a customer actually books. A session can be created automatically by materialising a Time slot recurrence rule, or directly by an admin via the sessions endpoint. Every Booking points at exactly one session.

Identity & key fields

  • Primary key: id (uuid, default gen_random_uuid()).
  • activityId (uuid, FK → activities.activities.id, on-delete cascade).
  • locationId (nullable uuid, FK → activities.locations.id).
  • timeSlotId (nullable uuid, FK → activities.time_slots.id, on-delete set null) — link back to the recurrence rule that generated this session.
  • startsAt, endsAt (timestamps, NOT NULL).
  • price (nullable decimal 10,2) — override over activity price.
  • capacityOverride (nullable integer).
  • status (enum session_status: AVAILABLE, BOOKED, CANCELLED, default AVAILABLE).

capacityOverride is the gate on auto-status: when set, status auto-syncs between AVAILABLE and BOOKED based on active-booking count. When NULL (unlimited), the session never moves to BOOKED even if it has many bookings — BOOKED is a “no more slots” flag, not “has at least one booking”.

Invariants

  • activityId ON DELETE CASCADE (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).
  • timeSlotId ON DELETE SET NULL — deleting the recurrence rule does not erase scheduled occurrences (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).
  • UNIQUE on (timeSlotId, startsAt) — prevents duplicate sessions for the same recurrence rule at the same time (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).
  • status defaults to AVAILABLE; startsAt, endsAt NOT NULL (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/activities.schema.ts).

Business invariants:

  • startsAt < endsAt — application-level only, no DB CHECK.
  • capacityOverride IS NULL → status NEVER auto-transitions to BOOKED. The session is effectively unlimited.
  • capacityOverride IS NOT NULL → status is auto-synced by SessionsService.syncStatus: BOOKED when active-booking count (excluding CANCELLED/REFUNDED) reaches capacity, AVAILABLE otherwise.
  • For Activity with type=SERVICE, sessions get capacityOverride=1 forced (sessions.service.ts).

Lifecycle

Status enum values: AVAILABLE, BOOKED, CANCELLED.

created → AVAILABLE (default — both from time-slot materialisation and admin create)
AVAILABLE ↔ BOOKED (auto, via SessionsService.syncStatus — only if capacityOverride IS NOT NULL)
* → CANCELLED (admin via PATCH /api/business/sessions/{id} with status: 'CANCELLED')

syncStatus is invoked from every booking mutation path:

  • bookings.service.ts:83, 93, 267 (admin booking flows)
  • bookings-client.service.ts:112, 151, 259 (client booking flows)

Materialisation sources (verified against code)

Sessions are created from three sources:

SourceWhenNotes
TimeSlotsService.generateSessionsOn time-slot create / update (sync)Generates sessions for N days ahead
BullMQ scheduler — generateSessionsForSlotPer-slot job that returns nextRunInMs for re-queueingSteady-state materialisation
ActivitiesScheduler.handleSessionGenerationSafetyNet (cron EVERY_DAY_AT_MIDNIGHT)Daily backstopBackfills any active slots that were missed
Admin POST /api/business/sessionsAd-hoc one-off sessionsInserted with timeSlotId=NULL

Files: libs/features/activities/src/lib/services/time-slots.service.ts, libs/features/activities/src/lib/services/activities-scheduler.service.ts.

Relationships

  • Activity (ENT-005) — activityIdactivities.activities.id, on-delete cascade. N:1.
  • Location (ENT-011) — locationIdactivities.locations.id. N:1, optional.
  • Time slot (ENT-013) — timeSlotIdactivities.time_slots.id, on-delete set null. N:1, optional.
  • Company member (coach) (ENT-019) — N:M via session_coaches (pure join, PK = (sessionId, memberId), on-delete cascade both sides).
  • Booking (ENT-003) — referenced by bookings.bookings.session_id. 1:N.

API surfaces

SurfaceExposedNotes
clientyes — /activities/{activityId}/sessions, /companies/{companyId}/activities/{activityId}/sessions (SessionAvailableClientResponseDto, BookingSessionDto, UpcomingSessionDto, SessionCoachClientDto)Swagger UI
businessyes — /sessions, /sessions/{id} (SessionResponseDto, CreateSessionDto, UpdateSessionDto, SessionStatus, SessionAttendeePreviewDto, SessionBookingDto)Swagger UI
super-adminno

Per-surface fields diverge: client gets aggregated availability + coach previews; business gets the editable shape with attendee and booking lists.

Known gotchas / open questions

  • Sessions can outlive their generating time_slot (on-delete set null). Reports filtered by slot must handle null.
  • The UNIQUE on (timeSlotId, startsAt) allows multiple ad-hoc sessions at the same startsAt only if timeSlotId is NULL — i.e. duplicate detection is per-rule.
  • BOOKED is NOT “has bookings”. It means “active bookings ≥ capacity”. Sessions with capacityOverride IS NULL (unlimited) never reach BOOKED regardless of booking count. This often confuses product folks reading the enum name.
  • syncStatus is not transactionally isolated with booking create/cancel — there is no SELECT FOR UPDATE on the session row. Under concurrent booking writes the count read may race the status write, leaving stale AVAILABLE on a full session momentarily.

Recommendations

Forward-looking improvements suggested while filling this doc — not currently in place.

  • DB CHECK starts_at < ends_at — promote this app-level invariant to the database.
  • Lock the session row in syncStatus (SELECT ... FOR UPDATE or PostgreSQL advisory lock) to remove the race window between count-read and status-update under concurrent bookings.
  • Rename SessionStatusEnum values to be unambiguous — e.g. OPEN | FULL | CANCELLED instead of AVAILABLE | BOOKED | CANCELLED. “BOOKED” wrongly implies “this session has bookings”.
  • Document BullMQ jobs (queue names, payload shapes, retry/dedup config) for session generation in Catalog context.
  • Add cancelledAt / cancelledReason columns to capture why and when a session was cancelled — useful for refund flows and admin reporting.