# Tournaments Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Tournaments module is a **full-featured competition management system** that handles:
- Tournament creation and configuration (knockout, round-robin, group stage formats)
- Registration lifecycle (open/close registration, participant eligibility)
- Bracket/fixture generation (knockout seeding, round-robin scheduling)
- Match result recording with automatic standings updates
- Winner advancement in knockout brackets
- Tournament completion and winner determination
- Standings calculation (points, wins, losses, draws)

It does **NOT** manage:
- Live match events/timeline (MatchCenter module handles that)
- Training sessions or groups (Training/TrainingGroups modules)
- Player profiles (Players module)
- Facility booking (Reservations module)
- Financial processing of entry fees (Payments/Accounting modules listen to events)

---

## 2. Directory & File Structure

```
app/Modules/Tournaments/
├── bootstrap.php                              # Menu + permission registration
├── Routes.php                                 # 17 routes (CRUD, participants, matches)
├── Controllers/
│   ├── TournamentController.php              # Tournament CRUD + lifecycle actions
│   ├── ParticipantController.php             # Register/withdraw participants, standings
│   └── MatchController.php                   # Match listing, result recording, rescheduling
├── Models/
│   ├── Tournament.php                        # Main model (types, formats, statuses, search)
│   ├── TournamentParticipant.php             # Participant model (standings, stats)
│   └── TournamentMatch.php                   # Match model (bracket, per-round queries)
├── Services/
│   └── TournamentService.php                 # Business logic (bracket gen, results, completion)
└── Views/
    ├── index.php                             # Tournament listing with filters
    ├── create.php                            # Create/edit form (shared)
    ├── show.php                              # Tournament detail (participants, bracket, standings)
    ├── bracket.php                           # Visual bracket display
    ├── matches.php                           # Match listing grouped by round
    ├── match_result.php                      # Record result form
    └── standings.php                         # Standings table
```

---

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

### 3.1 `tournaments` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| name_ar | varchar(300) | NO | | | Arabic tournament name |
| name_en | varchar(300) | YES | | NULL | English name |
| discipline_id | bigint unsigned | NO | MUL | | FK to sport_disciplines |
| tournament_type | varchar(30) | NO | MUL | | internal/friendly/league/championship/knockout/round_robin |
| format | varchar(30) | NO | | knockout | knockout/round_robin/group_stage/swiss/ladder |
| age_group | varchar(50) | YES | | NULL | e.g. "8-12", "under_16", "18+" |
| gender | varchar(10) | YES | | mixed | male/female/mixed |
| max_participants | int unsigned | YES | | NULL | Cap on registrations |
| min_participants | int unsigned | YES | | 2 | Minimum to generate bracket |
| registration_start | date | YES | | NULL | |
| registration_end | date | YES | | NULL | |
| start_date | date | NO | MUL | | Tournament start |
| end_date | date | YES | | NULL | |
| facility_id | bigint unsigned | YES | | NULL | FK to facilities |
| entry_fee | decimal(10,2) | NO | | 0.00 | Per-participant fee |
| prize_description_ar | text | YES | | NULL | Arabic prize details |
| rules_ar | text | YES | | NULL | Arabic rules text |
| status | varchar(20) | NO | MUL | draft | See statuses below |
| winner_player_id | bigint unsigned | YES | | NULL | FK to players (set on completion) |
| winner_team_name | varchar(200) | YES | | NULL | Winning team/player name |
| organizer_id | bigint unsigned | YES | | NULL | FK to employees |
| banner_image | varchar(500) | YES | | NULL | Banner image path |
| notes | text | YES | | NULL | Internal notes |
| 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 |

**Status lifecycle:** `draft` -> `registration_open` -> `registration_closed` -> `in_progress` -> `completed` (or `cancelled`)

### 3.2 `tournament_participants` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| tournament_id | bigint unsigned | NO | MUL | | FK to tournaments |
| player_id | bigint unsigned | YES | MUL | NULL | FK to players |
| team_name | varchar(200) | YES | | NULL | For team-based tournaments |
| team_members_json | json | YES | | NULL | Team member details (JSON) |
| seed_number | int unsigned | YES | | NULL | Assigned during bracket generation |
| registration_date | datetime | NO | | | When registered |
| status | varchar(20) | NO | MUL | registered | See statuses below |
| final_position | int unsigned | YES | | NULL | 1=winner, 2=runner_up, etc. |
| points | int unsigned | NO | | 0 | Accumulated tournament points |
| wins | int unsigned | NO | | 0 | |
| losses | int unsigned | NO | | 0 | |
| draws | int unsigned | NO | | 0 | |
| payment_id | bigint unsigned | YES | | NULL | FK to payments (entry fee) |
| notes | text | YES | | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |

**Participant statuses:** `registered`, `confirmed`, `withdrawn`, `disqualified`, `eliminated`, `winner`, `runner_up`

### 3.3 `tournament_matches` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| tournament_id | bigint unsigned | NO | MUL | | FK to tournaments |
| round_number | int unsigned | NO | MUL | 1 | Round in bracket |
| match_number | int unsigned | NO | | | Sequential within tournament |
| group_name | varchar(10) | YES | | NULL | For group-stage format |
| participant_a_id | bigint unsigned | YES | | NULL | FK to tournament_participants |
| participant_b_id | bigint unsigned | YES | | NULL | FK to tournament_participants |
| scheduled_date | date | YES | MUL | NULL | |
| scheduled_time | time | YES | | NULL | |
| facility_id | bigint unsigned | YES | | NULL | FK to facilities |
| score_a | varchar(50) | YES | | NULL | Flexible score format |
| score_b | varchar(50) | YES | | NULL | Flexible score format |
| winner_participant_id | bigint unsigned | YES | | NULL | FK to tournament_participants |
| is_draw | tinyint(1) | NO | | 0 | |
| status | varchar(20) | NO | MUL | scheduled | scheduled/in_progress/completed/cancelled/walkover |
| referee_name | varchar(200) | YES | | NULL | |
| match_notes | text | YES | | NULL | |
| completed_at | datetime | YES | | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | on update |

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

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| sport_disciplines | SportsActivity | discipline_id FK on tournaments |
| facilities | SportsActivity | facility_id FK on tournaments and matches |
| players | Players | player_id FK on participants and winner |
| payments | Payments | payment_id FK on participants (entry fee) |
| live_matches | MatchCenter | tournament_match_id FK (links live coverage to bracket match) |

---

## 4. Tournament Status Lifecycle

```
draft -> registration_open -> registration_closed -> in_progress -> completed
                                                                  \-> cancelled
```

| Status | Transitions To | Triggered By |
|--------|---------------|--------------|
| draft | registration_open | openRegistration() action |
| registration_open | registration_closed | closeRegistration() action |
| registration_closed | in_progress | generateBracket() (auto-sets in_progress) |
| in_progress | completed | completeTournament() |
| any | cancelled | Manual (not implemented in routes) |

---

## 5. Routes

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/tournaments` | TournamentController@index | tournament.view |
| GET | `/tournaments/create` | TournamentController@create | tournament.manage |
| POST | `/tournaments` | TournamentController@store | tournament.manage |
| GET | `/tournaments/{id}` | TournamentController@show | tournament.view |
| GET | `/tournaments/{id}/edit` | TournamentController@edit | tournament.manage |
| POST | `/tournaments/{id}` | TournamentController@update | tournament.manage |
| POST | `/tournaments/{id}/open-registration` | TournamentController@openRegistration | tournament.manage |
| POST | `/tournaments/{id}/close-registration` | TournamentController@closeRegistration | tournament.manage |
| POST | `/tournaments/{id}/generate-bracket` | TournamentController@generateBracket | tournament.manage |
| POST | `/tournaments/{id}/complete` | TournamentController@complete | tournament.manage |
| POST | `/tournaments/{tId}/participants` | ParticipantController@register | tournament.register_players |
| POST | `/tournaments/{tId}/participants/{pId}/withdraw` | ParticipantController@withdraw | tournament.register_players |
| GET | `/tournaments/{tId}/standings` | ParticipantController@standings | tournament.view |
| GET | `/tournaments/{tId}/matches` | MatchController@index | tournament.view |
| GET | `/matches/{matchId}/result` | MatchController@showResultForm | tournament.record_results |
| POST | `/matches/{matchId}/result` | MatchController@recordResult | tournament.record_results |
| POST | `/matches/{matchId}/reschedule` | MatchController@reschedule | tournament.manage |

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `tournament.created` | `{tournament_id, name, discipline_id}` | TournamentService::createTournament |
| `tournament.participant_registered` | `{tournament_id, participant_id, player_id}` | TournamentService::registerParticipant |
| `tournament.bracket_generated` | `{tournament_id, format, participant_count, match_count}` | TournamentService::generateBracket |
| `tournament.match_completed` | `{tournament_id, match_id, winner_id, is_draw}` | TournamentService::recordMatchResult |
| `tournament.completed` | `{tournament_id, winner_player_id, winner_name}` | TournamentService::completeTournament |

**Note:** `tournament.fee_collected` is listened to by Accounting but is NOT dispatched from this module — it must be dispatched from a payment processing flow elsewhere.

---

## 7. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `tournament.fee_collected` | Accounting | Posts journal entry (debit cash, credit tournament revenue) |
| `tournament.registration_open` | Notifications | Triggers notification to eligible players |

---

## 8. Cross-Module Dependencies

### 8.1 Tournaments IMPORTS FROM (via direct DB queries):

| Module/Table | What it uses |
|--------------|-------------|
| SportsActivity | `sport_disciplines` (name for display/filter), `facilities` (name for display) |
| Players | `players` table (full_name_ar, date_of_birth, gender for eligibility checks) |

### 8.2 Other modules that IMPORT FROM Tournaments:

| Module | What it imports |
|--------|----------------|
| MatchCenter | `tournament_match_id` FK on `live_matches` — links live match coverage to tournament bracket |
| Accounting | Listens to `tournament.fee_collected` for journal entries |
| Notifications | Listens to `tournament.registration_open` for push notifications |
| PlayerAffairs | References tournaments in injury tracking context |
| Reservations | References tournaments in private match bookings |

---

## 9. Permissions

| Key | Description |
|-----|-------------|
| tournament.view | View tournament list, details, bracket, standings |
| tournament.manage | Create, edit, lifecycle actions (open/close reg, generate bracket, complete) |
| tournament.register_players | Register/withdraw participants |
| tournament.record_results | Record match results |

---

## 10. Sidebar Menu

Registered under `tournaments` with order 260:
- Parent: "البطولات" (Tournaments) at `/tournaments` with trophy icon
- Child 1: "جميع البطولات" (All Tournaments) at `/tournaments`
- Child 2: "إنشاء بطولة" (Create Tournament) at `/tournaments/create`

---

## 11. Key Business Flows

### 11.1 Bracket Generation — Knockout Format
```
1. Get registered/confirmed participants (ordered by seed_number, then random)
2. Shuffle for random seeding
3. Calculate bracket size (next power of 2)
4. Create first-round matches (pair participants sequentially)
5. Auto-advance "bye" matches (odd participant count → some get free pass)
6. Create placeholder matches for subsequent rounds (null participants)
7. Auto-advance bye winners to round 2
8. Set tournament status to 'in_progress'
9. Assign seed numbers to all participants, status = 'confirmed'
```

### 11.2 Bracket Generation — Round Robin Format
```
1. Uses circle method for fair scheduling
2. If odd participants: adds dummy BYE (skipped in match creation)
3. Total rounds = participant_count - 1
4. Every participant plays every other participant exactly once
```

### 11.3 Match Result Recording
```
1. Record scores, winner, draw flag
2. Update participant stats:
   - Winner: wins + 1, points + 3
   - Loser: losses + 1
   - Draw: both draws + 1, both points + 1
3. For knockout: advance winner to next round match
4. For knockout: mark loser as 'eliminated'
5. Dispatch tournament.match_completed event
```

### 11.4 Winner Advancement (Knockout)
```
1. Determine position of current match in its round
2. Calculate next round target: floor(position/2)
3. Determine slot: even position → participant_a_id, odd → participant_b_id
4. Update next-round match with winner in the correct slot
```

### 11.5 Tournament Completion
```
- Knockout: winner = final match's winner
- Round Robin: winner = top of standings (by points, then wins, then differential)
- Sets tournament.status = 'completed', records winner_player_id and winner_team_name
- Marks winner participant status = 'winner', runner_up = 'runner_up'
```

---

## 12. Points System

| Outcome | Points Awarded |
|---------|---------------|
| Win | 3 |
| Draw | 1 |
| Loss | 0 |

Standings ordered by: points DESC, wins DESC, (wins - losses) DESC, id ASC

---

## 13. Player Eligibility Checks

Registration validates:
1. **Tournament status**: Must be `registration_open`
2. **Capacity**: Current active participants < max_participants
3. **Duplicate check**: Player not already registered (excluding withdrawn/disqualified)
4. **Player exists**: Player record must exist in `players` table
5. **Gender**: If tournament has gender restriction (not 'mixed'), player must match
6. **Age group**: Parsed formats supported:
   - `"8-12"` — age range (inclusive)
   - `"under_16"` — maximum age (exclusive)
   - `"18+"` — not explicitly checked (no min-only logic)

---

## 14. High-Risk Areas

### 14.1 Bracket Regeneration Destroys Data
- `generateBracket()` runs `DELETE FROM tournament_matches WHERE tournament_id = ?` before regenerating
- If called after some matches have results, all recorded results are lost
- No guard against calling this on an in_progress tournament

### 14.2 Score Field is VARCHAR
- `score_a` and `score_b` are `varchar(50)` not integers
- Service code casts to string but the values may not be numeric (sets support "6-4, 7-5" format)
- Calculations that compare scores may fail for non-numeric values

### 14.3 Points Accumulation Without Idempotency
- `recordMatchResult()` increments wins/losses/draws each time it's called
- If called twice on the same match (re-recording result), stats double-count
- `updateStandings()` exists for full recalculation but is not called automatically

### 14.4 Knockout Advancement Logic
- Complex positional math (match_number positioning within rounds)
- If matches are deleted and regenerated with different numbering, advancement breaks
- No validation that winner_participant_id is actually one of the match's participants

### 14.5 tournament.fee_collected Event Gap
- Accounting listens to `tournament.fee_collected` but no code in this module dispatches it
- Entry fee collection is presumably handled by a payment flow not yet connected
- **Impact:** Tournament entry fees may not generate accounting entries

---

## 15. Known Patterns & Gotchas

1. **Shared create/edit view**: `create.php` is reused for editing (checks `$isEdit` flag)
2. **No soft delete**: Tournaments table has no `is_archived` column — deletion is permanent
3. **Organizer vs created_by**: `organizer_id` is set from the current employee, `created_by` is auto-set by the Model — they may differ if an admin creates on behalf
4. **Format vs type confusion**: `tournament_type` (internal/friendly/league) and `format` (knockout/round_robin) are separate concepts but both have overlapping values like "knockout"
5. **Random seeding**: Knockout bracket uses `shuffle()` — results are not reproducible
6. **Walkover status**: When a participant has a bye (no opponent), the match is immediately set to 'walkover' with the solo participant as winner
7. **Team support**: `team_name` and `team_members_json` exist for team tournaments but the bracket logic is still 1v1 (team as unit)
