Skip to content

Instantly share code, notes, and snippets.

@korrio
Created March 17, 2026 17:37
Show Gist options
  • Select an option

  • Save korrio/d5aefc2f4b0c7dcc9a01365982b6e105 to your computer and use it in GitHub Desktop.

Select an option

Save korrio/d5aefc2f4b0c7dcc9a01365982b6e105 to your computer and use it in GitHub Desktop.
BOOKING-FLOWCHART.md

Court Booking Availability Flowchart

Overview

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).

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                         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"                                                    │
│    }                                                                         │
│  }                                                                           │
└─────────────────────────────────────────────────────────────────────────────┘

Booking Conflict Detection

Single Court Booking (DUNE or GLACIER)

┌─────────────────────────────────────────────────────────────────┐
│              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

Combined Court Booking (CONNECTED COURTS)

┌─────────────────────────────────────────────────────────────────┐
│           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

Frontend: Time Slot Selection Flow

┌─────────────────────────────────────────────────────────────────┐
│              /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

┌─────────────────────────────────────────────────────────────────┐
│                   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

Database Schema Relationships

┌─────────────────────────────────────────────────────────────────┐
│              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

Edge Cases Handled

Case 1: Pending Bookings Don't Block

Status = 'pending' → Slot shows as available
Only 'confirmed' bookings block slots

Case 2: Same Day Reschedule

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

Case 3: Time Range Closures

Branch closed 14:00-16:00 for maintenance
Slots in closure range show isAvailable = false
closureReason = "Maintenance"

Case 4: Past Time Slots

Current time = 15:30
Slots 06:00-15:00 show isAvailable = false, isPast = true
Slots 16:00-22:00 show normally

API Endpoints

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

Query Parameters

GET /api/courts/{courtId}/availability

Required:
- date: YYYY-MM-DD

Optional:
- excludeBookingId: string (for rescheduling)

Response Format

{
  "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"
  }
}

Sequence Diagram: Booking Creation

┌──────┐          ┌─────────────┐          ┌──────────────┐          ┌──────────┐
│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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment