# PlayerAffairs Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The PlayerAffairs module is the **sports player management system** — manages the lifecycle of players (athletes/trainees) in the club's sports academies. It handles:

- Player registration (member-linked or non-member/external)
- Player profile management (personal data, guardian info, school info)
- Player card lifecycle (inactive → active → suspended → revoked)
- Medical record management (upload, approval workflow, expiry tracking)
- Academy enrollment and dropout
- Player evaluations (coach-driven skill assessments with criteria scoring)
- Injury tracking and management (reporting, recovery, clearance)
- Fitness tests (physical metrics, progression tracking)
- Attendance tracking per academy/enrollment
- Player progression (level changes across disciplines)
- Registration serial number sequencing

It does **NOT** directly manage:
- Authentication/tokens for the mobile API (PlayerAuth module)
- Player-facing mobile API endpoints (PlayerApi module)
- The Medical Board approval committee UI (MedicalBoard module)
- Activity subscriptions billing (ActivitySubscriptions module)
- Training groups and schedules (SportsActivity/Disciplines modules)

---

## 2. Directory & File Structure

```
app/Modules/PlayerAffairs/
├── bootstrap.php                              # Permission registration
├── Routes.php                                 # Web routes (30 routes)
├── EventListeners.php                         # Cross-module event handlers
├── Controllers/
│   ├── PlayerController.php                   # Main CRUD, card, medical, enrollment
│   ├── AttendanceController.php               # Academy attendance tracking
│   ├── EvaluationController.php               # Player skill evaluations
│   ├── FitnessController.php                  # Physical fitness tests
│   ├── InjuryController.php                   # Injury reporting and management
│   └── MedicalApprovalController.php          # Internal medical approval workflow
├── Models/
│   ├── Player.php                             # Main player model
│   ├── AcademyEnrollment.php                  # Academy enrollment records
│   ├── EvaluationCriterion.php                # Per-discipline evaluation criteria
│   ├── PlayerDiscipline.php                   # Player-discipline associations
│   ├── PlayerEvaluation.php                   # Evaluation records
│   ├── PlayerFitnessTest.php                  # Fitness test records
│   ├── PlayerInjury.php                       # Injury records
│   └── PlayerMedicalRecord.php                # Medical certificate records
├── Services/
│   ├── PlayerRegistrationService.php          # Registration logic + serial generation
│   ├── PlayerCardService.php                  # Card activate/suspend/revoke
│   ├── MedicalRecordService.php               # Add/approve/reject medical records
│   ├── MedicalExpiryCron.php                  # Cron: check medical expiry
│   ├── MedicalScanService.php                 # Upload scans, expiry notifications
│   ├── EvaluationService.php                  # Submit/approve evaluations, group suggestion
│   ├── InjuryService.php                      # Report injuries, auto-restrict, clearance
│   ├── FitnessService.php                     # Record tests, progression data, clearance
│   ├── PlayerCardService.php                  # Card status management
│   └── PlayerProgressionService.php           # Level progression records
└── Views/
    ├── index.php                              # Player list with filters/pagination
    ├── create.php                             # New player registration form
    ├── edit.php                               # Edit player form
    ├── show.php                               # Player profile (tabs: disciplines, medical, history)
    ├── attendance.php                         # Attendance recording view
    ├── evaluations.php                        # Evaluation list
    ├── evaluation_create.php                  # New evaluation form
    ├── evaluation_show.php                    # Evaluation detail view
    ├── fitness_tests.php                      # Fitness test list
    ├── fitness_create.php                     # New fitness test form
    ├── fitness_show.php                       # Fitness test detail
    ├── injuries.php                           # Injury list
    ├── injury_create.php                      # New injury report form
    ├── injury_show.php                        # Injury detail
    └── medical_approvals.php                  # Medical approval queue (internal)
```

---

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

### 3.1 `players` Table (34 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_type | varchar(20) | NO | MUL | 'member' or 'non_member' |
| member_id | bigint unsigned | YES | MUL | FK to members (null for non-members) |
| activity_id_number | varchar(50) | YES | | Activity ID card number |
| registration_serial | varchar(50) | YES | UNI | Sequential registration serial |
| full_name_ar | varchar(300) | NO | | Arabic full name |
| full_name_en | varchar(300) | YES | | English full name |
| national_id | varchar(20) | YES | MUL | Egyptian 14-digit NID |
| passport_number | varchar(50) | YES | | For foreign players |
| date_of_birth | date | YES | | |
| gender | varchar(10) | YES | | 'male' or 'female' |
| phone | varchar(30) | YES | MUL | Primary contact (also used for auth) |
| password_hash | varchar(255) | YES | | For mobile app login |
| otp_code | varchar(6) | YES | | One-time password for OTP auth |
| otp_expires_at | datetime | YES | | OTP expiry timestamp |
| last_login_at | datetime | YES | | Last mobile app login |
| email | varchar(200) | YES | | |
| address | text | YES | | |
| guardian_name | varchar(300) | YES | | For minors |
| guardian_phone | varchar(30) | YES | | |
| guardian_national_id | varchar(20) | YES | | |
| guardian_relationship | varchar(50) | YES | | father/mother/brother/sister/other |
| photo_path | varchar(500) | YES | | Profile photo path |
| birth_cert_path | varchar(500) | YES | | Birth certificate scan |
| birth_cert_verified | tinyint(1) | NO | | Default: 0 |
| current_level_id | bigint unsigned | YES | | FK to academy_levels |
| overall_rating | decimal(3,1) | YES | | Latest evaluation score |
| medical_status | varchar(30) | YES | MUL | Default: 'pending' |
| medical_expiry_date | date | YES | | When medical clearance expires |
| school_name | varchar(300) | YES | | For student players |
| school_grade | varchar(100) | YES | | |
| registration_fee_paid | tinyint(1) | NO | | Default: 0 |
| card_status | varchar(20) | NO | MUL | Default: 'inactive' |
| access_window_minutes | int unsigned | YES | | Non-member access window |
| notes | text | YES | | |
| branch_id | bigint unsigned | YES | MUL | FK to branches |
| is_archived | tinyint(1) | NO | | Soft delete flag |
| 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 |

**Current data distribution:**
- player_type: non_member=34 (100% non-members currently)
- card_status: active=32, inactive=2
- medical_status: cleared=32, fit=1, pending=1

### 3.2 `player_medical_records` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | bigint unsigned | YES | MUL | FK to players |
| sa_player_id | int unsigned | YES | MUL | FK to sa_players (sports activity players) |
| record_type | varchar(50) | NO | | fitness_cert/medical_exam/blood_test/cardiac_screen/other |
| certificate_type | varchar(30) | YES | | recreational/academy/international |
| validity_months | int unsigned | YES | | Auto-set from certificate_type |
| exam_date | date | YES | | Date of medical examination |
| expiry_date | date | YES | MUL | Certificate expiry date |
| doctor_name | varchar(200) | YES | | |
| clinic_name | varchar(200) | YES | | |
| issuing_authority | varchar(300) | YES | | |
| cert_number | varchar(100) | YES | | Certificate number |
| result | varchar(30) | YES | MUL | pending/fit/conditional/unfit |
| approval_status | varchar(30) | NO | MUL | Default: 'pending' |
| source | varchar(30) | NO | MUL | 'membership' or 'sports_activity' |
| approved_by | bigint unsigned | YES | | FK to employees |
| approved_at | timestamp | YES | | |
| rejection_reason | varchar(500) | YES | | |
| restrictions | text | YES | | Activity restrictions |
| document_id | bigint unsigned | YES | MUL | FK to documents |
| sa_document_id | int unsigned | YES | | FK to sa_player_documents |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |

### 3.3 `player_evaluations` Table

| Column | Type | Nullable | Key | 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 |
| session_id | bigint unsigned | YES | | FK to training sessions |
| evaluator_coach_id | bigint unsigned | YES | MUL | FK to coaches |
| evaluation_date | date | NO | MUL | |
| overall_score | decimal(5,2) | YES | | Weighted average of criteria |
| skill_level | varchar(20) | YES | | beginner/intermediate/advanced/elite |
| recommended_group_type | varchar(20) | YES | | |
| recommended_group_id | bigint unsigned | YES | | FK to training_groups |
| scores_json | json | NO | | Array of {criterion_id, score, notes} |
| strengths_ar | text | YES | | Arabic strengths text |
| weaknesses_ar | text | YES | | Arabic weaknesses text |
| coach_notes | text | YES | | Private coach notes |
| parent_visible | tinyint(1) | NO | | Default: 1 (visible via app) |
| status | varchar(20) | NO | MUL | draft/submitted/approved |
| approved_by | bigint unsigned | YES | | |
| approved_at | datetime | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |
| updated_by | bigint unsigned | YES | | |

### 3.4 `player_injuries` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | bigint unsigned | NO | MUL | FK to players |
| discipline_id | bigint unsigned | YES | | FK to sport_disciplines |
| injury_type | varchar(50) | NO | | e.g., sprain, fracture, concussion |
| body_part | varchar(100) | NO | | |
| severity | varchar(20) | NO | MUL | minor/moderate/severe/critical |
| description_ar | text | NO | | Arabic injury description |
| occurred_date | date | NO | MUL | |
| occurred_during | varchar(30) | YES | | training/match/warmup/other |
| session_id | bigint unsigned | YES | | FK to training sessions |
| tournament_match_id | bigint unsigned | YES | | FK to matches |
| diagnosed_by | varchar(200) | YES | | Doctor/physio name |
| diagnosis_ar | text | YES | | Medical diagnosis |
| treatment_ar | text | YES | | Treatment plan |
| estimated_recovery_days | int unsigned | YES | | |
| actual_recovery_date | date | YES | MUL | |
| status | varchar(20) | NO | MUL | active/recovering/recovered/chronic |
| restricted_activities_json | json | YES | | Activities player cannot do |
| follow_up_date | date | YES | | |
| medical_report_document_id | bigint unsigned | YES | | FK to documents |
| clearance_document_id | bigint unsigned | YES | | FK to documents |
| cleared_by | varchar(200) | YES | | Who cleared the player |
| cleared_at | date | YES | | |
| notes | text | YES | | |
| reported_by | bigint unsigned | YES | | FK to employees |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| updated_by | bigint unsigned | YES | | |
| created_by | bigint unsigned | YES | | |

### 3.5 `player_fitness_tests` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | bigint unsigned | NO | MUL | FK to players |
| discipline_id | bigint unsigned | YES | | FK to sport_disciplines |
| test_date | date | NO | MUL | |
| test_type | varchar(50) | NO | MUL | |
| tester_name | varchar(200) | YES | | |
| coach_id | bigint unsigned | YES | | FK to coaches |
| results_json | json | NO | | Individual test results |
| overall_fitness_score | decimal(5,2) | YES | | |
| fitness_level | varchar(20) | YES | | excellent/good/average/below_average/poor |
| bmi | decimal(4,1) | YES | | Calculated from weight/height |
| weight_kg | decimal(5,1) | YES | | |
| height_cm | decimal(5,1) | YES | | |
| resting_heart_rate | int unsigned | YES | | |
| blood_pressure | varchar(20) | YES | | |
| recommendations_ar | text | YES | | |
| cleared_for_activity | tinyint(1) | NO | | Default: 1 |
| next_test_date | date | YES | | |
| created_at | timestamp | NO | | |

### 3.6 `academy_enrollments` Table

| 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 (for billing) |
| status | varchar(20) | NO | MUL | active/suspended/dropped |
| promoted_from_id | bigint unsigned | YES | MUL | Previous enrollment if promoted |
| dropped_at | timestamp | YES | | |
| dropped_reason | varchar(500) | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |

### 3.7 `player_attendance` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | bigint unsigned | NO | MUL | FK to players |
| enrollment_id | bigint unsigned | YES | MUL | FK to academy_enrollments |
| schedule_id | bigint unsigned | YES | MUL | FK to academy_schedules |
| facility_id | bigint unsigned | YES | MUL | FK to facilities |
| attendance_date | date | NO | MUL | |
| check_in_time | time | YES | | |
| check_out_time | time | YES | | |
| status | varchar(20) | NO | | present/absent/late/excused |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |

### 3.8 `player_progressions` Table

| Column | Type | Nullable | Key | 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 |
| from_level_id | bigint unsigned | YES | | FK to academy_levels |
| to_level_id | bigint unsigned | NO | | FK to academy_levels |
| progression_type | varchar(30) | NO | | promotion/demotion/lateral |
| assessment_score | decimal(5,2) | YES | | |
| assessed_by | bigint unsigned | YES | | FK to employees/coaches |
| assessment_notes | text | YES | | |
| effective_date | date | NO | | |
| created_at | timestamp | NO | | |

### 3.9 `evaluation_criteria` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| discipline_id | bigint unsigned | NO | MUL | FK to sport_disciplines |
| criterion_name_ar | varchar(200) | NO | | Arabic name |
| criterion_name_en | varchar(200) | YES | | English name |
| description_ar | text | YES | | |
| max_score | int | NO | | Default: 10 |
| weight | decimal(3,2) | NO | | Default: 1.00 |
| sort_order | int | NO | | |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.10 `player_disciplines` Table

| Column | Type | Nullable | Key | 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 |
| age_group | varchar(50) | YES | | |
| skill_level | varchar(50) | YES | | |
| season | varchar(20) | YES | MUL | |
| status | varchar(20) | NO | | Default: 'active' |
| registered_at | timestamp | NO | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

---

## 4. Routes (30 total)

### Player CRUD & Actions
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /players | PlayerController@index | player.view |
| GET | /players/create | PlayerController@create | player.register |
| POST | /players | PlayerController@store | player.register |
| GET | /players/{id} | PlayerController@show | player.view |
| GET | /players/{id}/edit | PlayerController@edit | player.edit |
| POST | /players/{id} | PlayerController@update | player.edit |
| POST | /players/{id}/card/activate | PlayerController@activateCard | player.manage_card |
| POST | /players/{id}/card/suspend | PlayerController@suspendCard | player.manage_card |
| POST | /players/{id}/card/revoke | PlayerController@revokeCard | player.manage_card |
| POST | /players/{id}/medical | PlayerController@addMedical | player.manage_medical |
| POST | /players/{id}/enroll | PlayerController@enroll | academy.enroll |
| POST | /players/{id}/enroll/drop | PlayerController@dropEnrollment | academy.enroll |

### Internal API
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| POST | /api/players/parse-nid | PlayerController@parseNid | player.register |
| GET | /api/players/academies | PlayerController@apiAcademies | player.view |

### Medical Approvals
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /medical-approvals | MedicalApprovalController@index | player.approve_medical |
| POST | /medical-approvals/{id}/approve | MedicalApprovalController@approve | player.approve_medical |
| POST | /medical-approvals/{id}/reject | MedicalApprovalController@reject | player.approve_medical |

### Attendance
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /attendance | AttendanceController@index | player.view |
| POST | /attendance/record | AttendanceController@record | player.edit |

### Evaluations
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /evaluations | EvaluationController@index | player.evaluate |
| GET | /evaluations/create | EvaluationController@create | player.evaluate |
| POST | /evaluations | EvaluationController@store | player.evaluate |
| GET | /evaluations/{id} | EvaluationController@show | player.evaluate |
| POST | /evaluations/{id}/approve | EvaluationController@approve | player.evaluate |
| GET | /api/evaluations/criteria/{disciplineId} | EvaluationController@apiCriteria | player.evaluate |

### Injuries
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /injuries | InjuryController@index | player.manage_injuries |
| GET | /injuries/create | InjuryController@create | player.manage_injuries |
| POST | /injuries | InjuryController@store | player.manage_injuries |
| GET | /injuries/{id} | InjuryController@show | player.manage_injuries |
| POST | /injuries/{id}/status | InjuryController@updateStatus | player.manage_injuries |
| POST | /injuries/{id}/clear | InjuryController@clearPlayer | player.manage_injuries |

### Fitness Tests
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /fitness-tests | FitnessController@index | player.manage_fitness |
| GET | /fitness-tests/create | FitnessController@create | player.manage_fitness |
| POST | /fitness-tests | FitnessController@store | player.manage_fitness |
| GET | /fitness-tests/{id} | FitnessController@show | player.manage_fitness |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `player.registered` | `{player_id, player_name, registration_serial, phone}` | EventListeners.php |
| `player.card_activated` | `{player_id, reason?}` | PlayerCardService, EventListeners |
| `player.card_suspended` | `{player_id, reason?}` | PlayerCardService, InjuryService |
| `player.card_revoked` | `{player_id}` | PlayerCardService |
| `player.medical_uploaded` | `{player_id, record_id, certificate_type}` | MedicalRecordService |
| `player.medical_approved` | `{player_id, record_id}` | MedicalRecordService |
| `player.medical_rejected` | `{player_id, record_id, reason}` | MedicalRecordService |
| `player.evaluation_submitted` | `{evaluation_id, player_id, discipline_id, overall_score, skill_level}` | EvaluationService |
| `player.evaluation_approved` | `{evaluation_id, player_id, discipline_id, approved_by}` | EvaluationService |
| `player.injury_reported` | `{injury_id, player_id, severity, injury_type, body_part}` | InjuryService |
| `player.injury_recovered` | `{injury_id, player_id, injury_type}` | InjuryService |
| `player.fitness_test_recorded` | `{test_id, player_id, fitness_level, overall_score, cleared}` | FitnessService |
| `player.progression.recorded` | `{player_id, type, to_level_id, score}` | PlayerProgressionService |
| `reservation.confirmed` | `{reservation_id, payment_id}` | EventListeners (payment.completed) |
| `rental.deposit_collected` | `{contract_id, payment_id}` | EventListeners (payment.completed) |
| `activity_sub.paid` | `{subscription_id, player_id, payment_id}` | EventListeners (payment.completed) |
| `activity_sub.generated` | `{subscription_id, player_id, enrollment_id, month, amount, is_half_month}` | EventListeners (academy.enrollment_created) |
| `medical.expiring_soon` | `{player_id, record_id, expiry_date, days_left, record_type}` | MedicalScanService |
| `medical.expired` | `{player_id, record_id, expiry_date, record_type}` | MedicalScanService |

---

## 6. Events Consumed (EventListeners.php)

| Event | Action |
|-------|--------|
| `payment.completed` (type: sports_reg_member/sports_reg_nonmember) | Mark player `registration_fee_paid=1` |
| `payment.completed` (type: facility_reservation) | Auto-confirm reservation |
| `payment.completed` (type: rental_deposit) | Update rental contract deposit status |
| `payment.completed` (type: activity_subscription) | Mark subscription paid, activate card |
| `academy.enrollment_created` | Auto-generate first month activity subscription |
| `academy.enrollment_dropped` | Cancel pending unpaid subscriptions for the enrollment |
| `activity_sub.paid` | Backup card activation (if not already active) |
| `player.registered` | Dispatch SMS notification via `sms.send` event |

---

## 7. Cross-Module Dependencies

### 7.1 PlayerAffairs IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | `NationalIdParser` (NID parsing for player registration) |
| Members | `Member` model (link member to player during registration) |
| Rules | `RuleEngine` (SPORTS_REG_SERIAL_START_MEMBER/NONMEMBER, MINOR_AGE_THRESHOLD) |

### 7.2 Other modules that IMPORT FROM PlayerAffairs:

| Module | What it imports |
|--------|----------------|
| ActivitySubscriptions | `AcademyEnrollment::create()` (enrollment wizard) |
| PlayerApi | Uses `PlayerNotificationService` from PlayerApi to notify players (reverse: PlayerApi listens to PlayerAffairs events) |
| TrainerPortal | `PlayerNotificationService` (sends notifications to players from coach actions) |
| Notifications | `PlayerNotificationService` (general notification triggering) |

### 7.3 Indirect dependencies (via EventBus):

| Module | Relationship |
|--------|-------------|
| Payments | Dispatches `payment.completed` which PlayerAffairs listens to |
| SportsActivity | Dispatches `academy.enrollment_created/dropped` |
| PlayerApi | Listens to various player.* events for push notifications |

---

## 8. Permissions

| Key | Description (Arabic) |
|-----|---------------------|
| player.view | View players list and profiles |
| player.register | Register new players |
| player.edit | Edit player data |
| player.manage_card | Activate/suspend/revoke player cards |
| player.manage_medical | Upload and manage medical records |
| player.approve_medical | Approve/reject medical certificates |
| player.evaluate | Create and approve player evaluations |
| player.manage_injuries | Report and manage injuries |
| player.manage_fitness | Record and manage fitness tests |

---

## 9. Background Processes (Cron)

`MedicalExpiryCron::run()` delegates to `MedicalScanService::checkExpiryAndNotify()`:
- Scans for medical records expiring within 14 days
- Dispatches `medical.expiring_soon` event for each
- Scans for expired records, dispatches `medical.expired`

---

## 10. Core Business Flows

### 10.1 Player Registration

```
1. User fills form: name, type, DOB, gender, NID (parsed), guardian info (if minor)
2. If player_type='member' and member_id provided:
   - PlayerRegistrationService::registerMemberPlayer()
   - Links to existing member, inherits profile data
3. If non-member:
   - PlayerRegistrationService::registerNonMemberPlayer()
   - Sets access_window_minutes (default 30)
4. Serial number assigned: MAX(registration_serial) + 1 (or rule start value)
5. Initial state: card_status='inactive', medical_status='pending'
6. Photo uploaded to storage/uploads/players/
```

### 10.2 Medical Approval Workflow

```
1. Staff uploads medical certificate via POST /players/{id}/medical
2. Record created with result='pending', approval_status='pending'
3. Event dispatched: player.medical_uploaded
4. Medical Board reviews (either /medical-approvals or /medical-board)
5a. Approve: result='fit', player.medical_status='fit', medical_expiry_date set
5b. Reject: result='unfit', reason recorded, player remains 'pending'/'unfit'
```

### 10.3 Evaluation & Group Suggestion

```
1. Coach creates evaluation: picks player, discipline, scores per criterion
2. EvaluationService calculates weighted overall_score
3. Determines skill_level: >=8.0=elite, >=6.0=advanced, >=3.0=intermediate, <3.0=beginner
4. Suggests best-fit training group based on:
   - Gender compatibility
   - Age range match
   - Level name keyword match
   - Available capacity
5. Evaluation status: submitted → approved (by manager)
6. On approval: player.evaluation_completed event dispatched (notifies player via app)
```

### 10.4 Injury Auto-Restriction

```
1. Injury reported with severity = 'severe' or 'critical'
2. InjuryService::autoRestrict() runs:
   - If player card is 'active', sets to 'suspended'
   - Dispatches player.card_suspended with injury reason
3. Player cannot attend activities until:
   - Injury status changed to 'recovered'
   - Cleared by medical staff (cleared_by, cleared_at set)
   - Card manually re-activated
```

---

## 11. Configuration Dependencies

| Rule Engine Keys | Purpose |
|-----------------|---------|
| SPORTS_REG_SERIAL_START_MEMBER | Starting serial for member-type players |
| SPORTS_REG_SERIAL_START_NONMEMBER | Starting serial for non-member players |
| MINOR_AGE_THRESHOLD | Age below which player is considered minor (default: 18) |

| SMS Templates | Purpose |
|--------------|---------|
| SMS_SPORTS_REGISTRATION | Sent on player registration |

---

## 12. High-Risk Areas

### 12.1 EventListeners.php (CRITICAL)
- Handles `payment.completed` with multiple payment types in a single handler
- Touches reservations, rental_contracts, activity_subscriptions, and player cards
- Auto-activates player cards on subscription payment — if logic is wrong, cards may activate incorrectly
- Priority 50 (runs after main payment handler at 100)

### 12.2 Medical Approval Dual Paths
- `MedicalApprovalController` (at `/medical-approvals`) handles PlayerAffairs-scoped approvals
- `MedicalBoardController` (at `/medical-board`) handles both PlayerAffairs AND SportsActivity players
- Both update the same `player_medical_records` table
- If both are used simultaneously, no lock prevents double-approval

### 12.3 Registration Serial Sequencing
- Uses `MAX(CAST(registration_serial AS UNSIGNED))` — race condition under concurrency
- Separate serial ranges for member vs non_member types
- If serials break: compliance issue with sequential numbering

### 12.4 Card Status Consistency
- Multiple code paths modify `card_status`: PlayerCardService, EventListeners (payment), InjuryService (auto-suspend)
- No central state machine enforcement
- A player could theoretically have card suspended by injury AND activated by payment in the same request

### 12.5 Injury Auto-Restrict
- Severe/critical injuries auto-suspend the player's card
- No automatic re-activation on recovery — requires manual intervention
- If autoRestrict fails silently (caught exception), player remains active despite critical injury

---

## 13. Known Patterns & Gotchas

1. **No soft delete on Player model**: `$softDelete = false` — uses manual `is_archived` column check in queries
2. **Medical expiry tracking is dual**: Both `player_medical_records.expiry_date` and `players.medical_expiry_date` — must stay in sync
3. **Enrollment creates subscription**: The `academy.enrollment_created` listener auto-generates the first month's activity subscription — half-month logic applies if enrolled after the 15th
4. **Photo upload is basic**: No resizing, no CDN, stored directly in `storage/uploads/players/`
5. **NID parser imported from Members module**: Shared service — changes to `NationalIdParser` affect both modules
6. **OTP fields on players table**: `otp_code`, `otp_expires_at`, `password_hash`, `last_login_at` are all on the same table — used by PlayerAuth module
7. **Dual player sources**: `player_medical_records` has both `player_id` (membership/PlayerAffairs) and `sa_player_id` (sports activity) — the MedicalBoard handles both

---

## Appendix: Files to Check Before Modifying PlayerAffairs

1. `app/Modules/PlayerAffairs/EventListeners.php` — Payment/enrollment event reactions
2. `app/Modules/PlayerApi/bootstrap.php` — Notification event listeners for player
3. `app/Modules/MedicalBoard/Controllers/MedicalBoardController.php` — Dual medical approval path
4. `app/Modules/ActivitySubscriptions/Controllers/EnrollWizardController.php` — Creates enrollments using AcademyEnrollment model
5. `app/Modules/TrainerPortal/bootstrap.php` — Coach-driven notifications to players
6. `app/Middleware/PlayerApiAuthMiddleware.php` — Reads players table for auth
7. `app/Modules/PlayerAuth/Services/PlayerAuthService.php` — Writes OTP/password to players table
