This document describes the complete flow of how the court booking system checks and manages court availability across single courts (DUNE, GLACIER) and combined courts (CONNECTED COURTS).
┌─────────────────────────────────────────────────────────────────────────────┐
│ AVAILABILITY CHECK FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────┐ ┌──────────────────────────────┐
│ Client │────▶│ Availability │────▶│ API: GET /api/courts/ │
│ (Browser) │ │ UI Component │ │ [courtId]/availability │
└──────────────┘ └──────────────────┘ └──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ API CONTROLLER │
│ │
│ 1. Parse Parameters │
│ ├── courtId (from path) │
│ ├── date (from query) │
│ └── excludeBookingId (optional - for rescheduling) │
│ │
│ 2. Fetch Court Info │
│ └── Get court details + branch info │
│ │
│ 3. Get Operating Hours (from system settings) │
│ ├── openingTime (default: 06:00) │
│ └── closingTime (default: 23:00) │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ CHECK CLOSURES │
│ │
│ Query: branchClosure.findMany() │
│ ├── type = 'full_day' → All slots unavailable │
│ └── type = 'time_range' → Specific slots unavailable │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ FETCH BOOKINGS (3 Queries) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. DIRECT BOOKINGS │ │
│ │ Query: booking.findMany({ │ │
│ │ courtId: targetCourtId, │ │
│ │ date: targetDate, │ │
│ │ status: 'confirmed' │ │
│ │ }) │ │
│ │ → Bookings where this court is the main court │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 2. LINKED BOOKINGS │ │
│ │ Query: booking.findMany({ │ │
│ │ linkedCourtId: targetCourtId, │ │
│ │ date: targetDate, │ │
│ │ status: 'confirmed' │ │
│ │ }) │ │
│ │ → Bookings where this court is the linked court │ │
│ │ (e.g., DUNE booked with GLACIER as linked) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 3. COMBINED COURT BOOKINGS │ │
│ │ (Only for DUNE/GLACIER - not CONNECTED) │ │
│ │ │ │
│ │ Step 3a: Find as linked court │ │
│ │ Query: booking.findMany({ │ │
│ │ isCombinedCourt: true, │ │
│ │ linkedCourtId: targetCourtId, │ │
│ │ status: 'confirmed' │ │
│ │ }) │ │
│ │ │ │
│ │ Step 3b: Find CONNECTED COURTS bookings │ │
│ │ Query: court.findFirst({ name: { contains: 'CONNECTED' } }) │ │
│ │ Then: booking.findMany({ │ │
│ │ courtId: connectedCourtId, │ │
│ │ isCombinedCourt: true, │ │
│ │ status: 'confirmed' │ │
│ │ }) │ │
│ │ → Blocks both DUNE and GLACIER when CONNECTED is booked │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ MERGE & FILTER BOOKINGS │
│ │
│ 1. Combine all bookings: [...direct, ...linked, ...combined] │
│ 2. Remove duplicates (by booking ID) │
│ 3. Filter out excluded booking (if rescheduling) │
│ │
│ Result: validBookings[] │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ GENERATE TIME SLOTS │
│ │
│ For each hour from openingTime to closingTime: │
│ ├── Format: "HH:00" │
│ ├── Check if slot is in the past (for today's date) │
│ ├── Check if slot is in closure time range │
│ ├── Check if slot overlaps with any validBooking │
│ │ └── Condition: booking.startTime <= slot < booking.endTime │
│ └── Calculate price (base + special pricing if applicable) │
│ │
│ Slot Object: │
│ { │
│ time: "14:00", │
│ isAvailable: true/false, │
│ price: 400, │
│ closureReason: null || "Maintenance", │
│ isPast: false │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ RETURN RESPONSE │
│ │
│ { │
│ date: "2026-03-20", │
│ slots: [/* array of slot objects */], │
│ operatingHours: { │
│ openingTime: "06:00", │
│ closingTime: "23:00" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CONFLICT CHECK FOR SINGLE COURT │
└─────────────────────────────────────────────────────────────────┘
User wants to book DUNE at 14:00-16:00
Check 1: Any booking where DUNE is main court?
├── booking.courtId = DUNE_ID
├── booking.startTime <= "14:00" AND booking.endTime > "14:00"
└── OR booking.startTime < "16:00" AND booking.endTime >= "16:00"
└── OR booking.startTime >= "14:00" AND booking.endTime <= "16:00"
Check 2: Any booking where DUNE is linked court?
├── booking.linkedCourtId = DUNE_ID
└── Same time overlap checks as above
Check 3: Any CONNECTED COURTS booking?
├── Find CONNECTED court
├── booking.courtId = CONNECTED_ID
├── booking.isCombinedCourt = true
└── Same time overlap checks
Result: CONFLICT if any check returns a booking
┌─────────────────────────────────────────────────────────────────┐
│ CONFLICT CHECK FOR CONNECTED COURTS │
└─────────────────────────────────────────────────────────────────┘
User wants to book CONNECTED COURTS at 14:00-16:00
(This books both DUNE + GLACIER)
Check 1: Is DUNE available?
├── DUNE direct bookings at this time
├── DUNE linked bookings at this time
└── DUNE as part of other combined bookings
Check 2: Is GLACIER available?
├── GLACIER direct bookings at this time
├── GLACIER linked bookings at this time
└── GLACIER as part of other combined bookings
Result: CONFLICT if EITHER DUNE OR GLACIER has a booking
┌─────────────────────────────────────────────────────────────────┐
│ /liff/book-court PAGE FLOW │
└─────────────────────────────────────────────────────────────────┘
1. User Selects Date
└── fetchAllCourtsAvailability(date)
└── For each court: GET /api/courts/{id}/availability?date=...
└── Store availability map in state
2. User Selects Court
└── If CONNECTED COURTS selected:
├── Fetch DUNE availability
├── Fetch GLACIER availability
└── Merge slots (available only if BOTH available)
└── If Single Court selected:
└── Fetch that court's availability
3. User Selects Time Slots
└── toggleTimeSelection(time)
└── Validate consecutive hours
└── Calculate total price
4. Submit Booking
└── POST /api/bookings
└── Server validates availability again
└── Create booking if available
┌─────────────────────────────────────────────────────────────────┐
│ RESCHEDULE FLOW │
└─────────────────────────────────────────────────────────────────┘
1. User Opens Reschedule Modal
└── fetchAvailableSlots(courtId, date, duration, isCombined, excludeBookingId)
└── excludeBookingId = current booking ID
2. API Excludes Current Booking
└── validBookings = allBookings.filter(b => b.id !== excludeBookingId)
└── This allows selecting the same slot for moving
3. User Selects New Date/Time
└── Check 48-hour policy (must reschedule before 48h of booking)
4. Submit Reschedule
└── POST /api/bookings/{id}/reschedule
└── Server validates:
├── New slot is available
├── Within 2 weeks
├── Not in the past
└── User hasn't rescheduled before
└── Update booking with new date/time
┌─────────────────────────────────────────────────────────────────┐
│ BOOKING RELATIONSHIPS │
└─────────────────────────────────────────────────────────────────┘
Court Table:
┌─────────┬────────────┬──────────┐
│ ID │ NAME │ TYPE │
├─────────┼────────────┼──────────┤
│ court_1 │ DUNE │ indoor │
│ court_2 │ GLACIER │ indoor │
│ court_3 │ CONNECTED │ combined │
└─────────┴────────────┴──────────┘
Booking Table:
┌───────────┬─────────┬─────────────┬─────────────────┬───────────┐
│ ID │ USER_ID │ COURT_ID │ LINKED_COURT_ID │ IS_COMBINED│
├───────────┼─────────┼─────────────┼─────────────────┼───────────┤
│ book_1 │ user_1 │ court_1 │ null │ false │ ← DUNE only
│ book_2 │ user_2 │ court_3 │ court_1 │ true │ ← CONNECTED (DUNE+GLACIER)
└───────────┴─────────┴─────────────┴─────────────────┴───────────┘
Query Logic:
- DUNE availability: WHERE courtId = court_1 OR linkedCourtId = court_1
- GLACIER availability: WHERE courtId = court_2 OR linkedCourtId = court_2
- CONNECTED booking blocks both: Check if CONNECTED (court_3) has booking
Status = 'pending' → Slot shows as available
Only 'confirmed' bookings block slots
User has booking at 14:00, wants to move to 16:00 same day
excludeBookingId excludes current booking from availability check
Both 14:00 and 16:00 show as available
Branch closed 14:00-16:00 for maintenance
Slots in closure range show isAvailable = false
closureReason = "Maintenance"
Current time = 15:30
Slots 06:00-15:00 show isAvailable = false, isPast = true
Slots 16:00-22:00 show normally
| Endpoint | Method | Description |
|---|---|---|
/api/courts/{id}/availability |
GET | Get availability for a court |
/api/courts |
GET | List all courts |
/api/bookings |
POST | Create new booking |
/api/bookings/{id}/reschedule |
POST | Reschedule existing booking |
Required:
- date: YYYY-MM-DD
Optional:
- excludeBookingId: string (for rescheduling)
{
"date": "2026-03-20",
"slots": [
{
"time": "06:00",
"isAvailable": true,
"price": 400,
"closureReason": null,
"isPast": false
},
{
"time": "14:00",
"isAvailable": false,
"price": 400,
"closureReason": null,
"isPast": false
}
],
"operatingHours": {
"openingTime": "06:00",
"closingTime": "23:00"
}
}┌──────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────┐
│Client│ │/liff/book │ │Availability │ │Database │
└──┬───┘ │ -court │ │ API │ └────┬─────┘
│ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │
│──Select Date───────▶│ │ │
│ │──GET /api/courts/1/───▶│ │
│ │ availability?date= │ │
│ │ │──Query Bookings──────▶│
│ │ │ (3 queries) │
│ │ │◀──Return bookings─────│
│ │ │ │
│ │ │──Generate slots──────▶│
│ │ │──Apply closures───▶ │
│ │◀──Return slots─────────│ │
│◀──Display slots─────│ │ │
│ │ │ │
│──Select 14:00──────▶│ │ │
│ │──Check consecutive───▶ │ │
│ │ slots available │ │
│◀──Highlight range───│ │ │
│ │ │ │
│──Click Book────────▶│ │ │
│ │──POST /api/bookings────│ │
│ │ │──Validate again──────▶│
│ │ │──Check conflicts──▶ │
│ │ │◀──No conflicts────────│
│ │ │ │
│ │ │──Create booking──────▶│
│ │◀──Booking created──────│ │
│◀──Show success──────│ │ │
Last Updated: 2026-03-16 Version: 1.0