## 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>
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:
dayOfWeekuses 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:
- Generating time slots from WeekTemplate
- Cleaning up expired slots
- Managing membership expiration
- 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:
-
By Expiration Date:
WHERE status = ACTIVE AND expireDate < NOW SET status = EXPIRED -
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.MANUALtemplateId: 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
expireDatenot 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:
- Validates slot is OPEN, membership is ACTIVE with remaining sessions
- Creates Booking(CONFIRMED)
- Increments slot.bookedCount (1 → 2)
- If now at capacity, sets slot.status = FULL
- Decrements membership.remainingTimes (5 → 4)
Member Cancels (within 2-hour window):
PUT /booking/booking-789/cancel
System:
- Checks if NOW + 2 hours ≤ slot start time ✓
- Sets booking.status = CANCELLED
- Decrements slot.bookedCount (2 → 1)
- If slot was FULL, restores to OPEN
- Increments membership.remainingTimes (4 → 5) ✓ refunded
Scenario 3: Membership Expires
Overnight at 03:00 UTC:
- Scheduler runs
handleCheckMemberships() - Updates all ACTIVE memberships where
expireDate < NOWto EXPIRED - User tries to book → BadRequestException "Membership is not active (status: EXPIRED)"
13. Testing Guide
Key Test Files
-
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
-
time-slot.service.spec.ts(existing)- Tests getAvailableSlots with user booking status
- Tests manual slot creation
-
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 queriesTimeSlot(status)- for status filteringBooking(userId)- for user's bookingsBooking(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
-
Overbooking Buffer:
- Allow configurable overbooking ratio (e.g., 110% capacity)
-
Waitlist Support:
- Add BookingStatus.WAITLISTED
- Auto-promote when slot opens
-
Recurring Cancellation:
- Cancel all future bookings of a series
- Batch refunds
-
Slot Availability Notifications:
- Alert users when slots available
- Implement notification queue
-
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.