Files
mp-pilates/docs/TIME_SLOT_SCHEDULING_SYSTEM.md
richarjiang b6986ba30c feat(admin): implement full day-by-day schedule editor with live preview
## Features

### Admin Schedule Page (`packages/app/src/pages/admin/schedule.vue`)
- Interactive date-based slot editor for managing daily schedules
- Real-time slot editing: start/end times, capacity adjustments
- Slot deletion with conflict warnings when bookings exist
- Add new slots with modal dialog
- Live booking status display (booked count, people names)
- Publish/Save changes with sync feedback
- Revert unsaved changes with confirmation
- Skeleton loading states and empty state handling
- Responsive design with optimized mobile UX

### Backend Enhancements
- **New DTO** (`PublishDaySlotsDto`): Structured slot publishing with validation
  - Date string validation
  - Slot array with existing slot IDs for updates
  - Time and capacity validation per slot

- **Schedule Preview API** (`getSchedulePreview`):
  - Check for existing published slots
  - Fallback to active WeekTemplates for unpublished dates
  - Unified response format with isPublished flag

- **Publish Slots API** (`publishDaySlots`):
  - Atomic transaction for consistency
  - Update existing slots with new times/capacity
  - Create new slots from template data
  - Delete unpublished slots or set to CLOSED if bookings exist
  - Prevent capacity reduction below existing bookings
  - Returns all published slots for feedback

### State Management
- Enhanced admin store with schedule state
- Support for pending/unsaved slot changes
- Optimistic UI updates with server sync

### Documentation
- Comprehensive scheduling system architecture docs
- Quick reference for admin workflows
- Flow diagrams and state transitions
- Implementation guide for future maintenance

## Breaking Changes
None

## Testing Recommendations
- Create slots for future dates via schedule editor
- Verify booking prevention for locked/full slots
- Test capacity adjustments with existing bookings
- Confirm template-based schedule generation
- Verify transaction rollback on publish failures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 12:18:49 +08:00

24 KiB

NestJS Time-Slot & Scheduling System Analysis

Executive Summary

This is a comprehensive analysis of the pilates studio booking system's time-slot generation and scheduling backend. The system automatically generates time slots from reusable weekly templates, maintains their lifecycle, and integrates tightly with the booking and membership management systems.


1. Data Models (Prisma Schema)

1.1 WeekTemplate Model

Location: packages/server/prisma/schema.prisma (lines 113-126)

model WeekTemplate {
  id        String   @id @default(uuid())
  dayOfWeek Int      @map("day_of_week")           // 1=Mon, 7=Sun (ISO standard)
  startTime String   @map("start_time")            // e.g., "09:00"
  endTime   String   @map("end_time")              // e.g., "10:00"
  capacity  Int      @default(1)                   // Max participants
  isActive  Boolean  @default(true)                // Enable/disable template
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  timeSlots TimeSlot[]                             // Generated slots from this template
}

Purpose:

  • Defines recurring time slots by day of week and time
  • Used as blueprint for automatic slot generation
  • Capacity defines how many people can book each slot

Key Constraints:

  • dayOfWeek uses ISO 8601 standard (1=Monday through 7=Sunday)
  • NOT JavaScript getDay() (0=Sunday)
  • Conversion happens in SlotGeneratorService.toIsoWeekday()

1.2 TimeSlot Model

Location: packages/server/prisma/schema.prisma (lines 128-148)

model TimeSlot {
  id          String         @id @default(uuid())
  date        DateTime       @db.Date              // Calendar date (midnight UTC)
  startTime   String         @map("start_time")    // "HH:mm" format
  endTime     String         @map("end_time")      // "HH:mm" format
  capacity    Int            @default(1)           // Max participants
  bookedCount Int            @default(0)           // Current bookings
  status      TimeSlotStatus @default(OPEN)        // OPEN | FULL | CLOSED
  source      TimeSlotSource @default(TEMPLATE)    // TEMPLATE | MANUAL
  templateId  String?        @map("template_id")   // Reference to WeekTemplate
  createdAt   DateTime       @default(now())
  updatedAt   DateTime       @updatedAt

  template WeekTemplate?  @relation(fields: [templateId], references: [id])
  bookings Booking[]

  @@unique([date, startTime, endTime])            // Prevent duplicate slots
  @@index([date])
  @@index([status])
}

Status Lifecycle:

  • OPEN: Accepts bookings, bookedCount < capacity
  • FULL: No more bookings, bookedCount >= capacity
  • CLOSED: Past date or manually closed, no bookings allowed

Source Types:

  • TEMPLATE: Auto-generated from WeekTemplate
  • MANUAL: Created directly by admin

1.3 Booking Model

Location: packages/server/prisma/schema.prisma (lines 150-168)

model Booking {
  id           String        @id @default(uuid())
  userId       String        @map("user_id")
  timeSlotId   String        @map("time_slot_id")
  membershipId String        @map("membership_id")
  status       BookingStatus @default(CONFIRMED)
  cancelledAt  DateTime?     @map("cancelled_at")
  createdAt    DateTime      @default(now())
  updatedAt    DateTime      @updatedAt

  user       User       @relation(fields: [userId], references: [id])
  timeSlot   TimeSlot   @relation(fields: [timeSlotId], references: [id])
  membership Membership @relation(fields: [membershipId], references: [id])

  @@unique([userId, timeSlotId])                  // One booking per user per slot
  @@index([userId])
  @@index([status])
}

Booking Status Values:

  • CONFIRMED: Active reservation
  • CANCELLED: User cancelled
  • COMPLETED: Slot time has passed
  • NO_SHOW: Marked manually if user didn't attend

2. SlotGeneratorService

Location: packages/server/src/time-slot/slot-generator.service.ts

2.1 Service Overview

Core service responsible for:

  1. Generating time slots from WeekTemplate
  2. Cleaning up expired slots
  3. Managing membership expiration
  4. Marking past bookings as completed

2.2 Key Methods

generateSlots(daysAhead: number = 14): Promise<number>

Purpose: Creates time slots for the next N days based on active WeekTemplates.

Algorithm:

1. Fetch all active WeekTemplates (isActive = true)
2. Calculate tomorrow at midnight UTC as start date
3. For each day in [tomorrow, tomorrow + daysAhead):
   a. Get ISO weekday (1-7) from JavaScript date
   b. Find matching templates for this weekday
   c. For each matching template, create slot data:
      - date: UTC midnight
      - startTime/endTime: from template
      - capacity: from template
      - source: TimeSlotSource.TEMPLATE
      - templateId: template.id
4. Batch create all slots using createMany() with skipDuplicates: true
5. Return count of newly created slots

Key Features:

  • Idempotent: Re-running is safe; duplicate date+startTime+endTime combos are skipped
  • Timezone Aware: Uses UTC midnight for dates
  • Weekday Mapping: Converts JS getDay() → ISO weekday
  • Batch Insert: Creates all slots in single database operation

Example Execution:

  • Today: Monday, April 7, 2026
  • Daylight: 14 days
  • Template: Monday 09:00-10:00, Friday 18:00-19:00
  • Result: 2 slots tomorrow (Monday), 0 Wed-Thu, 1 Friday, repeat pattern

cleanupExpiredSlots(): Promise<number>

Purpose: Marks all OPEN slots with dates before today as CLOSED.

Logic:

UPDATE time_slots
SET status = 'CLOSED'
WHERE status = 'OPEN' AND date < TODAY_MIDNIGHT_UTC

Returns: Count of slots closed.


checkExpiredMemberships(): Promise<number>

Purpose: Manages membership expiration in two ways:

  1. By Expiration Date:

    WHERE status = ACTIVE AND expireDate < NOW
    SET status = EXPIRED
    
  2. By Used-Up Sessions:

    WHERE status = ACTIVE AND remainingTimes = 0
    SET status = USED_UP
    

Returns: Total count of memberships updated.


completeBookings(): Promise<number>

Purpose: Marks CONFIRMED bookings for past time slots as COMPLETED.

Logic:

UPDATE bookings
SET status = 'COMPLETED'
WHERE status = 'CONFIRMED' 
  AND timeSlot.date < TODAY_MIDNIGHT_UTC

3. TimeSlotService

Location: packages/server/src/time-slot/time-slot.service.ts

3.1 Service Overview

Handles time slot queries and management for both members and admins.

3.2 Key Methods

getAvailableSlots(date: string, userId?: string): Promise<TimeSlotWithBookingStatus[]>

Purpose: Retrieve all non-closed slots for a specific date, optionally including user's booking status.

Query Logic:

1. Parse date string to Date object
2. Find all slots for that calendar day:
   - WHERE status != CLOSED
   - ORDER BY startTime ASC
3. If userId provided:
   - Include bookings where userId=X AND status=CONFIRMED
   - Map to "isBookedByMe" and "myBookingId" fields
4. Return TimeSlotWithBookingStatus[]

Response Type:

interface TimeSlotWithBookingStatus {
  id: string
  date: string                    // ISO date "YYYY-MM-DD"
  startTime: string               // "HH:mm"
  endTime: string
  capacity: number
  bookedCount: number
  status: TimeSlotStatus          // OPEN | FULL | CLOSED
  source: TimeSlotSource          // TEMPLATE | MANUAL
  templateId: string | null
  createdAt: string               // ISO datetime
  updatedAt: string
  isBookedByMe: boolean           // Current user's booking?
  myBookingId: string | null      // For cancellation
}

getSlotById(id: string): Promise<TimeSlot>

Returns full slot details including all bookings. Throws NotFoundException if not found.


createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot>

Purpose: Allow admins to create one-off time slots outside templates.

DTO:

class CreateManualSlotDto {
  date: string              // "YYYY-MM-DD"
  startTime: string         // "HH:mm"
  endTime: string           // "HH:mm"
  capacity?: number         // Defaults to DEFAULT_SLOT_CAPACITY (1)
}

Creates slot with:

  • source: TimeSlotSource.MANUAL
  • templateId: null

closeSlot(id: string): Promise<TimeSlot>

Sets slot status to CLOSED. Prevents new bookings but keeps existing ones.


getWeekTemplates(): Promise<WeekTemplate[]>

Lists all templates ordered by dayOfWeek and startTime.


replaceWeekTemplates(items: WeekTemplateItemDto[]): Promise<CreateBatchPayload>

Purpose: Atomic replacement of all templates (used during admin config).

Transaction:

1. DELETE FROM week_templates (all rows)
2. CREATE week_templates with new items
3. Return count

4. TimeSlotController & AdminTimeSlotController

Location: packages/server/src/time-slot/time-slot.controller.ts

4.1 Member Endpoints

GET /time-slot/available?date=YYYY-MM-DD

  • Returns available slots for the date
  • Includes current user's booking status
  • Requires JWT authentication

GET /time-slot/:id

  • Returns full slot details with all bookings
  • Requires JWT authentication

4.2 Admin Endpoints

All require @Roles(UserRole.ADMIN) and JWT auth.

GET /admin/week-template

Lists all WeekTemplate entries.

PUT /admin/week-template

Replaces all templates. Request body:

{
  "templates": [
    {
      "dayOfWeek": 1,
      "startTime": "09:00",
      "endTime": "10:00",
      "capacity": 1,
      "isActive": true
    }
  ]
}

POST /admin/time-slot/manual

Creates a manual slot. Request body:

{
  "date": "2026-04-10",
  "startTime": "14:00",
  "endTime": "15:00",
  "capacity": 2
}

PUT /admin/time-slot/:id/close

Closes a specific slot.

POST /admin/generate-slots

Manually trigger slot generation (default 14 days ahead).


5. SchedulerService - Automated Jobs

Location: packages/server/src/scheduler/scheduler.service.ts

5.1 Overview

Uses @nestjs/schedule to run daily maintenance tasks. All times in UTC.

5.2 Cron Jobs

Job 1: Slot Generation

@Cron('0 2 * * *')  // 02:00 UTC daily
async handleSlotGeneration()
  • Calls: slotGenerator.generateSlots(14)
  • Generates slots 14 days ahead
  • Purpose: Keep pipeline filled

Job 2: Slot Cleanup

@Cron('30 2 * * *')  // 02:30 UTC daily
async handleCleanupSlots()
  • Calls: slotGenerator.cleanupExpiredSlots()
  • Marks past OPEN slots as CLOSED

Job 3: Membership Check

@Cron('0 3 * * *')   // 03:00 UTC daily
async handleCheckMemberships()
  • Calls: slotGenerator.checkExpiredMemberships()
  • Expires memberships by date or used-up sessions

Job 4: Booking Completion

@Cron('0 22 * * *')  // 22:00 UTC daily
async handleCompleteBookings()
  • Calls: slotGenerator.completeBookings()
  • Marks past CONFIRMED bookings as COMPLETED

6. BookingService - Integration with TimeSlots

Location: packages/server/src/booking/booking.service.ts

6.1 Key Integration Points

createBooking(userId: string, dto: CreateBookingDto): Promise<BookingWithRelations>

DTO:

class CreateBookingDto {
  timeSlotId: string   // UUID of TimeSlot
  membershipId: string // UUID of Membership
}

Transaction Flow:

1. Fetch TimeSlot - validate status = OPEN
2. Check unique constraint - user not already booked this slot
3. Fetch Membership - validate:
   - Belongs to user
   - Status = ACTIVE
   - Has remaining capacity:
     * TIMES/TRIAL: remainingTimes > 0
     * DURATION: not expired
4. Create Booking(userId, timeSlotId, membershipId) → CONFIRMED
5. Update TimeSlot:
   - bookedCount++
   - If bookedCount >= capacity, set status = FULL
6. Update Membership (if time-based):
   - remainingTimes--
   - If remainingTimes = 0, set status = USED_UP
7. Return booking with relations

Error Handling:

  • TimeSlot not OPEN → BadRequestException
  • Duplicate booking → ConflictException
  • Invalid membership → ForbiddenException
  • No remaining sessions → BadRequestException

cancelBooking(userId: string, bookingId: string): Promise<CancelBookingResult>

Refund Logic:

cancelHoursLimit = StudioConfig.cancelHoursLimit (default 2 hours)
slotStartMs = Date(date).setUTC Hours + startTime
deadlineMs = NOW + (cancelHoursLimit * 3600 * 1000)
withinLimit = slotStartMs >= deadlineMs

IF withinLimit:
  Restore membership.remainingTimes++
ELSE:
  No refund

Transaction Flow:

1. Mark Booking → CANCELLED, set cancelledAt
2. Decrement TimeSlot.bookedCount
3. If slot was FULL, restore to OPEN
4. If within cancel window:
   - For TIMES/TRIAL: increment remainingTimes
   - Restore membership status if was USED_UP

getMyBookings(userId: string, status?, page, limit): Promise<PaginatedResult>

Lists user's bookings with pagination, optionally filtered by status.


getUpcomingBookings(userId: string): Promise<BookingWithRelations[]>

Returns all CONFIRMED bookings for dates >= today, ordered by date.


7. Data Flow Diagrams

7.1 Slot Generation Flow

Daily 02:00 UTC
       ↓
SchedulerService.handleSlotGeneration()
       ↓
SlotGeneratorService.generateSlots(14)
       ↓
1. Query WeekTemplate (isActive=true)
2. For next 14 days:
   - Match templates by ISO weekday
   - Create TimeSlot entries
3. Use createMany(skipDuplicates: true)
       ↓
Database: Insert new TimeSlot records
       ↓
Return: count of new slots

7.2 Booking Flow

User Action
       ↓
POST /booking
  timeSlotId: UUID
  membershipId: UUID
       ↓
BookingService.createBooking()
       ↓
START TRANSACTION
  ├─ Validate TimeSlot (status=OPEN)
  ├─ Check unique(userId, timeSlotId)
  ├─ Validate Membership (ACTIVE, not expired)
  ├─ CREATE Booking(CONFIRMED)
  ├─ UPDATE TimeSlot(bookedCount++, status=?)
  └─ UPDATE Membership(remainingTimes--)
COMMIT
       ↓
Return: BookingWithRelations

7.3 Cancellation Flow

User Action
       ↓
PUT /booking/:id/cancel
       ↓
BookingService.cancelBooking()
       ↓
Check: Now vs Slot Time + cancelHoursLimit
       ↓
START TRANSACTION
  ├─ UPDATE Booking(CANCELLED, cancelledAt=NOW)
  ├─ UPDATE TimeSlot(bookedCount--, status=?)
  └─ IF within cancel window:
     └─ UPDATE Membership(remainingTimes++)
COMMIT
       ↓
Return: { booking, refunded: boolean }

8. DTOs & Request/Response

8.1 Time Slot DTOs

Location: packages/server/src/time-slot/dto/

QuerySlotsDto

class QuerySlotsDto {
  @IsDateString()
  date!: string  // Format: YYYY-MM-DD
}

CreateManualSlotDto

class CreateManualSlotDto {
  @IsDateString()
  date!: string
  @IsString()
  startTime!: string
  @IsString()
  endTime!: string
  @IsOptional()
  @IsInt()
  @Min(1)
  capacity?: number
}

WeekTemplateItemDto & UpdateWeekTemplateDto

class WeekTemplateItemDto {
  @IsInt()
  @Min(1)
  @Max(7)
  dayOfWeek!: number  // ISO: 1=Mon, 7=Sun
  @IsString()
  startTime!: string
  @IsString()
  endTime!: string
  @IsOptional()
  capacity?: number
  @IsOptional()
  isActive?: boolean
}

class UpdateWeekTemplateDto {
  @ArrayNotEmpty()
  templates!: WeekTemplateItemDto[]
}

9. Shared Constants & Enums

Location: packages/shared/src/

9.1 Constants

// constants.ts
export const DEFAULT_CANCEL_HOURS_LIMIT = 2
export const DEFAULT_SLOT_CAPACITY = 1
export const SLOT_GENERATION_DAYS = 14
export const TIME_PERIODS = {
  MORNING: { label: '上午', start: '06:00', end: '12:00' },
  AFTERNOON: { label: '下午', start: '12:00', end: '18:00' },
  EVENING: { label: '晚上', start: '18:00', end: '22:00' },
}
export const DATE_SELECTOR_DAYS = 7
export const WEEKDAY_LABELS = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']

9.2 Enums

// enums.ts
enum TimeSlotStatus {
  OPEN = 'OPEN',
  FULL = 'FULL',
  CLOSED = 'CLOSED',
}

enum TimeSlotSource {
  TEMPLATE = 'TEMPLATE',
  MANUAL = 'MANUAL',
}

enum BookingStatus {
  CONFIRMED = 'CONFIRMED',
  CANCELLED = 'CANCELLED',
  COMPLETED = 'COMPLETED',
  NO_SHOW = 'NO_SHOW',
}

enum MembershipStatus {
  ACTIVE = 'ACTIVE',
  EXPIRED = 'EXPIRED',
  USED_UP = 'USED_UP',
}

10. File Structure Summary

packages/server/src/
├── time-slot/
│   ├── __tests__/
│   │   ├── slot-generator.service.spec.ts  (170 lines, comprehensive tests)
│   │   └── time-slot.service.spec.ts
│   ├── dto/
│   │   ├── query-slots.dto.ts
│   │   ├── create-manual-slot.dto.ts
│   │   └── week-template.dto.ts
│   ├── slot-generator.service.ts           (172 lines, 4 key methods)
│   ├── time-slot.service.ts                (142 lines)
│   ├── time-slot.controller.ts             (93 lines, 2 controllers)
│   └── time-slot.module.ts
│
├── scheduler/
│   ├── __tests__/
│   │   └── scheduler.service.spec.ts
│   ├── scheduler.service.ts                (55 lines, 4 cron jobs)
│   └── scheduler.module.ts
│
├── booking/
│   ├── __tests__/
│   │   └── booking.service.spec.ts
│   ├── dto/
│   │   └── create-booking.dto.ts
│   ├── booking.service.ts                  (367 lines)
│   ├── booking.controller.ts               (82 lines)
│   └── booking.module.ts
│
├── prisma/
│   └── schema.prisma                       (205 lines, includes models)
│
└── app.module.ts                           (imports TimeSlotModule, SchedulerModule)

packages/shared/src/
├── types/
│   ├── time-slot.ts
│   └── (others)
├── constants.ts                            (22 lines)
├── enums.ts                                (47 lines)
└── index.ts

11. Key Architectural Patterns

11.1 Idempotent Slot Generation

Problem: If scheduler crashes or delays, slots might not be generated. Solution:

  • Use createMany(skipDuplicates: true) with unique constraint on [date, startTime, endTime]
  • Safe to re-run multiple times
  • Only inserts new combinations

11.2 Atomic Transactions

For Booking Creation:

  • Create booking, update slot, update membership in single transaction
  • All-or-nothing: ensures consistency if any step fails

For Cancellation:

  • Cancel booking, restore slot, conditionally restore membership
  • Prevents race conditions

11.3 ISO Weekday Mapping

Problem: JavaScript Date.getDay() uses 0=Sunday, but WeekTemplate uses ISO 8601 (1=Monday).

Solution: Helper function toIsoWeekday():

function toIsoWeekday(jsDay: number): number {
  return jsDay === 0 ? 7 : jsDay
}

11.4 Membership Type Handling

TIMES/TRIAL cardType:

  • Deduct remainingTimes-- on booking
  • Mark USED_UP when remainingTimes = 0
  • Refund if cancelled within window

DURATION cardType:

  • Check expireDate not passed
  • No deduction; just check validity
  • No refund on cancellation

12. Example Scenarios

Scenario 1: Setup Studio with Mon-Fri Classes

Admin Actions:

PUT /admin/week-template
{
  "templates": [
    { "dayOfWeek": 1, "startTime": "09:00", "endTime": "10:00", "capacity": 1 },
    { "dayOfWeek": 1, "startTime": "10:30", "endTime": "11:30", "capacity": 1 },
    { "dayOfWeek": 2, "startTime": "09:00", "endTime": "10:00", "capacity": 1 },
    { "dayOfWeek": 3, "startTime": "09:00", "endTime": "10:00", "capacity": 1 },
    { "dayOfWeek": 4, "startTime": "09:00", "endTime": "10:00", "capacity": 1 },
    { "dayOfWeek": 5, "startTime": "09:00", "endTime": "10:00", "capacity": 1 },
    { "dayOfWeek": 5, "startTime": "18:00", "endTime": "19:00", "capacity": 1 }
  ]
}

Next Day (02:00 UTC):

  • Scheduler auto-generates 14 days of slots
  • Result: 14 Mon morning + 14 Mon mid-morning + 14 Tue morning + ... + 14 Fri evening

Member Action (View Availability):

GET /time-slot/available?date=2026-04-10
→ Returns all slots for April 10 (Friday)
→ Includes bookings for current user

Scenario 2: Member Books, Then Cancels

Member Books:

POST /booking
{
  "timeSlotId": "slot-123",
  "membershipId": "mem-456"
}

System:

  1. Validates slot is OPEN, membership is ACTIVE with remaining sessions
  2. Creates Booking(CONFIRMED)
  3. Increments slot.bookedCount (1 → 2)
  4. If now at capacity, sets slot.status = FULL
  5. Decrements membership.remainingTimes (5 → 4)

Member Cancels (within 2-hour window):

PUT /booking/booking-789/cancel

System:

  1. Checks if NOW + 2 hours ≤ slot start time ✓
  2. Sets booking.status = CANCELLED
  3. Decrements slot.bookedCount (2 → 1)
  4. If slot was FULL, restores to OPEN
  5. Increments membership.remainingTimes (4 → 5) ✓ refunded

Scenario 3: Membership Expires

Overnight at 03:00 UTC:

  • Scheduler runs handleCheckMemberships()
  • Updates all ACTIVE memberships where expireDate < NOW to EXPIRED
  • User tries to book → BadRequestException "Membership is not active (status: EXPIRED)"

13. Testing Guide

Key Test Files

  1. slot-generator.service.spec.ts (310 lines)

    • Tests slot generation from templates
    • Tests weekday mapping (JS vs ISO)
    • Tests cleanup and expiration logic
    • Tests membership and booking expiration
  2. time-slot.service.spec.ts (existing)

    • Tests getAvailableSlots with user booking status
    • Tests manual slot creation
  3. booking.service.spec.ts (existing)

    • Tests booking creation with all validations
    • Tests cancellation with refund logic

14. Configuration & Environment

Required Env Variables

DATABASE_URL=mysql://...

Studio Config (StudioConfig table)

  • cancelHoursLimit: Hours before slot to allow free cancellation (default 2)

Constants (shared package)

  • SLOT_GENERATION_DAYS: 14 (days ahead to generate)
  • DEFAULT_SLOT_CAPACITY: 1 (private lessons)
  • DEFAULT_CANCEL_HOURS_LIMIT: 2

15. Performance Considerations

Database Indexes

  • TimeSlot(date) - for date range queries
  • TimeSlot(status) - for status filtering
  • Booking(userId) - for user's bookings
  • Booking(status) - for status filtering

Batch Operations

  • Slot generation uses createMany() for efficiency
  • Expiration checks use updateMany() instead of loops

Transaction Isolation

  • All booking/cancellation operations wrapped in transactions
  • Prevents race conditions on bookedCount and remainingTimes

16. Security Notes

Authorization

  • JWT guard on all endpoints
  • RolesGuard for admin endpoints (only ADMIN role)
  • Users can only modify their own bookings/memberships

Validation

  • All DTOs have class-validator decorators
  • UUID validation on foreign keys
  • Date string validation (YYYY-MM-DD format)

Data Integrity

  • Unique constraint on [userId, timeSlotId] prevents duplicate bookings
  • Unique constraint on [date, startTime, endTime] prevents duplicate slots
  • Foreign key constraints on relations

17. Future Enhancement Ideas

  1. Overbooking Buffer:

    • Allow configurable overbooking ratio (e.g., 110% capacity)
  2. Waitlist Support:

    • Add BookingStatus.WAITLISTED
    • Auto-promote when slot opens
  3. Recurring Cancellation:

    • Cancel all future bookings of a series
    • Batch refunds
  4. Slot Availability Notifications:

    • Alert users when slots available
    • Implement notification queue
  5. Dynamic Pricing:

    • Peak vs off-peak pricing
    • Last-minute discounts

Summary

This time-slot and scheduling system is well-architected with:

Idempotent slot generation - Safe to re-run Atomic transactions - ACID compliance for bookings Automatic maintenance - 4 daily cron jobs Flexible membership types - TIMES, DURATION, TRIAL Refund policy - Configurable cancellation window ISO weekday standard - Proper international support Comprehensive validation - DTOs with decorators Role-based access - Admin vs member endpoints

The system handles:

  • Auto-generating 14 days of slots nightly
  • Accepting bookings with capacity management
  • Canceling with conditional refunds
  • Expiring memberships and marking past bookings
  • All with transactional integrity and concurrent safety.