# TrainingGroups Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The TrainingGroups module manages **training group lifecycle and player allocation** within the academy/sports system. It handles:
- Training group CRUD (create, edit, toggle active/inactive)
- Player assignment to groups (add, remove, transfer between groups)
- Capacity management (current_count, is_full flag)
- Waiting list management (when groups are full)
- Group transfer requests (formal request/approval workflow)
- Auto-assignment of players to best-fit groups

It does **NOT** directly manage:
- Academies, levels, or disciplines (SportsActivity module)
- Coaches (Coaches module)
- Players (Players module)
- Sessions or attendance (Sessions module)
- Academy enrollments (ActivitySubscriptions/SportsActivity)
- Scheduling (Sessions/Scheduling modules)

---

## 2. Directory & File Structure

```
app/Modules/TrainingGroups/
├── bootstrap.php                              # Permission registration only (no menu)
├── Routes.php                                 # 18 routes (groups, waiting list, transfers)
├── Controllers/
│   ├── TrainingGroupController.php           # Group CRUD + player add/remove/transfer
│   ├── WaitingListController.php             # Waiting list management (offer/cancel)
│   └── TransferRequestController.php         # Formal transfer request workflow
├── Models/
│   ├── TrainingGroup.php                     # Main model (soft delete, auto-author)
│   ├── GroupMembership.php                   # Player-to-group assignment records
│   ├── GroupTransferRequest.php              # Formal transfer request records
│   └── WaitingListEntry.php                  # Waiting list entries
├── Services/
│   ├── AutoAssignmentService.php             # Smart group assignment (score-based matching)
│   ├── GroupTransferService.php              # Transfer request submit/approve/reject
│   └── WaitingListService.php                # Process open spots, offer to waiting players
└── Views/
    ├── index.php                             # Group listing with filters
    ├── create.php                            # Create group form
    ├── edit.php                              # Edit group form
    ├── show.php                              # Group detail (members, add/remove/transfer)
    ├── waiting_list.php                      # Waiting list management
    ├── transfer_requests.php                 # Transfer request listing
    └── transfer_request_create.php           # Create transfer request form
```

---

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

### 3.1 `training_groups` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| code | varchar(50) | NO | UNI | | Auto-generated code (TG_{name}_{timestamp}) |
| name_ar | varchar(255) | NO | | | Arabic group name |
| name_en | varchar(255) | YES | | NULL | English group name |
| academy_id | bigint unsigned | NO | MUL | | FK to academies |
| level_id | bigint unsigned | NO | MUL | | FK to academy_levels |
| coach_id | bigint unsigned | YES | MUL | NULL | FK to coaches |
| facility_id | bigint unsigned | YES | MUL | NULL | FK to facilities |
| group_type | varchar(20) | NO | | group | private/small_group/group/team |
| min_capacity | int unsigned | NO | | 1 | |
| max_capacity | int unsigned | NO | | 20 | |
| current_count | int unsigned | NO | | 0 | Maintained manually on add/remove |
| age_from | int unsigned | YES | | NULL | Min age restriction |
| age_to | int unsigned | YES | | NULL | Max age restriction |
| gender_restriction | varchar(10) | YES | | NULL | male/female or NULL for mixed |
| day_of_week | tinyint unsigned | YES | | NULL | 0=Sunday..6=Saturday |
| start_time | time | YES | | NULL | |
| end_time | time | YES | | NULL | |
| pricing_tier | varchar(20) | NO | | standard | premium/standard/economy |
| is_full | tinyint(1) | NO | MUL | 0 | Set when current_count >= max_capacity |
| is_active | tinyint(1) | NO | | 1 | |
| is_archived | tinyint(1) | NO | | 0 | Soft delete flag |
| archived_at | timestamp | YES | | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | on update |
| created_by | bigint unsigned | YES | | NULL | FK to employees |
| updated_by | bigint unsigned | YES | | NULL | FK to employees |

### 3.2 `group_memberships` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| group_id | bigint unsigned | NO | MUL | | FK to training_groups |
| enrollment_id | bigint unsigned | NO | MUL | | FK to academy_enrollments |
| player_id | bigint unsigned | NO | MUL | | FK to players |
| joined_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| left_at | timestamp | YES | | NULL | Set on removal/transfer |
| status | varchar(20) | NO | | active | active/removed/transferred |
| transferred_to_group_id | bigint unsigned | YES | | NULL | FK to training_groups |
| transfer_reason | varchar(500) | YES | | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |

### 3.3 `group_transfer_requests` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | int unsigned | NO | PRI | auto_increment | |
| player_id | int unsigned | NO | MUL | FK to players | |
| from_group_id | int unsigned | NO | MUL | FK to training_groups | |
| to_group_id | int unsigned | YES | MUL | NULL | FK to training_groups (null = unspecified) |
| reason_ar | text | YES | | NULL | Arabic reason text |
| status | enum | NO | MUL | pending | pending/approved/rejected/waitlisted |
| reviewed_by | int unsigned | YES | | NULL | FK to employees |
| reviewed_at | datetime | YES | | NULL | |
| review_notes | text | YES | | NULL | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |
| updated_at | datetime | NO | | CURRENT_TIMESTAMP | on update |

### 3.4 `waiting_list` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| player_id | bigint unsigned | NO | MUL | FK to players | |
| discipline_id | bigint unsigned | NO | MUL | FK to sport_disciplines | |
| level_id | bigint unsigned | YES | | NULL | FK to academy_levels |
| preferred_day_of_week | tinyint unsigned | YES | | NULL | 0-6 |
| preferred_time | varchar(5) | YES | | NULL | AM/PM |
| priority | int unsigned | NO | | 100 | Lower = higher priority |
| requested_group_type | varchar(20) | YES | | NULL | |
| status | varchar(20) | NO | MUL | waiting | waiting/offered/cancelled/accepted |
| offered_group_id | bigint unsigned | YES | | NULL | FK to training_groups |
| offered_at | timestamp | YES | | NULL | |
| offer_expires_at | timestamp | YES | | NULL | 48 hours after offer |
| notes | text | YES | | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | on update |

### 3.5 Related Tables (owned by other modules)

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| academies | SportsActivity | academy_id FK on training_groups |
| academy_levels | SportsActivity | level_id FK on training_groups |
| academy_enrollments | SportsActivity/ActivitySubscriptions | enrollment_id FK on group_memberships |
| coaches | Coaches | coach_id FK on training_groups |
| facilities | SportsActivity | facility_id FK on training_groups |
| players | Players | player_id FK on memberships and requests |
| sport_disciplines | SportsActivity | discipline_id FK on waiting_list |

---

## 4. Routes

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/training-groups` | TrainingGroupController@index | training_group.view |
| GET | `/training-groups/create` | TrainingGroupController@create | training_group.manage |
| POST | `/training-groups` | TrainingGroupController@store | training_group.manage |
| GET | `/training-groups/{id}` | TrainingGroupController@show | training_group.view |
| GET | `/training-groups/{id}/edit` | TrainingGroupController@edit | training_group.manage |
| POST | `/training-groups/{id}` | TrainingGroupController@update | training_group.manage |
| POST | `/training-groups/{id}/toggle` | TrainingGroupController@toggle | training_group.manage |
| POST | `/training-groups/{id}/add-player` | TrainingGroupController@addPlayer | training_group.manage |
| POST | `/training-groups/{id}/remove-player` | TrainingGroupController@removePlayer | training_group.manage |
| POST | `/training-groups/{id}/transfer-player` | TrainingGroupController@transferPlayer | training_group.manage |
| GET | `/waiting-list` | WaitingListController@index | training_group.view |
| POST | `/waiting-list/{id}/offer` | WaitingListController@offer | training_group.manage |
| POST | `/waiting-list/{id}/cancel` | WaitingListController@cancel | training_group.manage |
| GET | `/transfer-requests` | TransferRequestController@index | training_group.manage |
| GET | `/transfer-requests/create` | TransferRequestController@create | training_group.manage |
| POST | `/transfer-requests` | TransferRequestController@store | training_group.manage |
| POST | `/transfer-requests/{id}/approve` | TransferRequestController@approve | training_group.manage |
| POST | `/transfer-requests/{id}/reject` | TransferRequestController@reject | training_group.manage |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `group.transfer_requested` | `{request_id, player_id, from_group_id, to_group_id}` | GroupTransferService::submitRequest |
| `group.transfer_approved` | `{request_id, player_id, from_group_id, to_group_id}` | GroupTransferService::approveRequest |

---

## 6. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `group.transfer_approved` | Notifications | Sends notification via NotificationTriggerService |

---

## 7. Cross-Module Dependencies

### 7.1 TrainingGroups IMPORTS FROM (via direct DB queries):

| Module/Table | What it uses |
|--------------|-------------|
| SportsActivity | `academies` (name, discipline_id), `academy_levels` (name, level_order), `academy_enrollments` (player enrollment status), `facilities` (name), `sport_disciplines` (name) |
| Coaches | `coaches` table (full_name_ar for display) |
| Players | `players` table (full_name_ar, date_of_birth, gender, phone) |

### 7.2 Other modules that IMPORT FROM TrainingGroups:

| Module | What it imports |
|--------|----------------|
| TrainerPortal | Reads `training_groups` for coach schedule display |
| ParentPortal | Reads `training_groups`/`group_memberships` for parent view of child's groups |
| Scheduling | Reads `training_groups` for conflict detection |
| Reservations | Reads `training_groups` for facility conflict detection |
| Sessions | Reads `training_groups` for session management, attendance |
| PlayerApi | Reads group/transfer data for player-facing API |
| Alerts | Reads `training_groups` for alert processing |
| Disciplines | Reads waiting list/group data for sports dashboard |
| Reports | Reads `group_memberships` for player retention reports |
| Notifications | Listens to `group.transfer_approved` event |

### 7.3 AutoAssignmentService (Entry Point)

`AutoAssignmentService::assign()` is a key integration point — designed to be called by enrollment flows when a player needs to be placed in a group. It:
1. Queries available groups by discipline + level
2. Filters by age, gender, time preference
3. Scores groups (age match +10, day preference +5, optimal density +3)
4. Assigns player to best group OR adds to waiting list

Currently no external module directly calls this service (it may be called from enrollment wizards not yet connected).

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| training_group.view | View training groups and waiting list |
| training_group.manage | Create, edit, toggle, add/remove players, manage transfers and waiting list |

**Note:** No sidebar menu is registered in bootstrap.php — groups are accessed via the SportsActivity or Disciplines module navigation.

---

## 9. Group Type & Pricing Model

### Group Types
| Code | Arabic | Capacity Range |
|------|--------|---------------|
| private | خاص (فردي) | 1 player |
| small_group | مجموعة صغيرة | 2-5 players |
| group | مجموعة | 6-15 players |
| team | فريق | 16+ players |

### Pricing Tiers
| Code | Arabic |
|------|--------|
| premium | بريميوم |
| standard | عادي |
| economy | اقتصادي |

---

## 10. Key Business Flows

### 10.1 Add Player to Group
```
1. Check player not already in group (active status)
2. Check group not full (current_count < max_capacity)
3. Insert into group_memberships (status='active')
4. Increment training_groups.current_count
5. Update is_full flag if at capacity
```

### 10.2 Remove Player from Group
```
1. Update group_memberships: status='removed', left_at=now
2. Decrement training_groups.current_count, set is_full=0
3. Call WaitingListService::processOpenSpot() to offer spot to waiting players
```

### 10.3 Transfer Player Between Groups
```
1. Mark old membership as 'transferred' with left_at and transferred_to_group_id
2. Create new membership in target group
3. Update counts on both source and target groups
4. Process waiting list for open spot in source group
```

### 10.4 Waiting List Flow
```
1. Player added to waiting_list when no suitable group available (or group full)
2. When spot opens: WaitingListService::processOpenSpot() auto-checks eligible waiters
3. Eligibility checked: discipline, level, age, gender, day preference, time preference
4. First eligible player gets offer (status='offered', 48-hour expiration)
5. Admin can also manually offer spots via UI
```

### 10.5 Transfer Request Workflow
```
1. Staff submits transfer request (player_id, from_group_id, optional to_group_id)
2. Status = 'pending'
3. Reviewer can approve or reject:
   - Approve: removes from source group_players, adds to target (or waitlists if full)
   - Reject: marks rejected with notes
4. If target full on approval: status becomes 'waitlisted'
```

---

## 11. High-Risk Areas

### 11.1 Denormalized current_count
- `training_groups.current_count` is manually maintained (incremented/decremented)
- If any add/remove path fails mid-way, count drifts from actual `group_memberships` active count
- No reconciliation mechanism exists
- **Impact:** Groups may incorrectly show as full or have wrong available spots

### 11.2 Two Parallel Transfer Mechanisms
- **Direct transfer** (TrainingGroupController::transferPlayer): Immediate, operates on `group_memberships` table
- **Transfer request** (GroupTransferService): Workflow-based with approval, operates on `group_players` table
- These use **different tables** (`group_memberships` vs `group_players`) which suggests they were built at different times
- Risk: Data inconsistency between the two tracking tables

### 11.3 Waiting List Offer Expiration
- Offers expire after 48 hours (`offer_expires_at`)
- No cron job or scheduled task checks for expired offers
- Expired offers remain in 'offered' status indefinitely unless manually cancelled
- **Impact:** Spots stay "reserved" for non-responding players

### 11.4 No Transaction Wrapping
- Multi-step operations (transfer player, remove + process waiting list) are not wrapped in DB transactions
- If any step fails, partial state changes persist

### 11.5 Race Conditions on Capacity
- Capacity check and increment are not atomic
- Two concurrent requests could both pass the `current_count < max_capacity` check
- **Impact:** Group could exceed max_capacity

---

## 12. Known Patterns & Gotchas

1. **No menu registration**: Module has no sidebar entry — accessed via SportsActivity links or direct URL
2. **Code auto-generation**: Group codes are generated as `TG_{name_prefix}_{timestamp%100000}` — not guaranteed unique in theory
3. **Academy scoping**: Players available for add are filtered by `academy_enrollments` for the group's academy
4. **Transfer between groups**: Only allows transfer to groups in the same academy
5. **Group types are display-only**: The `group_type` field doesn't enforce capacity ranges — `max_capacity` is set independently
6. **Day of week numbering**: 0=Sunday, 6=Saturday (PHP convention)
7. **Pricing tier is stored but not used**: The `pricing_tier` column exists but no pricing logic references it within this module
