# PlayerApi Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The PlayerApi module provides the **mobile app REST API** for players (athletes/trainees). It is the player-facing gateway to the club's sports system, enabling:

- Sport browsing (disciplines, training groups, availability)
- Self-enrollment in training groups (with waiting list)
- Group transfer requests
- Facility booking (pitches, courts, halls, tables, devices)
- Free-time entry recording (pool, gym, recreation)
- Carnet/invitation management (member-only guest entries)
- In-app notifications (push/pull)
- Medical record submission and document upload
- Player profile viewing and limited self-editing
- Evaluation viewing (parent/player visible)

It does **NOT** manage:
- Authentication (PlayerAuth handles login/OTP/tokens)
- Back-office player management (PlayerAffairs handles admin CRUD)
- Medical approval (MedicalBoard handles board decisions)
- Subscription billing (ActivitySubscriptions module)

---

## 2. Directory & File Structure

```
app/Modules/PlayerApi/
├── bootstrap.php                              # Event listeners for player notifications
├── ApiRoutes.php                              # Mobile API routes (30 endpoints)
├── Controllers/
│   └── Api/
│       ├── BookingController.php              # Facility booking CRUD
│       ├── CarnetController.php               # Carnet guest invitations
│       ├── EnrollmentController.php           # Group enrollment + evaluations
│       ├── FreeTimeController.php             # Free-time facility entry
│       ├── MedicalController.php              # Medical record submission + upload
│       ├── NotificationController.php         # In-app notifications
│       ├── ProfileController.php              # Player profile view/update
│       ├── SportBrowseController.php          # Browse sports, groups, group details
│       └── TransferController.php             # Group transfer requests
└── Services/
    ├── PlayerBookingService.php               # Booking logic: availability, create, cancel
    ├── PlayerFreeTimeService.php              # Free-time: facilities, slots, entry recording
    └── PlayerNotificationService.php          # Notification CRUD (used cross-module)
```

---

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

### 3.1 `player_notifications` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | int unsigned | NO | MUL | FK to players |
| category | varchar(30) | NO | MUL | sport/booking/attendance/medical/rating |
| title_ar | varchar(300) | NO | | Arabic notification title |
| body_ar | text | YES | | Arabic notification body |
| data_json | json | YES | | Additional context data |
| is_read | tinyint(1) | NO | | Default: 0 |
| read_at | datetime | YES | | When player read it |
| created_at | datetime | NO | | |

### 3.2 Tables Read/Written by PlayerApi (owned by other modules)

| Table | Owner | Operations | Purpose |
|-------|-------|-----------|---------|
| players | PlayerAffairs | READ | Profile, auth context |
| members | Members | READ | Membership info for carnet/pricing |
| training_groups | SportsActivity | READ, UPDATE (count) | Browse, enroll |
| group_memberships | SportsActivity | INSERT, READ | Enrollment records |
| waiting_list | SportsActivity | INSERT | Full group fallback |
| group_transfer_requests | SportsActivity | INSERT, READ | Transfer requests |
| facilities | Facilities | READ | Browse bookable/free-time facilities |
| facility_time_slots | Facilities | READ | Slot availability |
| reservations | Facilities | INSERT, UPDATE, READ | Create/cancel bookings |
| free_time_entries | Facilities | INSERT | Record free-time entry |
| carnets | Carnets | READ, UPDATE (used_invitations) | Guest invitation quota |
| carnet_guest_entries | Carnets | INSERT, READ | Guest entry records |
| player_medical_records | PlayerAffairs | INSERT, UPDATE, READ | Medical submission |
| player_evaluations | PlayerAffairs | READ | View approved evaluations |
| sport_disciplines | Disciplines | READ | Sport browsing |
| coaches | Coaches | READ | Coach info in groups |
| academy_levels | SportsActivity | READ | Level info |
| academies | SportsActivity | READ | Academy info |
| activity_pricing | Pricing | READ | Group pricing display |
| documents | Documents | INSERT | Medical document upload |

---

## 4. Routes (30 total — all under `/api/v1/player/`)

All routes use `['cors', 'player_auth']` middleware (except auth routes). No permission checks — access is identity-based (the authenticated player sees only their own data).

### Sport Browsing & Enrollment
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/sports | SportBrowseController@listSports |
| GET | /api/v1/player/sports/{id}/groups | SportBrowseController@listGroups |
| GET | /api/v1/player/groups/{id} | SportBrowseController@groupDetails |
| POST | /api/v1/player/groups/{id}/enroll | EnrollmentController@enroll |
| GET | /api/v1/player/enrollments | EnrollmentController@myEnrollments |
| GET | /api/v1/player/evaluations | EnrollmentController@myEvaluations |

### Group Transfer
| Method | Path | Handler |
|--------|------|---------|
| POST | /api/v1/player/transfers | TransferController@requestTransfer |
| GET | /api/v1/player/transfers | TransferController@myTransfers |

### Facility Booking
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/facilities | BookingController@facilities |
| GET | /api/v1/player/facilities/{id}/availability | BookingController@availability |
| POST | /api/v1/player/bookings | BookingController@book |
| GET | /api/v1/player/bookings | BookingController@myBookings |
| POST | /api/v1/player/bookings/{id}/cancel | BookingController@cancel |

### Free-Time Entry
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/free-time/facilities | FreeTimeController@availableFacilities |
| GET | /api/v1/player/free-time/{facilityId}/slots | FreeTimeController@slots |
| POST | /api/v1/player/free-time/enter | FreeTimeController@enter |

### Carnet (Invitations)
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/carnet | CarnetController@summary |
| POST | /api/v1/player/carnet/invite | CarnetController@invite |
| GET | /api/v1/player/carnet/history | CarnetController@history |

### Notifications
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/notifications | NotificationController@list |
| POST | /api/v1/player/notifications/{id}/read | NotificationController@markRead |
| POST | /api/v1/player/notifications/read-all | NotificationController@markAllRead |
| GET | /api/v1/player/notifications/unread-count | NotificationController@unreadCount |

### Medical
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/medical | MedicalController@records |
| POST | /api/v1/player/medical | MedicalController@submit |
| POST | /api/v1/player/medical/{id}/upload | MedicalController@upload |

### Profile
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/v1/player/profile | ProfileController@show |
| PUT | /api/v1/player/profile | ProfileController@update |

---

## 5. Events Consumed (bootstrap.php)

| Event | Notification Category | Action |
|-------|----------------------|--------|
| `player.enrolled` | sport | "You have been enrolled in group X" |
| `player.booking_confirmed` | booking | "Your booking at X on date Y confirmed" |
| `player.transfer_result` | sport | "Your transfer request approved/rejected" |
| `player.booking_cancelled` | booking | "Your booking at X on date Y cancelled" |
| `player.free_time_entry` | booking | "Free-time entry recorded at X" |
| `player.evaluation_completed` | rating | "You have been evaluated by your coach" |
| `player.medical_expiring` | medical | "Your medical expires in 7 days" |
| `facility.attendance_absent` | attendance | "Absence recorded on date X" |

---

## 6. Events Dispatched

| Event | Dispatched From | Payload |
|-------|-----------------|---------|
| `player.enrolled` | EnrollmentController@enroll | `{player_id, group_id, group_name}` |
| `player.booking_confirmed` | PlayerBookingService@createBooking | `{player_id, booking_id, facility_name, date}` |
| `player.booking_cancelled` | PlayerBookingService@cancelBooking | `{player_id, booking_id}` |
| `player.free_time_entry` | PlayerFreeTimeService@recordEntry | `{player_id, entry_id, facility_name}` |

---

## 7. Cross-Module Dependencies

### 7.1 PlayerApi IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| (None — no explicit `use` statements from other modules) | |

PlayerApi operates via direct SQL on shared tables and uses only core framework classes. The `PlayerNotificationService` is self-contained.

### 7.2 Other modules that IMPORT FROM PlayerApi:

| Module | What it imports |
|--------|----------------|
| TrainerPortal | `PlayerNotificationService::notify()` — sends notifications to players |
| Notifications | `PlayerNotificationService::notify()` — general notification routing |
| PlayerApi (self) | bootstrap.php event listeners use `PlayerNotificationService` |

### 7.3 Middleware dependency:

| Component | Role |
|-----------|------|
| `PlayerApiAuthMiddleware` (app/Middleware/) | Validates bearer token from `player_tokens` table, sets `App::currentPlayer()` |
| CORS middleware | Handles cross-origin requests from mobile app |

---

## 8. Permissions

PlayerApi has **no permission system** — all endpoints use the `player_auth` middleware which authenticates via bearer token. Authorization is identity-based: players can only view/modify their own data. The only restriction logic:

- Carnet endpoints: require `player.member_id` to be non-null (members only)
- Free-time `members_only` slots: check `player.member_id`
- Pricing: member vs non-member rates applied based on `player.member_id`

---

## 9. Core Business Flows

### 9.1 Group Enrollment (Self-Service)

```
1. Player browses: GET /sports → GET /sports/{id}/groups → GET /groups/{id}
2. Player enrolls: POST /groups/{id}/enroll
3. If group NOT full:
   - Insert into group_memberships (status='active')
   - Increment training_groups.current_count
   - Update is_full flag if at capacity
   - Dispatch player.enrolled event → notification
4. If group IS full:
   - Insert into waiting_list (status='pending')
   - Return 201 with status='waiting_list'
```

### 9.2 Facility Booking

```
1. GET /facilities → list bookable facilities (pitch, court, hall, table, device)
2. GET /facilities/{id}/availability?date=YYYY-MM-DD
   - Load facility_time_slots for that day_of_week
   - Check existing reservations for conflicts
   - Return slots with is_available flag
3. POST /bookings {facility_id, date, start_time, end_time}
   - Check facility exists and is active
   - Check for time conflicts
   - Calculate price: hourly_rate * hours (member vs non-member rate)
   - Generate reservation_number: RES-YYYYMMDD-NNNN
   - Insert reservation (status='confirmed')
   - Dispatch player.booking_confirmed
4. POST /bookings/{id}/cancel
   - Verify booker_type='player' AND booker_id matches authenticated player
   - Update status='cancelled'
   - Dispatch player.booking_cancelled
```

### 9.3 Free-Time Entry

```
1. GET /free-time/facilities → facilities with open/members_only slots
2. GET /free-time/{facilityId}/slots?date=YYYY-MM-DD → available free-time slots
3. POST /free-time/enter {facility_id, date?, entry_time?, activity_type?}
   - Find matching active time slot for current time/day
   - Check access_type: if 'members_only', verify player is member
   - Calculate amount: member_price or guest_price from slot
   - Insert free_time_entries record
   - Dispatch player.free_time_entry
```

### 9.4 Carnet Guest Invitation (Members Only)

```
1. GET /carnet → active carnet summary (total/used/remaining invitations)
2. POST /carnet/invite {guest_name, guest_phone?, guest_national_id?, activity_type}
   - Verify player has member_id
   - Verify active carnet with remaining invitations
   - Insert carnet_guest_entries
   - Increment carnets.used_invitations
3. GET /carnet/history → past guest entries
```

---

## 10. Notification Categories

| Category | Triggers |
|----------|----------|
| sport | Enrollment confirmed, transfer result |
| booking | Booking confirmed/cancelled, free-time entry |
| attendance | Absence recorded |
| medical | Medical expiring soon |
| rating | Evaluation completed |

---

## 11. High-Risk Areas

### 11.1 Group Enrollment Concurrency
- `current_count` increment is not atomic with the capacity check
- `is_full` flag update uses `current_count + 1 >= max_capacity` in the same query
- Under concurrent enrollments: could exceed `max_capacity`
- No transaction wrapping the check + insert + update

### 11.2 Booking Conflict Detection
- Conflict check (`start_time < ? AND end_time > ?`) could miss edge cases with concurrent bookings
- No row-level locking on the time slot
- Reservation number uses `random_int(1, 9999)` — potential collision on high-volume days

### 11.3 PlayerNotificationService as Cross-Module Gateway
- Used by 4+ modules (PlayerApi bootstrap, TrainerPortal, Notifications, self)
- If service interface changes, all consumers break
- No queue/async — notifications are synchronous inserts in event handlers
- Large notification volume could slow down event dispatch

### 11.4 Free-Time Entry Pricing
- Falls back from slot price to facility hourly rate if slot price is null
- No payment verification — entry is recorded with `amount_paid` but no actual payment flow
- Potential for price mismatch if slot data is stale

### 11.5 Carnet Invitation Count
- `used_invitations` increment is not atomic with the count check
- Concurrent invite requests could exceed `total_invitations`

---

## 12. Known Patterns & Gotchas

1. **No permission middleware**: All authorization is identity-based (player sees only own data) — a compromised token gives full access to that player's actions
2. **LAST_INSERT_ID() pattern**: Multiple controllers use `SELECT LAST_INSERT_ID()` after insert — not safe if connection pooling allows interleaved queries (currently safe with single-request PDO)
3. **Direct table writes**: PlayerApi writes to tables owned by other modules (reservations, group_memberships, carnets) without going through those modules' services — tight coupling via shared DB
4. **Profile field mismatch**: ProfileController reads `name_ar` from players but the column is `full_name_ar` — potential query issue
5. **Medical submission dual path**: Players can submit medical records via both `/api/v1/player/medical` (PlayerApi) and staff via `/players/{id}/medical` (PlayerAffairs) — same table, no deduplication
6. **Notification data_json**: Stored as MySQL JSON type but event payloads can contain any structure — no schema validation
7. **Booking status**: Created as 'confirmed' immediately — no pending/payment step in the mobile flow

---

## Appendix: Files to Check Before Modifying PlayerApi

1. `app/Middleware/PlayerApiAuthMiddleware.php` — Auth middleware that sets currentPlayer
2. `app/Modules/PlayerAuth/Services/PlayerAuthService.php` — Token creation/validation
3. `app/Modules/PlayerAffairs/EventListeners.php` — Payment events that affect player card/subscriptions
4. `app/Modules/TrainerPortal/bootstrap.php` — Uses PlayerNotificationService
5. `app/Modules/Notifications/Services/NotificationTriggerService.php` — Uses PlayerNotificationService
6. `app/Core/Router.php` — Maps 'player_auth' middleware name to class
