# Achievements Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Achievements module is a **gamification engine** for the PlayerAffairs/Sports domain. It manages:
- Achievement definition management (admin CRUD for achievement types with criteria)
- Criteria-based automatic awarding (attendance streaks, fitness scores, membership milestones, etc.)
- Player achievement tracking (which players earned which achievements, with points)
- Leaderboard (ranked players by total achievement points, filterable by discipline)
- Per-player achievement portfolio view

It does **NOT** directly manage:
- Player profiles (PlayerAffairs module owns players)
- Attendance records (session_attendance table is owned by SportsActivity/Attendance)
- Fitness tests or evaluations (owned by PlayerAffairs)
- Notifications (ParentPortal and Notifications modules listen to achievement events)

---

## 2. Directory & File Structure

```
app/Modules/Achievements/
├── bootstrap.php                              # Permission registration (no menu, no events)
├── Routes.php                                 # 6 routes (2 player-facing, 4 admin)
├── Controllers/
│   └── AchievementController.php             # All actions (index, leaderboard, admin CRUD)
├── Models/
│   ├── AchievementDefinition.php             # Achievement types/templates
│   └── PlayerAchievement.php                 # Earned achievements per player
├── Services/
│   └── AchievementEngine.php                 # Core engine: evaluate criteria & award
└── Views/
    ├── player_achievements.php               # Per-player achievement list with categories
    ├── leaderboard.php                       # Top players ranked by points
    ├── admin_index.php                       # Admin: paginated list of definitions
    └── admin_create.php                      # Admin: create new achievement definition
```

---

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

### 3.1 `achievement_definitions` Table (0 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(50) | NO | UNI | Unique achievement identifier |
| name_ar | varchar(200) | NO | | Arabic display name |
| name_en | varchar(200) | YES | | English display name |
| description_ar | text | YES | | Arabic description |
| category | varchar(30) | NO | MUL | attendance/performance/competition/milestone/social/fitness/special |
| icon | varchar(50) | YES | | Lucide icon name, default null |
| badge_color | varchar(20) | YES | | Hex color, default '#FFD700' (gold) |
| discipline_id | bigint unsigned | YES | MUL | FK to sport_disciplines (null = global) |
| criteria_json | json | NO | | JSON criteria for auto-evaluation |
| points | int unsigned | NO | | Points awarded, default 10 |
| is_active | tinyint(1) | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_by | bigint unsigned | YES | | |
| created_by | bigint unsigned | YES | | |

**criteria_json structure:**
```json
{
  "type": "attendance_total|attendance_streak|membership_months|fitness_score|evaluation_score|tournament_wins|custom",
  "trigger": "event_type_string (optional, filters which events trigger evaluation)",
  "count": 10,
  "months": 12,
  "min_score": 85.0,
  "field": "context_field_name (for custom type)",
  "operator": ">=|>|<=|<|==|!=",
  "value": 100
}
```

### 3.2 `player_achievements` Table (0 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| player_id | bigint unsigned | NO | MUL | FK to players |
| achievement_id | bigint unsigned | NO | MUL | FK to achievement_definitions |
| earned_at | datetime | NO | MUL | When the achievement was earned |
| context_json | json | YES | | Context data at time of award |
| points_earned | int unsigned | NO | | Points earned (copied from definition), default 0 |
| notified | tinyint(1) | NO | | Whether player was notified, default 0 |
| created_at | timestamp | NO | | |

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

| Table | Owner Module | Relationship |
|-------|-------------|--------------|
| players | PlayerAffairs | player_id FK, 1:many |
| sport_disciplines | Disciplines | discipline_id FK (optional, for discipline-scoped achievements) |
| session_attendance | SportsActivity | Queried for attendance_total / attendance_streak criteria |
| player_fitness_tests | PlayerAffairs | Queried for fitness_score criteria |
| player_evaluations | PlayerAffairs | Queried for evaluation_score criteria |

---

## 4. Routes

| Method | Path | Action | Middleware | Permission |
|--------|------|--------|------------|------------|
| GET | /achievements | index | auth | achievement.view |
| GET | /achievements/leaderboard | leaderboard | auth | achievement.view |
| GET | /achievements/admin | adminIndex | auth | achievement.manage |
| GET | /achievements/admin/create | adminCreate | auth | achievement.manage |
| POST | /achievements/admin | adminStore | auth, csrf | achievement.manage |
| POST | /achievements/admin/{id}/toggle | adminToggle | auth, csrf | achievement.manage |

---

## 5. Core Business Flows

### 5.1 Achievement Evaluation (AchievementEngine::checkAndAward)
1. Called with `(playerId, eventType, context)`
2. Loads all active achievement definitions
3. For each definition:
   - Skip if player already earned it (`hasEarned()` check)
   - Skip if definition's `trigger` field doesn't match `eventType`
   - Evaluate criteria based on `type`:
     - `attendance_total`: COUNT from session_attendance where status='present'
     - `attendance_streak`: consecutive 'present' records (max 100 lookback)
     - `membership_months`: TIMESTAMPDIFF from player's created_at
     - `fitness_score`: latest `overall_fitness_score` from player_fitness_tests
     - `evaluation_score`: latest approved `overall_score` from player_evaluations
     - `tournament_wins`: from context array
     - `custom`: arbitrary field/operator/value comparison against context
4. If criteria met: create PlayerAchievement row + dispatch event

### 5.2 Achievement Award
1. Double-checks not already awarded (idempotent)
2. Creates `player_achievements` row with points and context
3. Dispatches `player.achievement_earned` event
4. Logs via Logger

### 5.3 Leaderboard
- Aggregates `SUM(points_earned)` and `COUNT(id)` per player
- Optional discipline filter joins achievement_definitions
- Returns top N players with name, photo, serial, points, achievement count

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `player.achievement_earned` | `{player_id, achievement_id, achievement_code, achievement_name, points}` | AchievementEngine::awardAchievement |

---

## 7. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `player.achievement_earned` | ParentPortal | Creates notification for player's parent |
| `player.achievement_earned` | Notifications | Triggers NotificationTriggerService (SMS/push/in-app) |

---

## 8. Cross-Module Dependencies

### 8.1 Achievements IMPORTS FROM:

| Module/Component | What it uses |
|-----------------|-------------|
| App\Core\EventBus | Dispatches `player.achievement_earned` |
| App\Core\Logger | Logs achievement awards |

### 8.2 Database Dependencies (reads from other modules' data)

| Table | Owner Module | How it's used |
|-------|-------------|---------------|
| players | PlayerAffairs | Player lookup for views and leaderboard |
| sport_disciplines | Disciplines | Discipline names for filtering |
| session_attendance | SportsActivity | Attendance criteria evaluation |
| player_fitness_tests | PlayerAffairs | Fitness score criteria evaluation |
| player_evaluations | PlayerAffairs | Evaluation score criteria evaluation |

### 8.3 Other modules that IMPORT FROM Achievements:

| Module | What it imports |
|--------|----------------|
| ParentPortal | Queries player_achievements + achievement_definitions directly via SQL |
| Notifications | Listens to `player.achievement_earned` event |

---

## 9. Permissions

| Key | Description |
|-----|-------------|
| achievement.view | View player achievements and leaderboard |
| achievement.manage | Create/edit/toggle achievement definitions |

---

## 10. Background Processes (Cron)

None currently. However, `AchievementEngine::checkAttendanceAchievements()` and `checkMilestoneAchievements()` are public static methods designed to be called from a cron job — they are not currently wired up.

---

## 11. Configuration Dependencies

None. No system_config keys or external configuration used.

---

## 12. High-Risk Areas

### 12.1 Criteria Evaluation Performance
- `checkAndAward()` loads ALL active definitions every time it's called
- For each definition, may execute additional DB queries (attendance count, fitness score, etc.)
- If called frequently (e.g., on every attendance record), could become a bottleneck
- No caching of definitions or previously evaluated results

### 12.2 Idempotency
- `hasEarned()` is checked twice (in `checkAndAward` loop and in `awardAchievement`)
- No database-level UNIQUE constraint on `(player_id, achievement_id)` — relies on application-level check
- Race condition possible if two concurrent requests evaluate the same player

### 12.3 Criteria JSON Schema
- No validation of `criteria_json` structure on creation beyond what the form provides
- Invalid JSON or unrecognized `type` values silently return `false` (no award, no error)
- Custom criteria rely on arbitrary field names from context — no type safety

### 12.4 Module State: Empty
- Both tables have 0 rows in production — module is deployed but not yet populated
- No seed data for achievement definitions
- No automated trigger integration yet (e.g., no module listens to attendance events to call checkAndAward)

---

## 13. Known Patterns & Gotchas

1. **No menu registration**: bootstrap.php only registers permissions, no sidebar menu entry
2. **Achievement categories are hardcoded**: getCategories() returns a fixed array — not configurable
3. **Badge colors are predefined**: 8 color options (gold, silver, bronze, blue, green, purple, red, teal)
4. **Points are copied at award time**: `points_earned` on player_achievements is a snapshot — changing the definition's points won't update already-awarded achievements
5. **Discipline-scoped vs global**: If `discipline_id` is NULL, achievement is global; otherwise only evaluates for that sport
6. **No edit/delete for definitions**: Admin can only create and toggle active/inactive — no edit form exists
7. **Trigger field is optional**: If empty, the achievement is evaluated against ALL events (broad match)
8. **Streak calculation limits to 100 records**: Only looks back 100 attendance records for streak
