# Reservations Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Reservations module manages **facility bookings for individuals** (members, non-members, players, guests) and **private match bookings** for sports pitches/courts. It handles:
- Facility reservation creation with conflict detection
- Reservation number generation (sequential daily)
- Hourly rate calculation (member vs non-member, AM vs PM)
- Reservation lifecycle (pending -> confirmed -> checked_in -> completed)
- Calendar view for scheduling visibility
- Facility conflict detection across reservations, rental bookings, training sessions, and blackout dates
- Private match bookings (friendly, training, tournament matches)
- Payment tracking for private matches

It does **NOT** directly manage:
- Facility definitions (Facilities module)
- Accounting entries (Accounting module listens via events)
- Notifications (Notifications module listens to events)
- Training session scheduling (Scheduling module)
- Pool-specific bookings (PoolManagement module)

---

## 2. Directory & File Structure

```
app/Modules/Reservations/
├── bootstrap.php                          # Menu + permission registration
├── Routes.php                             # Web routes (15 routes)
├── Controllers/
│   ├── ReservationController.php          # Main reservations (index, create, confirm, cancel, checkin, calendar)
│   └── PrivateMatchController.php         # Private match bookings (CRUD + payment)
├── Models/
│   ├── Reservation.php                   # Main reservation model (soft delete)
│   └── PrivateMatchBooking.php           # Private match model (no soft delete)
├── Services/
│   ├── ReservationService.php            # Create, confirm, cancel, check-in logic
│   ├── FacilityConflictDetector.php      # Multi-source conflict detection (CRITICAL)
│   ├── FacilityPricingService.php        # Hourly rate calculation from system_config
│   └── PrivateMatchService.php           # Private match booking + payment logic
└── Views/
    ├── index.php                         # Reservation list with filters
    ├── create.php                        # New reservation form
    ├── show.php                          # Reservation detail with actions
    ├── calendar.php                      # Weekly calendar view
    ├── private_matches.php              # Private match listing
    └── private_match_create.php         # Private match booking form
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `reservations` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| reservation_number | varchar(50) | NO | UNI | | Format: RES-YYYYMMDD-XXXX |
| facility_id | bigint unsigned | NO | MUL | | FK to facilities |
| booker_type | varchar(20) | NO | | | member/non_member/player/guest |
| player_id | bigint unsigned | YES | MUL | | FK to players |
| member_id | bigint unsigned | YES | MUL | | FK to members |
| booker_name | varchar(300) | YES | | | |
| booker_phone | varchar(30) | YES | | | |
| reservation_date | date | NO | MUL | | |
| start_time | time | NO | | | |
| end_time | time | NO | | | |
| duration_hours | decimal(5,2) | NO | | 1.00 | Calculated from times |
| time_tier | varchar(5) | NO | | AM | AM or PM (affects pricing) |
| unit_rate | decimal(15,2) | NO | | 0.00 | Hourly rate |
| total_amount | decimal(15,2) | NO | | 0.00 | rate * duration |
| payment_id | bigint unsigned | YES | MUL | | FK to external payment |
| status | varchar(20) | NO | MUL | pending | pending/confirmed/checked_in/completed/cancelled/no_show |
| confirmed_at | timestamp | YES | | | |
| cancelled_at | timestamp | YES | | | |
| cancel_reason | varchar(500) | YES | | | |
| notes | text | YES | | | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | On update |
| is_archived | tinyint(1) | NO | | 0 | Soft delete |
| archived_at | timestamp | YES | | | |
| archived_by | bigint unsigned | YES | | | |
| created_by | bigint unsigned | YES | | | |
| updated_by | bigint unsigned | YES | | | |

### 3.2 `private_match_bookings` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | int unsigned | NO | PRI | auto_increment | |
| facility_id | int unsigned | NO | MUL | | FK to facilities |
| booked_by_member_id | int unsigned | YES | MUL | | FK to members |
| booked_by_name | varchar(200) | YES | | | |
| booked_by_phone | varchar(20) | YES | | | |
| booking_date | date | NO | MUL | | |
| start_time | time | NO | | | |
| end_time | time | NO | | | |
| match_type | enum | NO | | friendly | friendly/training/tournament/other |
| team_a_name | varchar(100) | YES | | | |
| team_b_name | varchar(100) | YES | | | |
| players_count | tinyint unsigned | NO | | 10 | |
| has_referee | tinyint(1) | NO | | 0 | |
| referee_cost | decimal(10,2) | NO | | 0.00 | |
| facility_cost | decimal(10,2) | NO | | 0.00 | |
| total_cost | decimal(10,2) | NO | | 0.00 | facility_cost + referee_cost |
| deposit_paid | decimal(10,2) | NO | | 0.00 | Running total of payments |
| status | enum | NO | MUL | pending | pending/confirmed/in_progress/completed/cancelled |
| payment_status | enum | NO | | unpaid | unpaid/partial/paid |
| notes | text | YES | | | |
| cancelled_reason | text | YES | | | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |
| updated_at | datetime | NO | | CURRENT_TIMESTAMP | On update |

---

## 4. Routes

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /reservations | ReservationController@index | reservation.view |
| GET | /reservations/create | ReservationController@create | reservation.create |
| POST | /reservations | ReservationController@store | reservation.create |
| GET | /reservations/calendar | ReservationController@calendar | reservation.view |
| GET | /reservations/{id} | ReservationController@show | reservation.view |
| POST | /reservations/{id}/confirm | ReservationController@confirm | reservation.confirm |
| POST | /reservations/{id}/cancel | ReservationController@cancel | reservation.cancel |
| POST | /reservations/{id}/checkin | ReservationController@checkIn | reservation.view |
| GET | /private-matches | PrivateMatchController@index | reservation.view |
| GET | /private-matches/create | PrivateMatchController@create | reservation.create |
| POST | /private-matches | PrivateMatchController@store | reservation.create |
| POST | /private-matches/{id}/confirm | PrivateMatchController@confirm | reservation.create |
| POST | /private-matches/{id}/cancel | PrivateMatchController@cancel | reservation.create |
| POST | /private-matches/{id}/complete | PrivateMatchController@complete | reservation.create |
| POST | /private-matches/{id}/pay | PrivateMatchController@recordPayment | reservation.create |

---

## 5. Core Business Flows

### 5.1 Reservation Creation Flow

```
1. User selects facility, date, start_time, end_time, booker info
2. FacilityConflictDetector checks 5 conflict sources:
   a. Existing reservations (same facility/date/overlapping time)
   b. Rental bookings (institutional rentals)
   c. Facility blackout dates (full-day or time-range)
   d. Training sessions (coach/group sessions)
   e. Coach scheduling conflicts (if coach specified)
3. If conflicts → return errors with conflict descriptions
4. If clear:
   - Generate reservation_number (RES-YYYYMMDD-XXXX)
   - Calculate duration_hours from time range
   - Determine time_tier (AM/PM based on SPORTS_PM_START_HOUR rule, default 17:00)
   - Calculate unit_rate via FacilityPricingService
   - Calculate total_amount = unit_rate * duration_hours
   - Create reservation with status='pending'
   - Dispatch: reservation.created
```

### 5.2 Pricing Logic (FacilityPricingService)

Priority order:
1. Direct rate columns on `facilities` table (`hourly_rate_member`, `hourly_rate_member_pm`, etc.)
2. System config: `sports_pricing_member` / `sports_pricing_non_member` (JSON arrays by facility_type)

Rates vary by:
- Member vs non-member
- AM vs PM time tier
- Facility type (pitch, court, hall)

### 5.3 Private Match Flow

```
1. Select facility (pitch/court/hall only), date, times, team details
2. ConflictDetectionService (Scheduling module) checks for overlaps
3. Calculate costs: facility_cost + referee_cost = total_cost
4. Create booking with status='pending', payment_status based on deposit
5. Confirm → Cancel → Complete lifecycle
6. recordPayment() updates deposit_paid running total and payment_status
```

### 5.4 Reservation Number Format

`RES-YYYYMMDD-XXXX` — sequential within the same day (based on COUNT query).

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `reservation.created` | `{reservation_id, facility_id, date}` | ReservationService::create |
| `reservation.confirmed` | `{reservation_id}` | ReservationService::confirm |
| `reservation.cancelled` | `{reservation_id}` | ReservationService::cancel |
| `private_match.booked` | `{booking_id, facility_id, amount, member_id}` | PrivateMatchService::createBooking |
| `private_match.confirmed` | `{booking_id, facility_id}` | PrivateMatchService::confirmBooking |
| `facility.entry_recorded` | `{entry_id, facility_id, amount, member_id, booker_type}` | PrivateMatchService::recordPayment |

---

## 7. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `reservation.created` | Scheduling | Sync to scheduling calendar |
| `reservation.confirmed` | Notifications | Send confirmation notification |
| `reservation.reminder` | Notifications | Send reminder notification |
| `private_match.booked` | Notifications | Send booking notification |
| `facility.entry_recorded` | Accounting | Auto-post journal entry for facility revenue |

---

## 8. Cross-Module Dependencies

### 8.1 Reservations module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Facilities | `Facility` model (list facilities, find by ID), `FacilityBlackoutDate` model |
| Rules | `RuleEngine::getValue('SPORTS_PM_START_HOUR')` (AM/PM tier cutoff) |
| Scheduling | `ConflictDetectionService::checkFacilityOverlap()` (private match conflicts) |
| Rentals | Queries `rental_bookings` table directly (conflict detection) |

### 8.2 Other modules that IMPORT FROM Reservations:

| Module | What it imports |
|--------|----------------|
| Scheduling | Listens to `reservation.created` event |
| Notifications | Listens to `reservation.confirmed`, `reservation.reminder`, `private_match.booked` |
| Accounting | Listens to `facility.entry_recorded` for revenue journal entries |

---

## 9. Permissions

| Key | Description |
|-----|-------------|
| reservation.view | View reservations, calendar, private matches |
| reservation.create | Create reservations and private matches, manage private match lifecycle |
| reservation.confirm | Confirm reservations |
| reservation.cancel | Cancel reservations |

---

## 10. Sidebar Menu

Registered with `order: 240`:
- All Reservations (`/reservations`)
- Calendar (`/reservations/calendar`)
- Private Matches (`/private-matches`)
- New Booking (`/reservations/create`)

---

## 11. High-Risk Areas

### 11.1 FacilityConflictDetector (CRITICAL)
- Central service that prevents double-booking across 5 data sources
- Queries `reservations`, `rental_bookings`, `facility_blackout_dates`, `training_sessions` tables
- Uses try/catch for `rental_bookings` and `training_sessions` — silently skips if tables don't exist
- If this service fails, overlapping bookings can be created

### 11.2 Reservation Number Sequencing
- Based on COUNT query: `SELECT COUNT(*) FROM reservations WHERE reservation_number LIKE 'RES-YYYYMMDD-%'`
- Under concurrent use, duplicate numbers are possible (no unique constraint enforcement in code)
- However, database has UNIQUE constraint on `reservation_number` column

### 11.3 Pricing Calculation
- Multiple fallback paths (facility direct rates -> system_config JSON)
- If `system_config` entries are missing, rate defaults to 0 (free booking)
- `bcmul` used for precision but `total_amount` can be 0 if pricing not configured

### 11.4 Private Match Payment Tracking
- `deposit_paid` is a running total updated via `recordPayment()`
- No payment reversal/void mechanism in PrivateMatchService
- Payment status derived by comparing deposit_paid to total_cost
- `facility.entry_recorded` event dispatched on every payment (Accounting posts revenue)

### 11.5 Two Different Conflict Detection Systems
- `FacilityConflictDetector` is used for general reservations
- `ConflictDetectionService` (from Scheduling module) is used for private matches
- These two systems may not check the same sources — potential for cross-system conflicts

---

## 12. Configuration Dependencies

| Config Key (system_config) | Purpose |
|---------------------------|---------|
| sports_pricing_member | JSON array: facility_type pricing for members |
| sports_pricing_non_member | JSON array: facility_type pricing for non-members |
| entry_tickets_non_member | JSON array: entry ticket prices by day type |
| general_fees | JSON array: miscellaneous fee codes/amounts |

| Rule Engine Keys | Purpose |
|-----------------|---------|
| SPORTS_PM_START_HOUR | Hour cutoff for AM/PM time tier (default: 17) |

---

## 13. Known Patterns & Gotchas

1. **Two separate booking systems**: `reservations` (general facility) and `private_match_bookings` (pitches/courts) are independent tables with different schemas
2. **Soft delete on reservations, NOT on private matches**: Different archiving strategies
3. **Private match uses raw SQL**: `PrivateMatchService` and `PrivateMatchBooking::search()` use direct `$db->insert/select` instead of Model methods
4. **Calendar uses raw SQL with JOIN**: Not through QueryBuilder, direct SQL query
5. **PrivateMatchController uses `$_GET/$_POST` directly**: Older pattern, not using `$request->get()/post()` consistently
6. **No check-in for private matches**: Only reservations have check-in flow
7. **FacilityConflictDetector::suggestAlternatives()**: Provides alternative time slots when conflicts detected (up to 5 suggestions)
8. **Reservation search includes facility JOIN**: Returns `facility_name` via LEFT JOIN to facilities table
