# Alerts Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Alerts module provides a **configurable rule-based notification system** for the sports/academy side of the ERP. It:

- Defines alert rules with configurable triggers (thresholds, expiry dates, schedules)
- Processes rules on a scheduled basis (hourly via cron)
- Logs triggered alerts with full context
- Provides a UI for viewing/managing alert rules and reviewing alert history
- Supports acknowledgement workflow for triggered alerts

Current alert categories cover:
- **Medical**: Player medical certificate expiry
- **Contracts**: Academy contract expiry
- **Attendance**: Player consecutive absence threshold
- **Capacity**: Training group near-full capacity
- **Subscriptions**: Overdue activity subscriptions
- **Financial**: Revenue below minimum, settlement due (rules exist but no code handler)
- **Safety**: Pool safety threshold (rule exists but no code handler)

It does **NOT**:
- Send external notifications (email, SMS, push) — status is set to 'sent' but no actual delivery mechanism
- Handle membership-related alerts (those are inline in the Dashboard module)
- Integrate with the EventBus system

---

## 2. Directory & File Structure

```
app/Modules/Alerts/
├── bootstrap.php                          # Permission + menu registration
├── Routes.php                             # 4 routes (index, log, toggle, acknowledge)
├── Controllers/
│   └── AlertController.php               # CRUD for rules, log viewing, acknowledgement
├── Models/
│   ├── AlertRule.php                     # Rule definitions model (alert_rules table)
│   └── AlertLog.php                      # Triggered alert log model (alert_log table)
├── Services/
│   └── AlertProcessorService.php         # Rule evaluation engine (also usable from web)
└── Views/
    ├── index.php                          # Rule management view
    └── log.php                            # Alert history/log view
```

Additionally, the cron job lives outside the module:
```
cron/jobs/AlertProcessorJob.php           # Hourly cron job — evaluates all active rules
```

---

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

### 3.1 `alert_rules` Table (9 rows)

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| code | varchar(50) | NO | UNI | | Unique rule identifier |
| name_ar | varchar(300) | NO | | | Arabic display name |
| category | varchar(50) | NO | MUL | | medical/contracts/financial/attendance/capacity/subscriptions/safety |
| trigger_type | varchar(30) | NO | | | threshold/expiry/schedule |
| trigger_config_json | json | NO | | | Category-specific configuration |
| notification_channels | varchar(200) | NO | | system | Comma-separated channels |
| recipient_roles_json | json | YES | | NULL | Which roles receive alerts |
| is_active | tinyint(1) | NO | MUL | 1 | Enable/disable toggle |
| last_triggered_at | timestamp | YES | | NULL | Last time this rule fired |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |

**Current rules:**

| Code | Category | Trigger Type | Active |
|------|----------|--------------|--------|
| ATTENDANCE_DROPPING | attendance | threshold | Yes |
| GROUP_NEAR_CAPACITY | capacity | threshold | Yes |
| WAITING_LIST_SPOT | capacity | threshold | Yes |
| CONTRACT_EXPIRY | contracts | expiry | Yes |
| REVENUE_BELOW_MINIMUM | financial | threshold | Yes |
| SETTLEMENT_DUE | financial | schedule | Yes |
| MEDICAL_CERT_EXPIRY | medical | expiry | Yes |
| POOL_SAFETY_THRESHOLD | safety | threshold | Yes |
| SUBSCRIPTION_OVERDUE | subscriptions | threshold | Yes |

### 3.2 `alert_log` Table (0 rows — no alerts triggered yet)

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| alert_rule_id | bigint unsigned | NO | MUL | | FK to alert_rules |
| triggered_at | timestamp | NO | | CURRENT_TIMESTAMP | When the alert fired |
| context_json | json | YES | | NULL | Full context (player name, dates, etc.) |
| recipients_json | json | YES | | NULL | Who was notified |
| status | varchar(20) | NO | MUL | sent | sent/acknowledged |
| acknowledged_by | bigint unsigned | YES | | NULL | FK to employees |
| acknowledged_at | timestamp | YES | | NULL | When acknowledged |

### 3.3 `trigger_config_json` Examples

```json
// MEDICAL_CERT_EXPIRY
{"days_before": 30}

// ATTENDANCE_DROPPING
{"consecutive_absences": 3}

// GROUP_NEAR_CAPACITY
{"capacity_percent": 90}

// SUBSCRIPTION_OVERDUE
{"days_overdue": 7}

// CONTRACT_EXPIRY
{"days_before": 30}
```

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /alerts | AlertController@index | auth | alert.view |
| GET | /alerts/log | AlertController@log | auth | alert.view |
| POST | /alerts/{id}/toggle | AlertController@toggle | auth, csrf | alert.manage |
| POST | /alerts/log/{id}/acknowledge | AlertController@acknowledge | auth, csrf | alert.view |

---

## 5. Events Dispatched

None. The Alerts module does not dispatch any events on the EventBus.

---

## 6. Events Consumed

None. The Alerts module does not listen to any EventBus events. It operates purely on scheduled polling (cron).

---

## 7. Cross-Module Dependencies

### 7.1 Alerts module IMPORTS FROM (via direct SQL queries):

| Module / Table | What it queries |
|----------------|----------------|
| PlayerAffairs | `player_medical_records` — expiry date checks |
| PlayerAffairs | `players` — player names, medical_expiry_date |
| Academies | `academy_contracts` — contract end dates |
| Academies | `academies` — academy names |
| SportsActivity | `session_attendance` — absence counting |
| SportsActivity | `training_sessions` — session dates |
| SportsActivity | `training_groups` + `training_group_players` — capacity checks |
| SportsActivity | `activity_subscriptions` — overdue checks |

### 7.2 Other modules that IMPORT FROM Alerts:

None. No other module imports from the Alerts module.

### 7.3 Dual Implementation (Web vs Cron)

There are **two parallel implementations** of the alert processing logic:

1. **`AlertProcessorService`** (web-callable, uses App singleton) — referenced in the module's service layer
2. **`AlertProcessorJob`** (cron/jobs/, uses injected Database) — the actual scheduled runner

They check similar but not identical rules, and the cron job includes deduplication logic (24-hour window) that the web service does not.

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| alert.view | View alert rules list and alert log |
| alert.manage | Enable/disable alert rules |

**Menu placement**: Child of `settings` menu, order 950.

---

## 9. Background Processes (Cron)

### AlertProcessorJob (`cron/jobs/AlertProcessorJob.php`)

- **Schedule**: Hourly (via system cron calling `php cli.php cron`)
- **Logic**: Iterates all active rules, evaluates each based on `code`, inserts `alert_log` entries
- **Deduplication**: For MEDICAL_CERT_EXPIRY, checks if same player was already alerted in last 24 hours
- **Rules handled in cron**: MEDICAL_CERT_EXPIRY, CONTRACT_EXPIRY, ATTENDANCE_DROPPING, GROUP_NEAR_CAPACITY, SUBSCRIPTION_OVERDUE
- **Rules NOT handled** (exist in DB but no code): REVENUE_BELOW_MINIMUM, SETTLEMENT_DUE, WAITING_LIST_SPOT, POOL_SAFETY_THRESHOLD

---

## 10. High-Risk Areas

### 10.1 Unimplemented Rules

4 of 9 alert rules have no code handler in either the cron job or the web service:
- `REVENUE_BELOW_MINIMUM` — defined but falls through to `default => []`
- `SETTLEMENT_DUE` — defined but falls through to `default => []`
- `WAITING_LIST_SPOT` — defined but falls through to `default => []`
- `POOL_SAFETY_THRESHOLD` — defined but falls through to `default => []`

These rules are marked as active (`is_active = 1`) but will never fire.

### 10.2 No Actual Notification Delivery

The `notification_channels` column defaults to `'system'` and `status` is set to `'sent'`, but there is:
- No email integration
- No SMS integration
- No push notification
- No real-time WebSocket/SSE

Alerts only appear when a user manually navigates to `/alerts/log`.

### 10.3 Dual Code Paths

The `AlertProcessorService` (web) and `AlertProcessorJob` (cron) implement the same checks with slightly different SQL queries and logic. For example:
- Cron checks `players.medical_expiry_date` directly; web service checks `player_medical_records.expiry_date`
- Cron has deduplication; web service does not
- If one is updated, the other may drift

### 10.4 No Volume Control

The cron job inserts a new `alert_log` row for every matching entity every hour (except medical with 24h dedup). A training group at 91% capacity would generate 24 alert_log entries per day. No rate limiting or suppression exists for other rule types.

---

## 11. Known Patterns & Gotchas

1. **Empty alert_log**: Despite 9 active rules, the log has 0 entries — suggests the cron job may not be running, or conditions have never been met
2. **Acknowledgement is advisory only**: Acknowledging an alert only changes `status` to 'acknowledged' — it does not prevent the same alert from firing again next hour
3. **JSON_EXTRACT in dedup query**: The medical dedup uses `JSON_EXTRACT(context_json, '$.player_id')` which requires MySQL 5.7+ JSON support
4. **Category-based routing in web service vs code-based routing in cron**: `AlertProcessorService` dispatches by `$rule['category']`, while `AlertProcessorJob` dispatches by `$rule['code']` — adding new rules requires updating different dispatch logic
5. **Toggle is a raw UPDATE**: The toggle action bypasses the Model layer and does a direct `$db->update()` — no event dispatch or audit trail

---

## 12. Configuration Dependencies

All configuration is stored in `trigger_config_json` per rule. No system_config or rule engine dependencies.

| Config Key (in JSON) | Rule(s) | Purpose |
|---------------------|---------|---------|
| days_before | MEDICAL_CERT_EXPIRY, CONTRACT_EXPIRY | How many days before expiry to alert |
| consecutive_absences | ATTENDANCE_DROPPING | Absence threshold |
| capacity_percent / capacity_pct | GROUP_NEAR_CAPACITY | Utilization percentage threshold |
| days_overdue | SUBSCRIPTION_OVERDUE | Days past due date |
