# AcademyContracts Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The AcademyContracts module manages the **financial agreements between the club and external/internal academy operators**. It handles:
- Contract CRUD (draft, activate, terminate)
- Revenue-share, fixed-rent, or hybrid contract types
- Monthly settlement generation and approval
- Revenue tracking and comparison against minimum guarantees
- Guarantee report (revenue vs minimum across periods)
- Contract document upload (PDF)

This module represents the **club's financial relationship** with academy operators — it determines how revenue is split, whether minimums are met, and who pays whom.

It does **NOT** directly manage:
- Academy definitions or enrollments (Academies module)
- Player registrations or subscriptions (PlayerAffairs, ActivitySubscriptions)
- Actual payment processing (handled via events to Payments/Cashier)
- Coach assignments (Coaches module)

**Classification:** Fully independent module with its own controllers, routes, views, services, and models. NOT a wrapper or redirect.

---

## 2. Directory & File Structure

```
app/Modules/AcademyContracts/
├── bootstrap.php                          # Permission registration (3 permissions)
├── Routes.php                             # Web routes (14 routes)
├── Controllers/
│   ├── AcademyContractController.php     # Contract CRUD + activate + terminate
│   ├── SettlementController.php          # Settlement list, show, generate, approve
│   └── ReportController.php             # Guarantee report
├── Models/
│   ├── AcademyContract.php               # Main model (soft delete, author tracking)
│   └── AcademySettlement.php             # Settlement model (no soft delete)
├── Services/
│   ├── AcademyContractService.php        # Activate, terminate, generate contract number
│   └── SettlementService.php             # Calculate settlement, generate monthly, approve
└── Views/
    ├── index.php                         # Contract list with filters
    ├── create.php                        # Contract creation form
    ├── edit.php                          # Contract edit form
    ├── show.php                          # Contract detail + settlements
    ├── settlement_index.php             # Settlement list with filters
    ├── settlement_show.php              # Settlement detail page
    └── guarantee_report.php             # Revenue vs guarantee report
```

---

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

### 3.1 `academy_contracts` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| contract_number | varchar(50) | NO | UNI | Format: AC-YYYY-XXXX |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| contract_type | varchar(30) | NO | | revenue_share/fixed_rent/hybrid |
| start_date | date | NO | MUL | Contract start |
| end_date | date | NO | | Contract end |
| minimum_revenue_guarantee | decimal(15,2) | NO | | Default: 0.00 |
| club_commission_pct | decimal(5,2) | NO | | Club's share % on surplus |
| academy_share_pct | decimal(5,2) | NO | | Academy's share % on surplus |
| fixed_monthly_rent | decimal(15,2) | NO | | For fixed_rent type |
| deposit_amount | decimal(15,2) | NO | | Security deposit |
| deposit_status | varchar(20) | NO | | pending/paid/returned |
| settlement_day | tinyint unsigned | NO | | Day of month for settlement (default: 5) |
| grace_period_days | tinyint unsigned | NO | | Days after settlement day (default: 7) |
| penalty_rate_pct | decimal(5,2) | NO | | Penalty on shortfall |
| auto_renew | tinyint(1) | NO | | Default: 0 |
| renewal_notice_days | int unsigned | NO | | Default: 30 |
| status | varchar(20) | NO | MUL | draft/pending_approval/active/suspended/expired/terminated |
| approved_by | bigint unsigned | YES | | FK to employees |
| approved_at | timestamp | YES | | |
| terminated_by | bigint unsigned | YES | | FK to employees |
| terminated_at | timestamp | YES | | |
| termination_reason | text | YES | | |
| terms_json | json | YES | | Additional terms |
| notes | text | YES | | |
| contract_document_path | varchar(500) | YES | | Uploaded PDF path |
| 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 | | |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |
| branch_id | bigint unsigned | YES | | FK to branches |

### 3.2 `academy_settlements` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| settlement_number | varchar(50) | NO | UNI | Format: STL-YYYY-XXXX |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| contract_id | bigint unsigned | NO | MUL | FK to academy_contracts |
| period_month | varchar(7) | NO | | Format: YYYY-MM |
| total_revenue | decimal(15,2) | NO | | Calculated total for the month |
| minimum_guarantee | decimal(15,2) | NO | | From contract |
| revenue_surplus | decimal(15,2) | NO | | Revenue above minimum |
| revenue_shortfall | decimal(15,2) | NO | | Revenue below minimum |
| club_commission | decimal(15,2) | NO | | Club's commission on surplus |
| academy_share | decimal(15,2) | NO | | Academy's share on surplus |
| net_amount | decimal(15,2) | NO | | Final amount to be paid |
| penalty_amount | decimal(15,2) | NO | | Penalty on shortfall |
| direction | varchar(20) | NO | | club_pays_academy/academy_pays_club/balanced |
| status | varchar(20) | NO | MUL | draft/pending_approval/approved/paid/disputed |
| approved_by | bigint unsigned | YES | | FK to employees |
| approved_at | timestamp | YES | | |
| payment_id | bigint unsigned | YES | | FK to payments (when paid) |
| paid_at | timestamp | YES | | |
| dispute_reason | text | YES | | |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |

### 3.3 `academy_revenue_records` Table (intermediate calculation table)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| contract_id | bigint unsigned | NO | MUL | FK to academy_contracts |
| period_month | varchar(7) | NO | | Format: YYYY-MM |
| subscription_revenue | decimal(15,2) | NO | | From activity_subscriptions |
| registration_revenue | decimal(15,2) | NO | | Default: 0.00 (not yet used) |
| other_revenue | decimal(15,2) | NO | | From coach group payments |
| total_revenue | decimal(15,2) | NO | | Sum of above |
| enrolled_count | int unsigned | NO | | Total enrollments for academy |
| active_players_count | int unsigned | NO | | Active enrollments |
| is_finalized | tinyint(1) | NO | | Default: 0 |
| calculated_at | timestamp | NO | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /academy-contracts | index | auth | academy_contract.view |
| GET | /academy-contracts/create | create | auth | academy_contract.manage |
| POST | /academy-contracts | store | auth, csrf | academy_contract.manage |
| GET | /academy-contracts/{id} | show | auth | academy_contract.view |
| GET | /academy-contracts/{id}/edit | edit | auth | academy_contract.manage |
| POST | /academy-contracts/{id} | update | auth, csrf | academy_contract.manage |
| POST | /academy-contracts/{id}/activate | activate | auth, csrf | academy_contract.manage |
| POST | /academy-contracts/{id}/terminate | terminate | auth, csrf | academy_contract.manage |
| GET | /academy-settlements/report | guaranteeReport | auth | academy_contract.settlement |
| GET | /academy-settlements | index | auth | academy_contract.settlement |
| GET | /academy-settlements/{id} | show | auth | academy_contract.settlement |
| POST | /academy-settlements/generate | generate | auth, csrf | academy_contract.settlement |
| POST | /academy-settlements/{id}/approve | approve | auth, csrf | academy_contract.settlement |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `academy.contract.activated` | `{contract_id, approved_by}` | AcademyContractService::activate |
| `academy.contract.terminated` | `{contract_id, terminated_by, reason}` | AcademyContractService::terminate |
| `academy.settlement.created` | `{settlement_id, contract_id, academy_id, month, direction, net_amount}` | SettlementService::calculateSettlement |
| `academy.settlement.approved` | `{settlement_id, approved_by, contract_id, academy_id, direction, net_amount}` | SettlementService::approveSettlement |

---

## 6. Events Consumed

None registered in bootstrap.php. This module does not listen to any events.

---

## 7. Cross-Module Dependencies

### 7.1 AcademyContracts module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Academies | `Academy::allActive()`, `Academy::find()` (contract-to-academy link) |

### 7.2 Data Dependencies (SQL queries, not PHP imports):

| Table | Owner Module | Usage |
|-------|--------------|-------|
| activity_subscriptions | ActivitySubscriptions | Revenue calculation (subscription income per month) |
| academy_enrollments | Academies | Revenue calculation (links subscriptions to academy) |
| payment_requests | Cashier | Coach group revenue calculation |
| sa_group_players | SportsActivity | Coach group revenue calculation |
| sa_groups | SportsActivity | Coach group revenue calculation |
| sa_coaches | SportsActivity | Coach group revenue by academy link |

### 7.3 Other modules with related functionality:

| Module | Overlap |
|--------|---------|
| SportsActivity | Has its own `AcademyContractController` at `/sa/academies/{aid}/contracts` routes |
| Accounting | Has `CrossEntitySettlementService` — separate from academy settlements |

---

## 8. Permissions

| Key | Arabic | English |
|-----|--------|---------|
| academy_contract.view | عرض عقود الأكاديميات | View Academy Contracts |
| academy_contract.manage | إدارة عقود الأكاديميات | Manage Academy Contracts |
| academy_contract.settlement | تسوية عقود الأكاديميات | Settle Academy Contracts |

---

## 9. Contract Types & Settlement Logic

### 9.1 Contract Types

| Type | Arabic | Description |
|------|--------|-------------|
| revenue_share | مشاركة إيرادات | Club and academy split revenue above minimum guarantee |
| fixed_rent | إيجار ثابت | Academy pays fixed monthly rent |
| hybrid | نظام مختلط | Combination of fixed and revenue share |

### 9.2 Contract Statuses

| Status | Arabic | Description |
|--------|--------|-------------|
| draft | مسودة | Initial creation state |
| pending_approval | في انتظار الموافقة | Submitted for review |
| active | نشط | Approved and operating |
| suspended | موقوف | Temporarily halted |
| expired | منتهي | End date passed |
| terminated | ملغى | Manually terminated with reason |

### 9.3 Settlement Calculation Flow

```
1. Query activity_subscriptions (by enrollment -> academy) for the month
2. Query payment_requests (by coach group -> sa_coaches linked to academy) for the month
3. Total Revenue = subscription_revenue + coach_group_revenue
4. Compare against minimum_revenue_guarantee:
   - If total >= minimum: surplus = total - minimum
     - academy_share = surplus * academy_share_pct%
     - club_commission = surplus * club_commission_pct%
     - direction = 'club_pays_academy'
   - If total < minimum: shortfall = minimum - total
     - penalty = shortfall * penalty_rate_pct%
     - direction = 'academy_pays_club'
   - If total == minimum: direction = 'balanced'
5. Create academy_revenue_records (detail breakdown)
6. Create academy_settlements (financial result)
```

### 9.4 Settlement Direction

| Direction | Arabic | Meaning |
|-----------|--------|---------|
| club_pays_academy | النادي يدفع للأكاديمية | Revenue exceeded guarantee |
| academy_pays_club | الأكاديمية تدفع للنادي | Revenue below guarantee |
| balanced | متوازن | Revenue exactly equals guarantee |

---

## 10. High-Risk Areas

### 10.1 Settlement Calculation — Revenue Source Complexity
- Revenue is aggregated from TWO different sources with complex joins:
  1. `activity_subscriptions` -> `academy_enrollments` -> `academy_id`
  2. `payment_requests` -> `sa_group_players` -> `sa_groups` -> `sa_coaches` -> `academy_id`
- If any of these table structures change, settlement calculations will silently produce wrong numbers
- No validation that total revenue makes sense (e.g., negative values, absurdly high)

### 10.2 Contract Number / Settlement Number Generation
- Uses MAX() query to determine next sequential number
- Race condition: two simultaneous generations could produce duplicate numbers
- Numbers are `AC-YYYY-XXXX` and `STL-YYYY-XXXX` format (4-digit sequence resets yearly)

### 10.3 No Idempotency on Settlement Generation
- `generateMonthlySettlements()` checks if a settlement EXISTS for (contract_id, month) before creating
- But `calculateSettlement()` directly creates without checking — could create duplicates if called directly
- `academy_revenue_records` does check for existing and updates via upsert pattern

### 10.4 Overlap with SportsActivity Module
- SportsActivity has routes for academy contracts at `/sa/academies/{aid}/contracts`
- This creates TWO paths to manage the same data — possible inconsistency
- The standalone AcademyContracts module has more features (settlements, reports)

### 10.5 PDF Document Upload
- Uses direct `move_uploaded_file()` — no virus scanning
- File type check only validates MIME type (easily spoofable)
- Size limit: 10MB
- Upload path: `storage/uploads/academy_contracts/`

---

## 11. Known Patterns & Gotchas

1. **Settlement approval does NOT trigger payment**: After approval, `payment_id` and `paid_at` exist as columns but the approved settlement must be manually linked to a payment elsewhere
2. **No auto-settlement generation**: Settlements must be manually triggered via POST /academy-settlements/generate — no cron job runs this automatically
3. **Guarantee report is read-only**: Only shows data, does not modify settlements
4. **Contract document is optional**: Can be uploaded on create or edit, stored as PDF
5. **branch_id exists in DB** but is not used in the PHP model's `$fillable` array
6. **deposit_status** tracked but no automated deposit management workflow
