# Academies Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Academies module manages the **definition, configuration, and enrollment** of sports academies within the club. It provides:
- Academy CRUD (create, list, show, edit, toggle active/inactive)
- Level management within each academy (age-based grouping, capacity)
- Schedule management (day/time/facility/coach assignments)
- Enrollment lifecycle (enroll, promote, suspend, drop, reactivate)
- Age eligibility checking for academy levels

It does **NOT** directly manage:
- Academy financial contracts (AcademyContracts module)
- Player registration (PlayerAffairs module)
- Subscription billing (ActivitySubscriptions module)
- Training groups (SportsActivity/TrainingGroups modules)
- Coach assignments (Coaches module manages linking coaches to academies)

**Classification:** This is a fully independent module with its own controllers, routes, views, services, and models. NOT a legacy wrapper or redirect to SportsActivity.

---

## 2. Directory & File Structure

```
app/Modules/Academies/
├── bootstrap.php                          # Permission registration (3 permissions)
├── Routes.php                             # Web routes (11 routes)
├── Controllers/
│   └── AcademyController.php             # Full CRUD + levels + schedules
├── Models/
│   ├── Academy.php                       # Main model (soft delete, author tracking)
│   ├── AcademyLevel.php                  # Level sub-entity (no soft delete)
│   └── AcademySchedule.php              # Schedule sub-entity (no soft delete)
├── Services/
│   ├── AcademyService.php               # Query helpers (getActive, getByDiscipline, grouped)
│   └── EnrollmentService.php            # Enrollment lifecycle (enroll, promote, suspend, drop)
└── Views/
    ├── index.php                         # Paginated list with type/discipline filters
    ├── create.php                        # Create form (name, code, discipline, type, config)
    ├── edit.php                          # Edit form
    └── show.php                          # Detail page (levels table, schedule table)
```

---

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

### 3.1 `academies` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(50) | NO | UNI | Auto-generated from name if empty |
| name_ar | varchar(200) | NO | | Arabic name (required) |
| name_en | varchar(200) | YES | | English name |
| discipline_id | bigint unsigned | NO | MUL | FK to sport_disciplines |
| academy_type | varchar(50) | NO | MUL | football/gymnastics/swimming/combat/racket/general |
| description_ar | text | YES | | Arabic description |
| config_json | json | YES | | age_min, age_max, capacity, requires_medical_cert, requires_guardian_consent_under |
| sort_order | int unsigned | NO | | Default: 0 |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| is_archived | tinyint(1) | NO | | Soft delete flag, default: 0 |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | On update cascade |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |

### 3.2 `academy_levels` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| code | varchar(50) | NO | | Unique within academy |
| name_ar | varchar(200) | NO | | |
| name_en | varchar(200) | YES | | |
| level_order | int unsigned | NO | | Sorting order (0-based) |
| age_min | int unsigned | YES | | Minimum age for level |
| age_max | int unsigned | YES | | Maximum age for level |
| max_capacity | int unsigned | YES | | Player cap per level |
| config_json | json | YES | | Additional config |
| is_active | tinyint(1) | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.3 `academy_schedules` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| level_id | bigint unsigned | YES | MUL | FK to academy_levels (optional) |
| facility_id | bigint unsigned | YES | MUL | FK to facilities (optional) |
| day_of_week | tinyint unsigned | NO | MUL | 0=Sunday .. 6=Saturday |
| start_time | time | NO | | |
| end_time | time | NO | | |
| coach_name | varchar(200) | YES | | Free-text coach name |
| coach_id | bigint unsigned | YES | MUL | FK to coaches |
| session_type | varchar(20) | NO | | Default: 'group' |
| season | varchar(20) | YES | | e.g., '2025/2026' |
| is_active | tinyint(1) | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.4 `academy_enrollments` Table (managed by EnrollmentService)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | bigint unsigned | NO | MUL | FK to players |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| level_id | bigint unsigned | YES | MUL | FK to academy_levels |
| schedule_id | bigint unsigned | YES | MUL | FK to academy_schedules |
| season | varchar(20) | YES | MUL | e.g., '2025/2026' |
| enrolled_at | timestamp | NO | | |
| enrollment_day | tinyint unsigned | YES | | Day of month enrolled |
| status | varchar(20) | NO | MUL | Default: 'active' (active/promoted/suspended/dropped) |
| promoted_from_id | bigint unsigned | YES | MUL | FK to previous enrollment (self-reference) |
| dropped_at | timestamp | YES | | When dropped |
| dropped_reason | varchar(500) | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | FK to employees |

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /academies | index | auth | academy.view |
| GET | /academies/create | create | auth | academy.manage |
| POST | /academies | store | auth, csrf | academy.manage |
| GET | /academies/{id} | show | auth | academy.view |
| GET | /academies/{id}/edit | edit | auth | academy.manage |
| POST | /academies/{id} | update | auth, csrf | academy.manage |
| POST | /academies/{id}/toggle | toggle | auth, csrf | academy.manage |
| POST | /academies/{id}/levels | addLevel | auth, csrf | academy.manage |
| POST | /academies/{id}/levels/remove | removeLevel | auth, csrf | academy.manage |
| POST | /academies/{id}/schedules | addSchedule | auth, csrf | academy.manage |
| POST | /academies/{id}/schedules/remove | removeSchedule | auth, csrf | academy.manage |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `academy.enrollment_created` | `{enrollment_id, player_id, academy_id, level_id}` | EnrollmentService::enroll |
| `academy.enrollment_promoted` | `{old_enrollment_id, new_enrollment_id, player_id, academy_id, new_level_id}` | EnrollmentService::promote |
| `academy.enrollment_suspended` | `{enrollment_id, player_id, academy_id, reason}` | EnrollmentService::suspend |
| `academy.enrollment_dropped` | `{enrollment_id, player_id, academy_id, reason}` | EnrollmentService::drop |
| `academy.enrollment_reactivated` | `{enrollment_id, player_id, academy_id}` | EnrollmentService::reactivate |

---

## 6. Events Consumed

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

---

## 7. Cross-Module Dependencies

### 7.1 Academies module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Disciplines | `SportDiscipline::allActive()`, `SportDiscipline::find()` (dropdown, validation) |
| Facilities | `Facility` model imported but not currently used in controllers (available for schedule linking) |

### 7.2 Other modules that IMPORT FROM Academies:

| Module | What it imports |
|--------|----------------|
| AcademyContracts | `Academy::allActive()`, `Academy::find()` (contract links to academy) |
| Coaches | `CoachService::assignToAcademy()` / `unassignFromAcademy()`, `getAcademyAssignments()` |
| PlayerAffairs | `AcademyEnrollment` model (has own copy), `apiAcademies` endpoint, show page enrollments |
| ActivitySubscriptions | `Academy::allActive()` for enrollment wizard |
| Disciplines | `SportsDashboardController` queries `academies` table count |
| Facilities | `FacilityTimeSlot` has `linked_academy_id` column |
| SportsActivity | Has its own `AcademyContractController` at `/sa/academies/{aid}/contracts` |

---

## 8. Permissions

| Key | Arabic | English |
|-----|--------|---------|
| academy.view | عرض الأكاديميات | View Academies |
| academy.manage | إدارة الأكاديميات | Manage Academies |
| academy.enroll | تسجيل في الأكاديمية | Enroll in Academy |

---

## 9. Academy Types

| Code | Arabic Label | Color |
|------|-------------|-------|
| football | كرة قدم | #0D7377 |
| gymnastics | جمباز | #7C3AED |
| swimming | سباحة | #2563EB |
| combat | فنون قتالية | #DC2626 |
| racket | راكيت | #D97706 |
| general | عام | #059669 |

---

## 10. High-Risk Areas

### 10.1 EnrollmentService — Duplicate Enrollment Check
- Checks for existing `active` enrollment before inserting
- No transaction wrapping — race condition possible under concurrent enrollment

### 10.2 Code Auto-Generation
- If no code is provided, one is generated from the name + timestamp
- If a code collision is detected, random bytes are appended
- Not atomic — two simultaneous creates with same name could theoretically collide

### 10.3 Level "Remove" is Soft-Deactivation
- `removeLevel()` sets `is_active = 0` — does NOT delete the row
- `removeSchedule()` also sets `is_active = 0`
- Existing enrollments referencing deactivated levels remain intact

### 10.4 Overlap with SportsActivity Module
- SportsActivity module has its own `AcademyContractController` at `/sa/academies/{aid}/contracts`
- Potential for confusion about which contract management path is canonical
- The standalone AcademyContracts module appears to be the primary/newer version

### 10.5 PlayerAffairs Has Duplicate AcademyEnrollment Model
- `app/Modules/PlayerAffairs/Models/AcademyEnrollment.php` exists as a separate model class
- Could diverge from the Academies module's enrollment service logic

---

## 11. Known Patterns & Gotchas

1. **config_json is flexible**: Stores age ranges, capacity, medical cert requirements — schema is not enforced at DB level
2. **No sidebar menu registered**: bootstrap.php only registers permissions, not a MenuRegistry entry — navigation is likely handled by a parent SportsActivity or Disciplines sidebar
3. **Schedule coach_name vs coach_id**: The DB has both a free-text `coach_name` and a FK `coach_id` — the controller currently only uses `coach_name`
4. **session_type column**: The DB has `session_type` (default: 'group') but the controller does not set it
5. **Enrollment uses `players` table**, not `members` table — academy players are distinct from club members
