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>
This commit is contained in:
richarjiang
2026-04-05 12:18:49 +08:00
parent 9c5dd4a911
commit b6986ba30c
29 changed files with 7810 additions and 19 deletions

606
docs/TIME_SLOT_DIAGRAMS.md Normal file
View File

@@ -0,0 +1,606 @@
# Time-Slot & Scheduling System - Architecture Diagrams
## 1. Data Model Relationships
```
┌─────────────────────────────────────────────────────────────────┐
│ WEEK TEMPLATE │
│ │
│ dayOfWeek (1-7, ISO standard) │
│ startTime, endTime (e.g., "09:00", "10:00") │
│ capacity (default 1) │
│ isActive (can disable template) │
│ │
│ ↓ (auto-generates) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ TIME SLOT │
│ │
│ date (calendar date, midnight UTC) │
│ startTime, endTime (from template) │
│ capacity (from template) │
│ bookedCount (# of current bookings) │
│ status (OPEN | FULL | CLOSED) │
│ source (TEMPLATE | MANUAL) │
│ templateId (reference to WeekTemplate) │
│ │
│ ↓ (has many) ↓ (belongs to) │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↑ ↑
│ │
└────────────┬─────────────────────────
│ 1:1 booking
┌─────────────────────────────────────────────────────────────────┐
│ BOOKING │
│ │
│ userId (FK to User) │
│ timeSlotId (FK to TimeSlot) │
│ membershipId (FK to Membership) │
│ status (CONFIRMED | CANCELLED | COMPLETED | NO_SHOW) │
│ cancelledAt (timestamp when cancelled) │
│ │
│ Constraints: │
│ - Unique [userId, timeSlotId] (one booking per user per slot) │
│ - ONE booking per TimeSlot per user │
└─────────────────────────────────────────────────────────────────┘
↑ ↑
│ │
└─────────────┬───────────┘
belongs to
┌─────────────────────────────────────────────────────────────────┐
│ MEMBERSHIP │
│ │
│ userId (FK to User) │
│ cardTypeId (FK to CardType) │
│ remainingTimes (for TIMES/TRIAL card types) │
│ expireDate (for DURATION card types) │
│ status (ACTIVE | EXPIRED | USED_UP) │
└─────────────────────────────────────────────────────────────────┘
```
---
## 2. Daily Scheduler Timeline
```
00:00 ─────────────────────────────────────────────────
├─ [Midnight] - Time passes
├─ System running in background
├─ ... (various other operations)
02:00 ─────────────────────────────────────────────────
├─► 🟢 SLOT GENERATION
│ SlotGeneratorService.generateSlots(14)
│ ├─ Query WeekTemplate (all isActive=true)
│ ├─ For each day in [tomorrow, tomorrow+14):
│ │ ├─ Get ISO weekday
│ │ ├─ Find matching templates
│ │ └─ Create TimeSlot entries
│ ├─ Batch insert with skipDuplicates: true
│ └─ Log: "Generated X new time slots"
02:30 ─────────────────────────────────────────────────
├─► 🟡 SLOT CLEANUP
│ SlotGeneratorService.cleanupExpiredSlots()
│ ├─ Find all OPEN slots with date < TODAY
│ ├─ Mark as CLOSED
│ └─ Log: "Closed X expired time slots"
03:00 ─────────────────────────────────────────────────
├─► 🟠 MEMBERSHIP CHECK
│ SlotGeneratorService.checkExpiredMemberships()
│ ├─ Update ACTIVE memberships with expireDate < NOW
│ │ └─ Set status = EXPIRED
│ ├─ Update ACTIVE memberships with remainingTimes = 0
│ │ └─ Set status = USED_UP
│ └─ Log: "Expired X by date, Y by sessions"
├─ ... (users awake, making bookings)
22:00 ─────────────────────────────────────────────────
├─► 🔴 BOOKING COMPLETION
│ SlotGeneratorService.completeBookings()
│ ├─ Find CONFIRMED bookings with timeSlot.date < TODAY
│ ├─ Mark as COMPLETED
│ └─ Log: "Completed X past bookings"
└─ (Day ends, repeat tomorrow)
```
---
## 3. Booking Lifecycle
```
┌─────────────────────────────────────────────────────────┐
│ BOOKING CREATION │
│ (POST /booking) │
└─────────────────────────────────────────────────────────┘
├─ Input: { timeSlotId, membershipId }
├─ TRANSACTION START ──────────────────
│ │
│ ├─► Fetch TimeSlot
│ │ └─ Check: status = OPEN? ✓
│ │
│ ├─► Check Duplicate
│ │ └─ Query: SELECT * FROM bookings WHERE userId=? AND timeSlotId=?
│ │ └─ Must not exist
│ │
│ ├─► Fetch Membership
│ │ └─ Check: belongs to user? ✓
│ │ └─ Check: status = ACTIVE? ✓
│ │ └─ Check: has capacity?
│ │ └─ IF TIMES/TRIAL: remainingTimes > 0? ✓
│ │ └─ IF DURATION: expireDate > NOW? ✓
│ │
│ ├─► CREATE Booking(CONFIRMED)
│ │ └─ INSERT: { userId, timeSlotId, membershipId, status: CONFIRMED }
│ │
│ ├─► UPDATE TimeSlot
│ │ ├─ bookedCount = bookedCount + 1
│ │ ├─ IF bookedCount >= capacity:
│ │ │ └─ status = FULL
│ │ └─ ELSE:
│ │ └─ status = OPEN (unchanged)
│ │
│ ├─► UPDATE Membership (if TIMES/TRIAL)
│ │ ├─ remainingTimes = remainingTimes - 1
│ │ ├─ IF remainingTimes <= 0:
│ │ │ └─ status = USED_UP
│ │ └─ ELSE:
│ │ └─ status = ACTIVE (unchanged)
│ │
│ └─ TRANSACTION COMMIT ──────────────
└─► Return: BookingWithRelations (includes timeSlot, membership)
┌─────────────────────────────────────────────────────────┐
│ BOOKING CANCELLATION │
│ (PUT /booking/:id/cancel) │
└─────────────────────────────────────────────────────────┘
├─ Fetch Booking + TimeSlot + Membership
├─ Check: booking.status = CONFIRMED? ✓
├─ Calculate Refund Eligibility
│ │
│ ├─ cancelHoursLimit = StudioConfig.cancelHoursLimit (default 2)
│ ├─ slotStartMs = Date(timeSlot.date) + timeSlot.startTime
│ ├─ deadlineMs = NOW + (cancelHoursLimit * 3600 * 1000)
│ │
│ ├─ IF slotStartMs >= deadlineMs:
│ │ └─ withinLimit = TRUE ✓ (User gets refund)
│ └─ ELSE:
│ └─ withinLimit = FALSE (No refund)
├─ TRANSACTION START ──────────────────
│ │
│ ├─► UPDATE Booking
│ │ ├─ status = CANCELLED
│ │ └─ cancelledAt = NOW
│ │
│ ├─► UPDATE TimeSlot
│ │ ├─ bookedCount = MAX(0, bookedCount - 1)
│ │ ├─ IF slot was FULL:
│ │ │ └─ status = OPEN
│ │ └─ ELSE:
│ │ └─ status = (unchanged)
│ │
│ ├─► IF withinLimit = TRUE:
│ │ └─ UPDATE Membership (if TIMES/TRIAL)
│ │ ├─ remainingTimes = remainingTimes + 1
│ │ ├─ IF was USED_UP:
│ │ │ └─ status = ACTIVE
│ │ └─ ELSE:
│ │ └─ status = (unchanged)
│ │
│ └─ TRANSACTION COMMIT ──────────────
└─► Return: { booking, refunded: boolean }
```
---
## 4. Slot Generation from Template
```
Template Setup:
┌─────────────────────────────────────────────────────┐
│ PUT /admin/week-template │
│ │
│ { │
│ "templates": [ │
│ { │
│ "dayOfWeek": 1, // Monday (ISO standard)│
│ "startTime": "09:00", │
│ "endTime": "10:00", │
│ "capacity": 1, // Private lesson │
│ "isActive": true │
│ }, │
│ { │
│ "dayOfWeek": 5, // Friday (ISO standard)│
│ "startTime": "18:00", │
│ "endTime": "19:00", │
│ "capacity": 1, │
│ "isActive": true │
│ } │
│ ] │
│ } │
└─────────────────────────────────────────────────────┘
Stored in database as WeekTemplates
Each day at 02:00 UTC, generateSlots(14) runs:
Today: Monday, April 7, 2026
├─ Tomorrow = Tuesday, April 8
├─ For next 14 days:
│ Day 0: Tue (ISO 2) → no matching template → skip
│ Day 1: Wed (ISO 3) → no matching template → skip
│ Day 2: Thu (ISO 4) → no matching template → skip
│ Day 3: Fri (ISO 5) → MATCH! template (18:00-19:00)
│ └─ CREATE TimeSlot(date=Apr12, time=18:00-19:00, capacity=1)
│ Day 4: Sat (ISO 6) → no matching template → skip
│ Day 5: Sun (ISO 7) → no matching template → skip
│ Day 6: Mon (ISO 1) → MATCH! template (09:00-10:00)
│ └─ CREATE TimeSlot(date=Apr14, time=09:00-10:00, capacity=1)
│ Day 7: Tue (ISO 2) → no matching template → skip
│ ... (repeats pattern)
└─ All created with:
├─ status = OPEN
├─ bookedCount = 0
├─ source = TEMPLATE
├─ templateId = (reference to template)
└─ skipDuplicates = true (safe to re-run)
Result:
14 Friday 18:00-19:00 slots generated
14 Monday 09:00-10:00 slots generated
Total: 28 new slots
```
---
## 5. User Booking Flow (Frontend → Backend)
```
┌──────────────────────────────────────────────────────┐
│ MEMBER CLIENT │
└──────────────────────────────────────────────────────┘
│ 1. Click "View Available Slots"
├─► GET /time-slot/available?date=2026-04-10
│ Response: [{
│ id: "slot-123",
│ date: "2026-04-10",
│ startTime: "09:00",
│ endTime: "10:00",
│ status: "OPEN",
│ bookedCount: 0,
│ capacity: 1,
│ isBookedByMe: false, ← User's booking status
│ myBookingId: null
│ }, ...]
├─ Display available slots in UI
│ 2. User selects slot and membership
├─► POST /booking
│ Body: {
│ "timeSlotId": "slot-123",
│ "membershipId": "mem-456"
│ }
│ Response: {
│ id: "booking-789",
│ userId: "user-001",
│ timeSlotId: "slot-123",
│ status: "CONFIRMED",
│ createdAt: "2026-04-05T10:30:00Z",
│ timeSlot: { ... }, ← Full slot details
│ membership: { ... } ← Full membership details
│ }
├─ Display confirmation
│ 3. [Later] User cancels booking
└─► PUT /booking/booking-789/cancel
Response: {
booking: { ... },
refunded: true ← Was refund issued?
}
Display: "Booking cancelled. You've been refunded."
```
---
## 6. State Transitions
### TimeSlot Status
```
┌─────────────────────────────────┐
│ AUTO-GENERATED │
│ by generateSlots() │
└─────────────┬───────────────────┘
┌─────────────────┐
│ OPEN │ ← Can accept bookings
│ (bookedCount < │ bookedCount starts at 0
│ capacity) │
└────────┬────────┘
┌──────────┼──────────┐
│ │ │
│ │ │
[booking │ [cleanup
creates] │ or manual
│ close]
│ │ │
↓ ↓ ↓
FULL CLOSED
(bookedCount >= capacity)
│ [booking cancelled]
OPEN (back to)
Once slot date passes:
├─ OPEN → CLOSED (by cleanup job at 02:30 UTC)
├─ FULL → CLOSED (when cleanup runs)
└─ CANCELLED bookings don't affect slot status
```
### Booking Status
```
┌──────────────────┐
│ CONFIRMED │ ← Default when created
│ │ User has active reservation
└────────┬─────────┘
┌─────┼─────┐
│ │ │
[user │ [auto-mark
cancels] │ when date
│ │ passes]
│ │ │
↓ ↓ ↓
CANCELLED COMPLETED
(free (slot time
cancellation has passed)
until deadline)
CANCELLED bookings stay in history
COMPLETED bookings show in past bookings
CONFIRMED bookings show in upcoming bookings
```
### Membership Status
```
┌─────────────────┐
│ ACTIVE │ ← Can book classes
│ │ Has remaining capacity
└────────┬────────┘
┌─────┼─────┐
│ │ │
[booking │ [auto-check
depletes │ by scheduler
sessions] │ at 03:00 UTC]
│ │ │
↓ ↓ ↓
USED_UP EXPIRED
(for (for
TIMES) DURATION)
USED_UP: remainingTimes = 0 (for TIMES/TRIAL only)
EXPIRED: expireDate < NOW (for DURATION) OR date-based expiry
All non-ACTIVE statuses prevent new bookings
```
---
## 7. Timezone & Date Handling
```
User Timezone: Local (browser/app)
API Timezone: UTC (backend)
Database: UTC
┌──────────────────────────────────────────────┐
│ User in Shanghai (UTC+8) │
│ Local time: 2026-04-10 15:00:00 CST │
│ UTC time: 2026-04-10 07:00:00 UTC │
└──────────────────────────────────────────────┘
├─ Query: GET /time-slot/available?date=2026-04-10
│ (User sends local date, frontend converts to ISO)
├─ Backend receives:
│ ├─ Parse "2026-04-10"
│ ├─ Build start of day: 2026-04-10T00:00:00 UTC
│ ├─ Build end of day: 2026-04-10T23:59:59.999 UTC
│ ├─ Query TimeSlots WHERE date BETWEEN [00:00, 23:59]
└─ Return slots for that calendar day in UTC
┌──────────────────────────────────────────────┐
│ TimeSlot Storage (Database) │
│ │
│ date: 2026-04-10 (DATE type, midnight UTC) │
│ startTime: "09:00" (string, no timezone) │
│ endTime: "10:00" (string, no timezone) │
│ │
│ When combined: │
│ Slot datetime = 2026-04-10T09:00:00 UTC │
└──────────────────────────────────────────────┘
├─ For Shanghai user (UTC+8):
│ └─ 09:00 UTC = 17:00 CST (5 PM)
└─ For New York user (UTC-4):
└─ 09:00 UTC = 05:00 EDT (5 AM)
Scheduler (UTC times):
┌─────────────────────────────────────────────┐
│ 02:00 UTC = Generate slots │
│ 02:30 UTC = Cleanup │
│ 03:00 UTC = Check memberships │
│ 22:00 UTC = Complete bookings │
│ │
│ When scheduler checks "is date < today?": │
│ ├─ Create midnight UTC boundary │
│ ├─ Compare slot.date < today's midnight │
│ └─ Mark as CLOSED/COMPLETED if older │
└─────────────────────────────────────────────┘
```
---
## 8. Error Handling Tree
```
POST /booking
├─ TimeSlot not found
│ └─ Return: NotFoundException
├─ TimeSlot.status ≠ OPEN
│ └─ Return: BadRequestException("TimeSlot is not available")
├─ Duplicate booking exists
│ └─ Return: ConflictException("Already booked this slot")
├─ Membership not found
│ └─ Return: NotFoundException
├─ Membership.userId ≠ current user
│ └─ Return: ForbiddenException("Not your membership")
├─ Membership.status ≠ ACTIVE
│ └─ Return: BadRequestException("Membership inactive")
├─ Card type is TIMES/TRIAL:
│ │
│ └─ remainingTimes ≤ 0
│ └─ Return: BadRequestException("No remaining times")
└─ Card type is DURATION:
└─ expireDate < NOW
└─ Return: BadRequestException("Membership expired")
PUT /booking/:id/cancel
├─ Booking not found
│ └─ Return: NotFoundException
├─ Booking.userId ≠ current user
│ └─ Return: ForbiddenException("Not your booking")
├─ Booking.status ≠ CONFIRMED
│ └─ Return: BadRequestException("Can't cancel this status")
└─ ✓ Cancel successful
├─ Check refund eligibility
├─ Update booking status
├─ Update timeSlot bookedCount
└─ Conditionally refund membership
```
---
## 9. Integration Points
```
┌─────────────────────────────────────────────────────┐
│ APP MODULE │
│ (packages/server/src/app.module.ts) │
└─────────────────────────────────────────────────────┘
├─ imports: [
│ AuthModule,
│ TimeSlotModule, ← Time-Slot logic
│ SchedulerModule, ← Auto jobs (cron)
│ BookingModule, ← Booking logic
│ MembershipModule, ← Membership checks
│ StudioModule, ← Config (cancelHoursLimit)
│ ...
│ ]
└─ Controllers route to:
├─ TimeSlotController (public slots viewing)
├─ AdminTimeSlotController (templates, admin actions)
├─ BookingController (create, cancel bookings)
└─ ... (other endpoints)
SchedulerModule dependencies:
├─ ScheduleModule.forRoot() ← Enable @Cron decorators
└─ TimeSlotModule ← Access to SlotGeneratorService
BookingModule dependencies:
├─ MembershipModule ← Check membership status
└─ StudioModule ← Read cancelHoursLimit config
Services call chain:
├─ Controller
│ ├─ TimeSlotService
│ │ └─ PrismaService
│ └─ BookingService
│ ├─ PrismaService
│ ├─ MembershipService
│ └─ StudioService
```

364
docs/TIME_SLOT_INDEX.md Normal file
View File

@@ -0,0 +1,364 @@
# Time-Slot & Scheduling System - Documentation Index
This directory contains comprehensive documentation of the NestJS backend time-slot and scheduling system for the pilates studio booking platform.
## 📚 Documentation Files
### 1. **TIME_SLOT_SCHEDULING_SYSTEM.md** (966 lines, 24KB)
**Most comprehensive reference** - Full system analysis with all details
**Contents:**
- Executive Summary
- Data Models (WeekTemplate, TimeSlot, Booking) with Prisma schema
- SlotGeneratorService (4 key methods: generateSlots, cleanupExpiredSlots, checkExpiredMemberships, completeBookings)
- TimeSlotService (queries and management)
- TimeSlotController & AdminTimeSlotController (all endpoints)
- SchedulerService (4 daily cron jobs at 02:00, 02:30, 03:00, 22:00 UTC)
- BookingService (integration with time slots)
- Data Flow Diagrams
- DTOs & Request/Response examples
- Shared Constants & Enums
- File Structure Summary
- Key Architectural Patterns
- Example Scenarios
- Testing Guide
- Configuration & Environment
- Performance Considerations
- Security Notes
- Future Enhancement Ideas
**When to use:** Deep dive into how the system works, understanding all components
---
### 2. **TIME_SLOT_QUICK_REFERENCE.md** (355 lines, 9KB)
**Quick lookup guide** - Essential information at a glance
**Contents:**
- File Locations (all key files in one table)
- Key Concepts (WeekTemplate, TimeSlot, Booking)
- Daily Scheduler Jobs (quick table with times and purposes)
- Important Methods (TypeScript signatures for all key methods)
- API Endpoints (member and admin endpoints with request/response)
- Status Values (all enum values explained)
- Key Logic (booking creation & cancellation flows in pseudocode)
- Weekday Mapping (ISO standard vs JavaScript)
- Database Constraints
- Configuration
- Common Errors (troubleshooting table)
- Testing
- Development Workflow
- Architecture Highlights
**When to use:** Quick lookup while coding, API reference, debugging errors
---
### 3. **TIME_SLOT_DIAGRAMS.md** (606 lines, 25KB)
**Visual references** - ASCII diagrams and flowcharts
**Contents:**
1. Data Model Relationships (entity diagram)
2. Daily Scheduler Timeline (24-hour cron schedule visualization)
3. Booking Lifecycle (detailed creation and cancellation flows)
4. Slot Generation from Template (step-by-step with example)
5. User Booking Flow (frontend → backend interaction)
6. State Transitions (TimeSlot, Booking, Membership status flows)
7. Timezone & Date Handling (UTC, local time conversion)
8. Error Handling Tree (decision tree for POST /booking and cancellation)
9. Integration Points (module dependencies)
**When to use:** Understanding the big picture, presenting to team, tracing flow execution
---
## 🔍 Key Information at a Glance
### Source Code Locations
```
Backend Time-Slot System:
├── packages/server/src/time-slot/
│ ├── slot-generator.service.ts (172 lines)
│ ├── time-slot.service.ts (142 lines)
│ ├── time-slot.controller.ts (93 lines)
│ ├── time-slot.module.ts
│ └── dto/
│ ├── query-slots.dto.ts
│ ├── create-manual-slot.dto.ts
│ └── week-template.dto.ts
├── packages/server/src/scheduler/
│ ├── scheduler.service.ts (55 lines)
│ └── scheduler.module.ts
├── packages/server/src/booking/
│ ├── booking.service.ts (367 lines)
│ ├── booking.controller.ts (82 lines)
│ ├── booking.module.ts
│ └── dto/
│ └── create-booking.dto.ts
├── packages/server/prisma/
│ └── schema.prisma (Models: WeekTemplate, TimeSlot, Booking)
└── packages/shared/src/
├── constants.ts (Slot generation, capacity defaults)
├── enums.ts (TimeSlotStatus, BookingStatus, etc.)
└── types/
└── time-slot.ts (Type definitions)
```
### Daily Scheduler (UTC)
| Time | Job | Method |
|------|-----|--------|
| 02:00 | Generate 14 days of slots | `SlotGeneratorService.generateSlots(14)` |
| 02:30 | Close expired OPEN slots | `SlotGeneratorService.cleanupExpiredSlots()` |
| 03:00 | Expire memberships | `SlotGeneratorService.checkExpiredMemberships()` |
| 22:00 | Complete past bookings | `SlotGeneratorService.completeBookings()` |
### Important Constants
```
DEFAULT_SLOT_CAPACITY = 1 (private lessons)
SLOT_GENERATION_DAYS = 14 (days ahead to auto-generate)
DEFAULT_CANCEL_HOURS_LIMIT = 2 (hours before slot to allow refund)
```
### API Endpoints
**Member:**
```
GET /time-slot/available?date=YYYY-MM-DD
GET /time-slot/:id
POST /booking
PUT /booking/:id/cancel
GET /booking/my
GET /booking/my/upcoming
```
**Admin:**
```
GET /admin/week-template
PUT /admin/week-template
POST /admin/time-slot/manual
PUT /admin/time-slot/:id/close
POST /admin/generate-slots
GET /admin/bookings
```
---
## 🎯 Common Tasks & Where to Find Info
| Task | Reference |
|------|-----------|
| **Understand slot generation algorithm** | TIME_SLOT_SCHEDULING_SYSTEM.md § 2.2 or DIAGRAMS § 4 |
| **See all API endpoints** | QUICK_REFERENCE § "API Endpoints" or TIME_SLOT_SCHEDULING_SYSTEM.md § 4 |
| **Booking creation logic** | TIME_SLOT_DIAGRAMS.md § 3 or QUICK_REFERENCE § "Key Logic" |
| **Weekday mapping (ISO vs JS)** | QUICK_REFERENCE § "Weekday Mapping" or DIAGRAMS § 7 |
| **Cancellation refund policy** | TIME_SLOT_SCHEDULING_SYSTEM.md § 6.1 or DIAGRAMS § 3 |
| **Scheduler jobs timeline** | QUICK_REFERENCE § "Daily Scheduler Jobs" or DIAGRAMS § 2 |
| **Error handling** | QUICK_REFERENCE § "Common Errors" or DIAGRAMS § 8 |
| **Data model relationships** | DIAGRAMS § 1 or TIME_SLOT_SCHEDULING_SYSTEM.md § 1 |
| **Configuration & setup** | QUICK_REFERENCE § "Configuration" or TIME_SLOT_SCHEDULING_SYSTEM.md § 14 |
| **Performance tips** | TIME_SLOT_SCHEDULING_SYSTEM.md § 15 or QUICK_REFERENCE § "Performance Tips" |
| **Module dependencies** | DIAGRAMS § 9 or TIME_SLOT_SCHEDULING_SYSTEM.md § 11.2 |
| **Testing** | TIME_SLOT_SCHEDULING_SYSTEM.md § 13 or QUICK_REFERENCE § "Testing" |
---
## 📋 System Overview
### What It Does
This system manages the complete lifecycle of time slots and bookings for a pilates studio:
1. **Automated Slot Generation**: Every day at 02:00 UTC, generates 14 days of time slots from reusable weekly templates
2. **Capacity Management**: Tracks slot capacity and prevents overbooking
3. **Booking Management**: Allows members to book slots with their memberships
4. **Cancellation & Refunds**: Members can cancel with conditional refunds (within 2-hour window)
5. **Membership Expiration**: Automatically expires memberships by date or used sessions
6. **Cleanup**: Marks past slots as closed and completed bookings as finished
### Key Concepts
- **WeekTemplate**: Defines recurring schedule (e.g., "Monday 09:00-10:00")
- **TimeSlot**: Individual class instance (e.g., "April 10, 2026 09:00-10:00")
- **Booking**: User's reservation (links user + slot + membership)
- **Status Tracking**: OPEN → FULL → CLOSED (slots) and CONFIRMED → COMPLETED (bookings)
### Architecture Highlights
**Idempotent** - Safe to re-run slot generation
**Transactional** - ACID compliance for bookings
**Automated** - 4 daily cron jobs maintain state
**Flexible** - Supports TIMES, DURATION, and TRIAL memberships
**Scalable** - Batch operations, proper database indexes
**Secure** - Role-based access, comprehensive validation
---
## 🚀 Getting Started
### For New Developers
1. **Start with**: TIME_SLOT_QUICK_REFERENCE.md
- Get oriented with file locations and key methods
2. **Then read**: TIME_SLOT_DIAGRAMS.md § 1 (Data Model)
- Understand how entities relate
3. **Deep dive**: TIME_SLOT_SCHEDULING_SYSTEM.md § 2
- Study the SlotGeneratorService algorithm
4. **Explore the code**: Read actual source files for implementation details
### For System Integration
1. Review TIME_SLOT_DIAGRAMS.md § 9 (Integration Points)
2. Check the module imports in `app.module.ts`
3. Understand dependencies in QUICK_REFERENCE.md § "Configuration"
### For API Integration
1. Start with TIME_SLOT_QUICK_REFERENCE.md § "API Endpoints"
2. See examples in TIME_SLOT_SCHEDULING_SYSTEM.md § 12
3. Check DTOs in TIME_SLOT_SCHEDULING_SYSTEM.md § 8
### For Debugging
1. Check common errors in QUICK_REFERENCE.md § "Common Errors"
2. Trace error handling in DIAGRAMS.md § 8
3. Review actual error handling in source code
---
## 📖 Reading Recommendations by Role
### Backend Developer
1. TIME_SLOT_SCHEDULING_SYSTEM.md (all)
2. TIME_SLOT_DIAGRAMS.md (all)
3. Source code in `packages/server/src/time-slot/`
### Frontend Developer
1. TIME_SLOT_QUICK_REFERENCE.md (API Endpoints section)
2. TIME_SLOT_SCHEDULING_SYSTEM.md § 12 (Example Scenarios)
3. TIME_SLOT_DIAGRAMS.md § 5 (User Booking Flow)
### DevOps / Sysadmin
1. TIME_SLOT_SCHEDULING_SYSTEM.md § 14 (Configuration)
2. TIME_SLOT_QUICK_REFERENCE.md § "Daily Scheduler Jobs"
3. TIME_SLOT_DIAGRAMS.md § 2 (Scheduler Timeline)
### Product Manager
1. TIME_SLOT_SCHEDULING_SYSTEM.md § "Executive Summary"
2. TIME_SLOT_DIAGRAMS.md § 3 & 5 (Booking flows)
3. TIME_SLOT_QUICK_REFERENCE.md § "Architecture Highlights"
### QA / Tester
1. TIME_SLOT_QUICK_REFERENCE.md (all)
2. TIME_SLOT_SCHEDULING_SYSTEM.md § 13 (Testing Guide)
3. TIME_SLOT_SCHEDULING_SYSTEM.md § 12 (Example Scenarios)
---
## 🔗 Related Documentation
- **Database Schema**: See `packages/server/prisma/schema.prisma` (lines 113-168)
- **Shared Types**: See `packages/shared/src/types/` and `enums.ts`
- **Authentication**: See booking endpoints require JwtAuthGuard
- **Membership System**: See `BookingService` integration with `MembershipService`
- **Studio Config**: See `StudioService` for `cancelHoursLimit`
---
## 📊 Document Statistics
| File | Lines | Size | Topics |
|------|-------|------|--------|
| TIME_SLOT_SCHEDULING_SYSTEM.md | 966 | 24KB | 17 comprehensive sections |
| TIME_SLOT_QUICK_REFERENCE.md | 355 | 9KB | 15 quick-lookup sections |
| TIME_SLOT_DIAGRAMS.md | 606 | 25KB | 9 visual flowcharts |
| **Total** | **1,927** | **58KB** | **Complete system coverage** |
---
## 🎓 Learning Path
```
Entry Level
├─ README.md (this file)
├─ TIME_SLOT_QUICK_REFERENCE.md (20 min read)
└─ TIME_SLOT_DIAGRAMS.md § 1 (5 min)
Intermediate
├─ TIME_SLOT_DIAGRAMS.md (all, 15 min)
├─ TIME_SLOT_QUICK_REFERENCE.md (re-read, 15 min)
└─ TIME_SLOT_SCHEDULING_SYSTEM.md § 1-6 (30 min)
Advanced
├─ TIME_SLOT_SCHEDULING_SYSTEM.md (full, 60 min)
├─ Source code reading (packages/server/src/time-slot/)
└─ Prisma schema study
Expert
└─ Code review + contributions
```
---
## 🤝 Contributing
When adding features or making changes:
1. **Update the code** in `packages/server/src/time-slot/` and related modules
2. **Update tests** in `__tests__/` directories
3. **Update documentation** in this docs folder if behavior changes
4. Use the **Quick Reference** as checklist for all affected pieces
---
## ❓ FAQ
**Q: Where do time slots come from?**
A: Auto-generated from WeekTemplates every day at 02:00 UTC by `generateSlots(14)`.
**Q: Can I disable slot generation?**
A: Yes, make templates `isActive: false` or disable the cron job in `scheduler.service.ts`.
**Q: How is capacity managed?**
A: `bookedCount` increments on booking, slot status becomes FULL when `bookedCount >= capacity`.
**Q: What if I cancel a booking?**
A: `bookedCount` decrements; if within 2-hour window, membership refunded; slot status restored if was FULL.
**Q: Timezone support?**
A: All times stored in UTC. Scheduler uses UTC times (02:00, 02:30, etc.). See DIAGRAMS § 7.
**Q: How are memberships expired?**
A: Automatically by scheduler job at 03:00 UTC daily; marks EXPIRED if date passed or USED_UP if sessions depleted.
---
## 📞 Quick Reference Card
### Status Values
- **TimeSlot**: OPEN | FULL | CLOSED
- **Booking**: CONFIRMED | CANCELLED | COMPLETED | NO_SHOW
- **Membership**: ACTIVE | EXPIRED | USED_UP
### Key Dates & Times
- **Slot generation**: Daily 02:00 UTC (14 days ahead)
- **Cleanup**: Daily 02:30 UTC
- **Membership check**: Daily 03:00 UTC
- **Booking completion**: Daily 22:00 UTC
- **Cancellation window**: 2 hours before slot (configurable)
### Key Files
- **Slot generation**: `slot-generator.service.ts`
- **Slot queries**: `time-slot.service.ts`
- **Booking logic**: `booking.service.ts`
- **Database**: `prisma/schema.prisma`

View File

@@ -0,0 +1,355 @@
# 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<number>
// Close all past OPEN slots
cleanupExpiredSlots(): Promise<number>
// Expire memberships by date or session count
checkExpiredMemberships(): Promise<number>
// Mark past bookings as COMPLETED
completeBookings(): Promise<number>
```
### TimeSlotService
```typescript
// Get all slots for a date (with user's booking status if provided)
getAvailableSlots(date: string, userId?: string): Promise<TimeSlotWithBookingStatus[]>
// Manually create a one-off slot
createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot>
// Close a slot (prevent new bookings)
closeSlot(id: string): Promise<TimeSlot>
// Get/replace weekly templates
getWeekTemplates(): Promise<WeekTemplate[]>
replaceWeekTemplates(items: WeekTemplateItemDto[]): Promise<CreateBatchPayload>
```
### BookingService
```typescript
// Create a booking (validates slot/membership, updates counts)
createBooking(userId: string, dto: CreateBookingDto): Promise<BookingWithRelations>
// Cancel a booking (conditionally refunds membership)
cancelBooking(userId: string, bookingId: string): Promise<CancelBookingResult>
// Get user's bookings (paginated, filterable by status)
getMyBookings(userId: string, status?, page, limit): Promise<PaginatedResult>
// Get all CONFIRMED bookings for dates >= today
getUpcomingBookings(userId: string): Promise<BookingWithRelations[]>
```
---
## 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

View File

@@ -0,0 +1,966 @@
# 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)
```prisma
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)
```prisma
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)
```prisma
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:**
```sql
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:**
```sql
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:**
```typescript
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:**
```typescript
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:
```json
{
"templates": [
{
"dayOfWeek": 1,
"startTime": "09:00",
"endTime": "10:00",
"capacity": 1,
"isActive": true
}
]
}
```
#### `POST /admin/time-slot/manual`
Creates a manual slot. Request body:
```json
{
"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:**
```typescript
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`
```typescript
class QuerySlotsDto {
@IsDateString()
date!: string // Format: YYYY-MM-DD
}
```
#### `CreateManualSlotDto`
```typescript
class CreateManualSlotDto {
@IsDateString()
date!: string
@IsString()
startTime!: string
@IsString()
endTime!: string
@IsOptional()
@IsInt()
@Min(1)
capacity?: number
}
```
#### `WeekTemplateItemDto` & `UpdateWeekTemplateDto`
```typescript
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
```typescript
// 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
```typescript
// 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()`:
```typescript
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:**
```json
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.