# Time-Slot & Scheduling System - Quick Reference ## File Locations | Component | Path | |-----------|------| | **Slot Generator** | `packages/server/src/time-slot/slot-generator.service.ts` | | **TimeSlot Service** | `packages/server/src/time-slot/time-slot.service.ts` | | **TimeSlot Controller** | `packages/server/src/time-slot/time-slot.controller.ts` | | **Scheduler** | `packages/server/src/scheduler/scheduler.service.ts` | | **Booking Service** | `packages/server/src/booking/booking.service.ts` | | **Booking Controller** | `packages/server/src/booking/booking.controller.ts` | | **Database Schema** | `packages/server/prisma/schema.prisma` | | **Shared Constants** | `packages/shared/src/constants.ts` | | **Shared Enums** | `packages/shared/src/enums.ts` | --- ## Key Concepts ### WeekTemplate Defines **recurring class schedule** by day of week (1=Monday, 7=Sunday) and time. - Used to auto-generate TimeSlots nightly - Can be enabled/disabled - Has capacity (default 1 for private lessons) ### TimeSlot **Individual class instance** on a specific date with a specific time. - Status: OPEN → FULL → CLOSED - Source: TEMPLATE (auto-generated) or MANUAL (admin-created) - Cannot have duplicates (unique constraint on date+startTime+endTime) ### Booking **User's reservation** for a specific TimeSlot. - Status: CONFIRMED → COMPLETED (or CANCELLED) - Links user + timeSlot + membership - Unique constraint: one booking per user per slot --- ## Daily Scheduler Jobs All times in UTC: | Time | Job | What It Does | |------|-----|--------------| | **02:00** | `handleSlotGeneration()` | Generate slots 14 days ahead from WeekTemplates | | **02:30** | `handleCleanupSlots()` | Mark past OPEN slots as CLOSED | | **03:00** | `handleCheckMemberships()` | Expire memberships by date or used-up sessions | | **22:00** | `handleCompleteBookings()` | Mark past CONFIRMED bookings as COMPLETED | --- ## Important Methods ### SlotGeneratorService ```typescript // Generate N days of slots from WeekTemplates generateSlots(daysAhead = 14): Promise // Close all past OPEN slots cleanupExpiredSlots(): Promise // Expire memberships by date or session count checkExpiredMemberships(): Promise // Mark past bookings as COMPLETED completeBookings(): Promise ``` ### TimeSlotService ```typescript // Get all slots for a date (with user's booking status if provided) getAvailableSlots(date: string, userId?: string): Promise // Manually create a one-off slot createManualSlot(dto: CreateManualSlotDto): Promise // Close a slot (prevent new bookings) closeSlot(id: string): Promise // Get/replace weekly templates getWeekTemplates(): Promise replaceWeekTemplates(items: WeekTemplateItemDto[]): Promise ``` ### BookingService ```typescript // Create a booking (validates slot/membership, updates counts) createBooking(userId: string, dto: CreateBookingDto): Promise // Cancel a booking (conditionally refunds membership) cancelBooking(userId: string, bookingId: string): Promise // Get user's bookings (paginated, filterable by status) getMyBookings(userId: string, status?, page, limit): Promise // Get all CONFIRMED bookings for dates >= today getUpcomingBookings(userId: string): Promise ``` --- ## API Endpoints ### Member Endpoints ``` GET /time-slot/available?date=2026-04-10 → Returns slots for that date with user's booking status GET /time-slot/:id → Returns full slot details with all bookings POST /booking Body: { "timeSlotId": "uuid", "membershipId": "uuid" } → Create a booking PUT /booking/:id/cancel → Cancel a booking (refund if within window) GET /booking/my?status=CONFIRMED&page=1&limit=10 → Get user's bookings (paginated) GET /booking/my/upcoming → Get all upcoming CONFIRMED bookings ``` ### Admin Endpoints ``` GET /admin/week-template → List all templates PUT /admin/week-template Body: { "templates": [ {...}, {...} ] } → Replace all templates (atomic) POST /admin/time-slot/manual Body: { "date", "startTime", "endTime", "capacity" } → Create a one-off slot PUT /admin/time-slot/:id/close → Close a slot POST /admin/generate-slots → Manually trigger slot generation GET /admin/bookings?page=1&limit=10&status=CONFIRMED → View all bookings (admin) ``` --- ## Status Values ### TimeSlotStatus - **OPEN**: Accepts bookings (bookedCount < capacity) - **FULL**: At capacity (bookedCount >= capacity) - **CLOSED**: Past date or manually closed ### BookingStatus - **CONFIRMED**: Active reservation - **CANCELLED**: User cancelled - **COMPLETED**: Slot time has passed - **NO_SHOW**: Marked manually ### MembershipStatus - **ACTIVE**: Valid for booking - **EXPIRED**: End date passed - **USED_UP**: No remaining sessions (for TIMES/TRIAL) ### CardTypeCategory - **TIMES**: N sessions (e.g., "5-pack") - **DURATION**: Valid for X days (e.g., "1-month") - **TRIAL**: Free trial sessions --- ## Key Logic ### Booking Creation Transaction ``` 1. Validate TimeSlot exists and status = OPEN 2. Check user not already booked this slot 3. Validate Membership: - Belongs to user - Status = ACTIVE - Has capacity: * TIMES/TRIAL: remainingTimes > 0 * DURATION: expireDate > NOW 4. CREATE Booking(CONFIRMED) 5. UPDATE TimeSlot: - bookedCount++ - IF bookedCount >= capacity THEN status = FULL 6. UPDATE Membership (if time-based): - remainingTimes-- - IF remainingTimes = 0 THEN status = USED_UP 7. Return with relations ``` ### Cancellation Refund Logic ``` cancelHoursLimit = 2 (configurable in StudioConfig) slotStartTime = TimeSlot.date + TimeSlot.startTime deadline = NOW + (cancelHoursLimit * hours) IF slotStartTime >= deadline: Refund = TRUE Increment membership.remainingTimes ELSE: Refund = FALSE No membership change ``` --- ## Weekday Mapping **ISO Standard** (what WeekTemplate uses): ``` 1 = Monday 2 = Tuesday 3 = Wednesday 4 = Thursday 5 = Friday 6 = Saturday 7 = Sunday ``` **JavaScript getDay()** (what Date does): ``` 0 = Sunday 1 = Monday 2 = Tuesday ... 6 = Saturday ``` **Conversion function:** ```typescript function toIsoWeekday(jsDay: number): number { return jsDay === 0 ? 7 : jsDay } ``` --- ## Database Constraints ### TimeSlot - Unique: `[date, startTime, endTime]` - prevents duplicate slots - Index: `date` - for date range queries - Index: `status` - for filtering ### Booking - Unique: `[userId, timeSlotId]` - one booking per user per slot - Index: `userId` - for user's bookings - Index: `status` - for status filtering --- ## Configuration ### Environment Variables ``` DATABASE_URL=mysql://... (required) ``` ### From StudioConfig Table ``` cancelHoursLimit = 2 (hours before slot to allow free cancellation) ``` ### From Shared Constants ``` DEFAULT_SLOT_CAPACITY = 1 SLOT_GENERATION_DAYS = 14 DEFAULT_CANCEL_HOURS_LIMIT = 2 ``` --- ## Common Errors | Error | Cause | Solution | |-------|-------|----------| | TimeSlot not found | Invalid slot ID | Check slot exists | | TimeSlot is not available | Status ≠ OPEN | Slot is FULL or CLOSED | | You have already booked this slot | Duplicate booking | Check user's bookings | | This membership does not belong to you | Membership not user's | Verify membership | | Membership is not active | Status ≠ ACTIVE | Renew or purchase membership | | No remaining times on this membership | remainingTimes ≤ 0 | Purchase more sessions | | Membership has expired | expireDate < NOW | Renew membership | | Cannot cancel booking with status | Status ≠ CONFIRMED | Can only cancel CONFIRMED bookings | --- ## Testing Run tests with: ```bash npm test -- slot-generator.service.spec.ts npm test -- booking.service.spec.ts npm test -- time-slot.service.spec.ts ``` Key test areas: - Slot generation from templates - Weekday mapping (JS vs ISO) - Booking creation with all validations - Cancellation with/without refund - Membership expiration --- ## Performance Tips 1. **Avoid N+1 queries** - Always include relations in findMany 2. **Batch operations** - Use createMany/updateMany for large operations 3. **Transactions** - Wrap multi-step operations to prevent race conditions 4. **Indexes** - Queries filter by date and status (both indexed) --- ## Development Workflow 1. **Setup templates** → `PUT /admin/week-template` 2. **Manually trigger generation** → `POST /admin/generate-slots` 3. **View available slots** → `GET /time-slot/available?date=...` 4. **Create booking** → `POST /booking` 5. **Cancel booking** → `PUT /booking/:id/cancel` For testing without scheduler: ```typescript // Inject SlotGeneratorService and call directly const count = await slotGenerator.generateSlots(7) ``` --- ## Architecture Highlights ✅ **Idempotent** - Safe to re-run slot generation ✅ **Transactional** - Bookings are atomic ✅ **Automated** - 4 daily cron jobs maintain state ✅ **Flexible** - Supports multiple membership types ✅ **Scalable** - Batch operations, proper indexes ✅ **Validating** - DTO decorators + business logic checks