# Facilities Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Facilities module manages the **physical infrastructure** of the club — pitches, courts, halls, pools, tables, tracks, etc. It provides:
- Facility CRUD (create, list, show, edit, toggle active)
- Time slot management (weekly recurring availability/blocking)
- Blackout date management (one-off date blocking for maintenance, events, etc.)
- Availability checking (slot + blackout intersection logic)
- Rate management (member/non-member, AM/PM pricing tiers)
- Access control service (member-only, VIP-only slot enforcement)
- Facility entry recording (creates pool_bookings with price calculation)

It serves as the **resource layer** — other modules (Reservations, Rentals, Academies, SportsActivity) consume facility data to book and schedule activities.

It does **NOT** directly manage:
- Reservations/bookings workflow (Reservations module)
- Rentals (Rentals module)
- Academy schedules that use facilities (Academies module references facility_id)
- Training session scheduling (SportsActivity module)

**Classification:** Fully independent module with its own controllers, routes, views, services, and models. NOT a wrapper or redirect.

---

## 2. Directory & File Structure

```
app/Modules/Facilities/
├── bootstrap.php                          # Permission registration only (sidebar owned elsewhere)
├── Routes.php                             # Web routes (12 routes)
├── Controllers/
│   └── FacilityController.php            # Full CRUD + blackout + timeslots
├── Models/
│   ├── Facility.php                      # Main model (soft delete, author tracking)
│   ├── FacilityBlackoutDate.php          # Blackout dates (no soft delete)
│   └── FacilityTimeSlot.php             # Weekly time slots (no soft delete)
├── Services/
│   ├── FacilityService.php              # Query helpers + availability checking + rate lookup
│   └── AccessControlService.php         # Access enforcement + entry recording
└── Views/
    ├── index.php                         # Paginated list with type/location/discipline filters
    ├── create.php                        # Create form (rates, location, type, config)
    ├── edit.php                          # Edit form
    ├── show.php                          # Detail page (time slots table, blackout dates)
    └── timeslots/
        └── create.php                    # Create time slot form
```

---

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

### 3.1 `facilities` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(50) | NO | UNI | Auto-generated if empty |
| name_ar | varchar(200) | NO | | Arabic name (required) |
| name_en | varchar(200) | YES | | English name |
| facility_type | varchar(50) | NO | MUL | pitch/court/hall/lane/table/device/track/pool |
| location | varchar(50) | NO | | indoor/outdoor |
| capacity | int unsigned | YES | | Max occupancy |
| hourly_rate_member | decimal(15,2) | NO | | AM rate for members |
| hourly_rate_nonmember | decimal(15,2) | NO | | AM rate for non-members |
| hourly_rate_member_pm | decimal(15,2) | YES | | PM rate for members |
| hourly_rate_nonmember_pm | decimal(15,2) | YES | | PM rate for non-members |
| linked_discipline_id | bigint unsigned | YES | MUL | FK to sport_disciplines |
| config_json | json | YES | | surface_type, has_lighting, has_changing_rooms, dimensions, notes |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| is_archived | tinyint(1) | NO | | Soft delete flag |
| maintenance_schedule_json | json | YES | | Maintenance schedule config |
| peak_hours_json | json | YES | | Peak hours config |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |

### 3.2 `facility_time_slots` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| facility_id | bigint unsigned | NO | MUL | FK to facilities |
| day_of_week | tinyint unsigned | NO | MUL | 0=Sunday .. 6=Saturday |
| start_time | time | NO | | |
| end_time | time | NO | | |
| slot_type | varchar(30) | NO | | available/maintenance/academy_reserved/blocked |
| linked_academy_id | bigint unsigned | YES | | FK to academies (for academy_reserved slots) |
| is_active | tinyint(1) | NO | | Default: 1 |
| access_type | varchar(20) | NO | MUL | open/members_only/vip (default: 'open') |
| member_price | decimal(10,2) | YES | | Override price for members in this slot |
| guest_price | decimal(10,2) | YES | | Override price for guests in this slot |
| carnet_guest_price | decimal(10,2) | YES | | Carnet price for guests |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.3 `facility_blackout_dates` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| facility_id | bigint unsigned | NO | MUL | FK to facilities |
| blackout_date | date | NO | | The blocked date |
| start_time | time | YES | | Partial-day block start (null = all day) |
| end_time | time | YES | | Partial-day block end (null = all day) |
| reason | varchar(500) | YES | | Why it's blocked |
| created_by | bigint unsigned | YES | | FK to employees |
| created_at | timestamp | NO | | |

### 3.4 `facility_units` Table (referenced in code but NOT present in production DB)

The `FacilityController::createTimeSlot()` method queries `facility_units` but this table does **not exist** in the live database. This would cause a runtime error if the timeslot creation form is used.

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /facilities | index | auth | facility.view |
| GET | /facilities/create | create | auth | facility.manage |
| POST | /facilities | store | auth, csrf | facility.manage |
| GET | /facilities/{id} | show | auth | facility.view |
| GET | /facilities/{id}/edit | edit | auth | facility.manage |
| POST | /facilities/{id} | update | auth, csrf | facility.manage |
| POST | /facilities/{id}/toggle | toggle | auth, csrf | facility.manage |
| POST | /facilities/{id}/blackout | addBlackout | auth, csrf | facility.manage |
| POST | /facilities/{id}/blackout/remove | removeBlackout | auth, csrf | facility.manage |
| GET | /facilities/{id}/timeslots/create | createTimeSlot | auth | facility.manage |
| POST | /facilities/{id}/timeslots | storeTimeSlot | auth, csrf | facility.manage |
| POST | /facilities/{id}/timeslots/{slotId}/delete | deleteTimeSlot | auth, csrf | facility.manage |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `facility.entry_recorded` | `{entry_id, facility_id, member_id, amount, booker_type}` | AccessControlService::recordFacilityEntry |

---

## 6. Events Consumed

None registered in bootstrap.php. This module does not listen to any events.

---

## 7. Cross-Module Dependencies

### 7.1 Facilities module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Disciplines | `SportDiscipline::allActive()` (discipline dropdown for linking) |

### 7.2 Other modules that IMPORT FROM Facilities:

| Module | What it imports |
|--------|----------------|
| Academies | `Facility` model imported in controller (available for schedule linking) |
| Reservations | `Facility::allActive()`, `FacilityBlackoutDate` for conflict detection |
| Rentals | `Facility::allActive()`, `Facility::find()` for rental booking |
| PlayerApi | `FreeTimeController::availableFacilities()`, `BookingController::getBookableFacilities()` |
| SportsActivity | Has its own `FacilityController` at `/sa/facilities` routes; uses `Facility` model |
| SportsDashboard | Queries `facilities` table for utilization stats |

### 7.3 Data Dependencies (SQL queries in AccessControlService):

| Table | Owner Module | Usage |
|-------|--------------|-------|
| members | Members | Member status check for access control |
| pool_bookings | (shared?) | Entry recording destination table |

---

## 8. Permissions

| Key | Arabic | English |
|-----|--------|---------|
| facility.view | عرض الملاعب والمرافق | View Facilities |
| facility.manage | إدارة الملاعب والمرافق | Manage Facilities |

---

## 9. Facility Types & Locations

### Facility Types

| Code | Arabic Label | Color |
|------|-------------|-------|
| pitch | ملعب | #0D7377 |
| court | كورت | #7C3AED |
| hall | قاعة | #0284C7 |
| lane | حارة | #059669 |
| table | طاولة | #D97706 |
| device | جهاز | #DC2626 |
| track | مضمار | #EA580C |
| pool | حمام سباحة | #2563EB |

### Locations

| Code | Arabic Label |
|------|-------------|
| indoor | داخلي |
| outdoor | خارجي |

---

## 10. Time Slot Types & Access Control

### Slot Types

| Code | Arabic | Description |
|------|--------|-------------|
| available | متاح | Open for booking |
| maintenance | صيانة | Under maintenance |
| academy_reserved | محجوز للأكاديمية | Reserved for academy use |
| blocked | محظور | Manually blocked |

### Access Types (per slot)

| Code | Arabic | Description |
|------|--------|-------------|
| all/open | الكل | Anyone can book |
| members_only | أعضاء فقط | Only active club members |
| vip | VIP فقط | Only VIP/premium/gold members |

---

## 11. Pricing Logic

### Rate Tiers
- **AM (before 4 PM)**: `hourly_rate_member` / `hourly_rate_nonmember`
- **PM (4 PM and after)**: `hourly_rate_member_pm` / `hourly_rate_nonmember_pm`

### Price Resolution Priority (AccessControlService)
1. **Slot-level price**: `member_price` / `guest_price` on `facility_time_slots` (if non-zero)
2. **Facility-level price**: AM/PM rates from `facilities` table (fallback)

### Availability Logic (FacilityService::getAvailableSlots)
```
1. Determine day_of_week from requested date
2. Get all active slots for that facility + day where slot_type = 'available'
3. For each slot, check if start_time is blacked out on that specific date
4. Return only non-blocked slots
```

---

## 12. Access Control Flow (AccessControlService::checkAccess)

```
1. Find matching time_slot for (facility, day_of_week, time)
2. If no slot → DENY ("no available slot at this time")
3. Check access_type:
   - members_only → verify member has active membership
   - vip → verify member has vip/premium/gold membership_type
   - all → allow
4. Determine price (slot override or facility rate)
5. Return {allowed, price, access_type, slot_id, is_member}
```

---

## 13. High-Risk Areas

### 13.1 AccessControlService Queries `members` with Wrong Column Name
- Queries `membership_status` column: `WHERE membership_status = 'active'`
- The actual `members` table uses column `status` not `membership_status`
- Also queries `membership_type IN ('vip', 'premium', 'gold')` but the column is `membership_type` which exists
- This could cause access control to ALWAYS deny member access

### 13.2 `facility_units` Table Does Not Exist
- `FacilityController::createTimeSlot()` queries `facility_units` table
- This table is NOT in production — will throw a MySQL error
- The `facility_unit_id` column in the time slot INSERT is also missing from schema
- Time slot creation form is effectively broken

### 13.3 Blackout Date Removal — Awkward Delete Pattern
- `removeBlackout()` uses a convoluted pattern: no-op update, query for verification, then raw DELETE
- The raw `$db->execute()` call bypasses the model layer
- Could leave orphan references if other tables ever FK to blackout_dates

### 13.4 No Sidebar Menu Registration
- bootstrap.php comment: "sidebar menu owned by Disciplines bootstrap.php"
- But Disciplines bootstrap also doesn't register it
- Navigation must be handled by a parent module or hardcoded elsewhere

### 13.5 `pool_bookings` Table Used for All Entry Types
- `AccessControlService::recordFacilityEntry()` inserts into `pool_bookings` table
- Despite the name, this is used for ALL facility types (not just pools)
- Table name is misleading — future developers may not find entry records here

### 13.6 Overlap with SportsActivity Module
- SportsActivity has its own `FacilityController` at `/sa/facilities` with manage routes
- SportsActivity bootstrap registers `sa.facility.view` and `sa.facility.manage` permissions
- Two separate management paths for the same `facilities` table data

---

## 14. Known Patterns & Gotchas

1. **config_json stores physical attributes**: surface_type, has_lighting, has_changing_rooms, dimensions — informational only, not used in logic
2. **maintenance_schedule_json and peak_hours_json exist in DB** but are NOT in the model's `$fillable` array and not set by any controller method
3. **linked_discipline_id is optional**: Allows filtering facilities by sport type but has no enforcement
4. **Time slots are weekly recurring**: day_of_week based — for one-off blocks, use blackout_dates
5. **Blackout can be full-day or partial**: If start_time/end_time are NULL, the entire day is blocked
6. **FacilityService::getAvailableSlots** only returns slots where slot_type = 'available' — maintenance/academy_reserved/blocked are excluded
7. **Carnet guest pricing**: `carnet_guest_price` column exists on time_slots but is not used anywhere in the current PHP code
8. **`$_SESSION['user_id']` used directly** in addBlackout and blockSlot — should use App session but bypasses it
9. **Dual rate columns**: `hourly_rate_member`/`hourly_rate_nonmember` (AM) and `hourly_rate_member_pm`/`hourly_rate_nonmember_pm` (PM) on the facility itself, PLUS `member_price`/`guest_price` on individual time slots
