# RULE FILE 04 — Participant Lifecycle & Training Rules

---

## Participant Status State Machine — THE LAW

### The `participants` Table Contract

```
id, uuid, academy_id, person_id (FK people), branch_id,
status, status_changed_at, status_reason,
registration_date, first_enrollment_date (nullable),
classification, source,
medical_notes, skill_level,
metadata (jsonb), created_by, created_at, updated_at, deleted_at
```

### Status Values

```
CHECK (status IN ('registered', 'active', 'frozen', 'suspended', 'inactive', 'graduated', 'transferred', 'withdrawn', 'blacklisted'))
```

### Valid State Transitions — ONLY THESE ARE ALLOWED

```
registered  → active (first enrollment paid/confirmed)
registered  → withdrawn (never enrolled, left)

active      → frozen (participant/guardian requests freeze)
active      → suspended (rule violation OR attendance threshold breach)
active      → inactive (all enrollments ended, no renewal)
active      → graduated (completed program with graduation criteria)
active      → transferred (moving to another branch or academy)
active      → withdrawn (voluntary leave)
active      → blacklisted (severe violation, admin decision)

frozen      → active (freeze period ends, resumes)
frozen      → withdrawn (decides not to return during freeze)
frozen      → inactive (freeze expires without renewal)

suspended   → active (suspension lifted by admin)
suspended   → withdrawn (during suspension, decides to leave)
suspended   → blacklisted (escalation)

inactive    → active (re-enrolls)
inactive    → withdrawn (confirms won't return)

graduated   → active (enrolls in new program)

transferred → (terminal for this academy — person record stays, status won't change)
withdrawn   → active (comes back, re-enrolls)
blacklisted → (terminal — only super_admin can override)
```

### INVALID Transitions — HARD BLOCK IN SERVICE

- `frozen → graduated` (can't graduate while frozen)
- `suspended → graduated` (can't graduate while suspended)
- `blacklisted → anything` (terminal without override)
- `graduated → frozen` (doesn't make sense)
- Any status → `registered` (can't go back to initial state)

### Transition Enforcement

```php
class ParticipantService
{
    private const VALID_TRANSITIONS = [
        'registered' => ['active', 'withdrawn'],
        'active' => ['frozen', 'suspended', 'inactive', 'graduated', 'transferred', 'withdrawn', 'blacklisted'],
        'frozen' => ['active', 'withdrawn', 'inactive'],
        'suspended' => ['active', 'withdrawn', 'blacklisted'],
        'inactive' => ['active', 'withdrawn'],
        'graduated' => ['active'],
        'transferred' => [],
        'withdrawn' => ['active'],
        'blacklisted' => [],
    ];

    public function changeStatus(Participant $participant, string $newStatus, string $reason): void
    {
        $allowed = self::VALID_TRANSITIONS[$participant->status] ?? [];
        if (!in_array($newStatus, $allowed)) {
            throw new InvalidStatusTransitionException(
                "Cannot transition from '{$participant->status}' to '{$newStatus}'"
            );
        }
        // ... proceed with transition
    }
}
```

---

## Freeze Rules

### The `participant_freezes` Table

```
id, participant_id, academy_id,
start_date, end_date, actual_end_date (nullable),
reason, approved_by (FK users),
status (active/completed/cancelled),
extends_enrollment (boolean — does freeze extend the enrollment end_date?),
created_at, updated_at
```

### Freeze Business Rules

1. **Maximum freeze duration**: configurable per academy (default: 30 days per year)
2. **Freeze count per year**: configurable (default: 2 freezes per calendar year)
3. **Cannot freeze if**: overdue balance exists (must pay first or set up plan)
4. **Enrollment extension**: if `extends_enrollment = true`, all active enrollments get `end_date += freeze_duration`
5. **During freeze**: participant is NOT expected in attendance (records not generated)
6. **Freeze expiry**: scheduled job checks daily. If `end_date` reached → auto-transition to `active` or `inactive` (based on whether enrollment is still valid)
7. **Early unfreeze**: admin can end freeze early → `actual_end_date = today`, status → 'active'
8. **Financial during freeze**: no new invoices generated for subscription periods during freeze

---

## Suspension Rules

### Auto-Suspension Triggers

| Trigger | Threshold | Action |
|---------|-----------|--------|
| Consecutive absences | configurable (default: 5) | Auto-suspend + notify guardian |
| Overdue payment | configurable (default: 30 days overdue) | Auto-suspend + notify |
| Admin manual | N/A | Immediate with reason |

### Suspension Effects

1. Participant marked `status = 'suspended'`
2. Attendance records still generated (marked 'exempt' or not generated — configurable)
3. Cannot make new purchases (except paying outstanding balance)
4. Cannot enroll in new programs
5. Guardian receives notification with reason
6. Lifting suspension: admin action only, must provide reason, creates audit log

---

## Enrollment

### The `enrollments` Table Contract

```
id, uuid, academy_id, participant_id, training_group_id,
status, enrolled_at, start_date, end_date (nullable),
fee_amount (bigint — price at time of enrollment, from pricing engine),
invoice_id (nullable — FK invoices),
payment_status (paid/unpaid/partial),
enrolled_by (FK users), cancelled_at, cancellation_reason,
metadata (jsonb), created_at, updated_at
```

### Enrollment Status

```
CHECK (status IN ('pending', 'active', 'completed', 'cancelled', 'expired', 'waitlisted'))
```

### Enrollment Rules

1. **One participant, one group**: cannot enroll same participant in same group twice (unique constraint on participant_id + training_group_id where status IN ('pending', 'active'))
2. **One participant, multiple programs**: ALLOWED (e.g., football + swimming)
3. **Group capacity check**: before enrollment, check `current_enrolled < group.max_capacity`
4. **If at capacity**: enrollment goes to `waitlisted` status (waitlist module handles)
5. **Enrollment creates invoice**: unless `fee_amount = 0` (scholarship, free trial)
6. **Pending → Active**: when payment confirmed OR admin manually activates
7. **Participant becomes 'active'**: on FIRST enrollment activation (if was 'registered')
8. **End date**: calculated from `start_date + program.duration_months` (or null for ongoing)
9. **Renewal**: new enrollment record created (not modifying existing). Old one → 'completed'.
10. **Cancellation**: refund policy applies based on days since start (configurable per program)

### Enrollment → Attendance Connection

When enrollment is `active`:
- Participant is expected at ALL sessions of the enrolled `training_group`
- Attendance records auto-generated for upcoming sessions
- If enrollment cancelled → future attendance records deleted (past ones remain as historical)

---

## Training Programs

### The `training_programs` Table Contract

```
id, uuid, academy_id, activity_id (FK activities),
name, name_ar, code (unique per academy),
description, description_ar,
duration_type (fixed/ongoing/session_based),
duration_months (nullable — for fixed),
session_count (nullable — for session_based),
min_age, max_age (nullable),
min_participants, max_participants,
skill_level_required (nullable),
is_active, status (draft/active/archived),
metadata (jsonb), created_by, created_at, updated_at, deleted_at
```

### Program Rules

1. Program is the COMMERCIAL PRODUCT (what parents see and buy)
2. Program contains one or more GROUPS (operational instances)
3. `duration_type`:
   - `fixed`: has explicit start and end (3-month course)
   - `ongoing`: no end date, monthly billing cycle
   - `session_based`: ends after N sessions attended
4. Age restrictions enforced at enrollment time (not retroactively)
5. `skill_level_required`: if set, participant must have matching `skill_level` to enroll
6. Program can belong to multiple branches (through groups)
7. Archiving a program does NOT affect active enrollments — they continue until end_date

---

## Training Groups

### The `training_groups` Table Contract

```
id, uuid, academy_id, training_program_id, branch_id,
name, name_ar, code (unique per academy),
status, min_capacity, max_capacity, current_count (denormalized),
skill_level, age_group_label,
start_date, end_date (nullable),
schedule_summary (string — "Sun/Tue/Thu 5:00-6:30 PM"),
metadata (jsonb), created_by, created_at, updated_at
```

### Group Status

```
CHECK (status IN ('forming', 'active', 'full', 'on_hold', 'completed', 'cancelled'))
```

### Group Status Transitions

```
forming   → active (min_capacity reached + start_date arrived)
forming   → cancelled (insufficient enrollment before deadline)
active    → full (current_count >= max_capacity)
active    → on_hold (temporary pause — holiday, facility issue)
active    → completed (end_date reached OR all sessions conducted)
active    → cancelled (admin decision)
full      → active (spot opens via cancellation)
on_hold   → active (resumes)
```

### Group Rules

1. `current_count` is denormalized — updated on enrollment/cancellation. Source of truth is count of active enrollments.
2. When group is `full`: new enrollment → waitlist
3. When group is `on_hold`: sessions are NOT generated/expected. Attendance paused.
4. `schedule_summary` is a human-readable string for display. Actual schedule lives in `group_schedules` table.

---

## Sessions

### The `training_sessions` Table Contract

```
id, uuid, academy_id, training_group_id,
facility_id, space_reservation_id (nullable),
date, start_time, end_time,
status, actual_start_time (nullable), actual_end_time (nullable),
trainer_id (FK people — primary trainer for this session),
substitute_trainer_id (nullable),
topic, notes,
actual_participants_count (nullable — filled after attendance),
is_makeup (boolean — is this a makeup session?),
original_session_id (nullable — if makeup, which session is it making up),
cancelled_at, cancellation_reason, cancelled_by,
created_at, updated_at
```

### Session Status

```
CHECK (status IN ('scheduled', 'in_progress', 'completed', 'cancelled', 'rescheduled'))
```

### Session Rules

1. Sessions are AUTO-GENERATED from group schedule + date range (SessionGenerationService)
2. Generation: for each group_schedule, create sessions for the next N weeks (configurable, default: 4 weeks ahead)
3. Skip holidays: `holidays` table defines dates where no sessions generated
4. Trainer assignment: from group's primary trainer assignment (Assignment Engine)
5. Substitute: overrides primary trainer for THIS session only
6. Cancellation: sets `status='cancelled'`, all attendance records for this session → `status='cancelled'`
7. Rescheduling: old session → `status='rescheduled'`, new session created with `original_session_id` pointing to old
8. Completion: after attendance is taken and session time has passed → `status='completed'`
9. `actual_participants_count` filled after attendance marking (COUNT of present + late + partial)
10. Makeup sessions: `is_makeup=true`, linked to original. Participant tracks which sessions they "owe" and which are made up.

### Session Generation Service

```php
SessionGenerationService::generateForGroup(TrainingGroup $group, Carbon $fromDate, Carbon $toDate): int
// Returns count of sessions created

// Algorithm:
// 1. Get all group_schedules for this group
// 2. For each schedule: iterate days from fromDate to toDate
// 3. If day_of_week matches AND day is not a holiday → create session
// 4. Link session to facility from group_schedule
// 5. Create space reservation if space_layout is defined
// 6. Return total sessions created
```

---

## Waitlist

### The `waitlists` Table

```
id, academy_id, participant_id, training_group_id,
position (integer — FIFO order),
status (waiting/notified/enrolled/expired/cancelled),
added_at, notified_at, notification_expires_at,
enrolled_at, expired_at,
notes, created_at, updated_at
```

### Waitlist Rules

1. Position is sequential per group (1, 2, 3...)
2. When spot opens: notify position 1 participant/guardian
3. Notification window: configurable (default: 48 hours to respond)
4. If no response within window: `status='expired'`, move to position 2 → notify
5. If accepts: `status='enrolled'`, create enrollment, remove from waitlist
6. If declines: `status='cancelled'`, move to next
7. Guardian receives: push notification + SMS (if configured) + email
8. Admin can override waitlist order (manual enrollment bypasses queue)

---

## Evaluations

### The `evaluations` Table

```
id, uuid, academy_id, participant_id, training_group_id,
evaluation_period_id (nullable), evaluator_id (FK people — trainer),
date, type (periodic/progress/final/skill_test),
overall_score (decimal 5,2), overall_rating (A/B/C/D/F or numeric),
notes, notes_for_guardian,
is_published (boolean — visible to guardian?),
created_by, created_at, updated_at
```

### The `evaluation_scores` Table (per-criterion)

```
id, evaluation_id, criterion_id (FK evaluation_criteria),
score (decimal 5,2), max_score (decimal 5,2),
notes, created_at
```

### Evaluation Rules

1. Criteria are defined per PROGRAM (not per group) — `evaluation_criteria` table
2. Each criterion has: name, name_ar, max_score, weight (for weighted average)
3. `overall_score` = weighted average of all criterion scores
4. Publishing: `is_published = false` initially. Trainer submits → admin reviews → publishes
5. Once published: guardian can view in parent portal
6. Evaluation does NOT auto-determine graduation — that's a separate admin action based on evaluation + attendance
7. Historical: evaluations are never deleted (soft-delete at most). They form the participant's progress record.

---

## Activities (Sport/Activity Types)

### The `activities` Table

```
id, uuid, academy_id, name, name_ar, slug,
description, icon, color,
is_active, sort_order,
metadata (jsonb), created_at, updated_at
```

### Activity Rules

1. Activity is the SPORT TYPE: Football, Swimming, Karate, Basketball, etc.
2. Activity is NOT a product — programs are products
3. One academy can have multiple activities
4. Programs belong to ONE activity
5. Activity deletion: RESTRICT if programs exist under it
6. Activities are tenant-specific (each academy defines their own list)
7. Default activities seeded on academy creation: Football, Swimming, Basketball, Karate, Gymnastics

---

## Cross-Module Integration: Participant

| When This Happens | These Modules React |
|-------------------|-------------------|
| Participant enrolls | Financial: invoice created. Attendance: records generated for future sessions. Pricing: engine calculates fee. |
| Participant frozen | Attendance: stop generating records. Financial: pause subscription billing. Sessions: mark exempt. |
| Participant suspended | Attendance: configurable (exempt or still expected). Financial: block new purchases except payments. POS: block sales. |
| Participant cancels enrollment | Financial: refund policy check. Attendance: delete future records. Group: decrement current_count. Waitlist: notify next. |
| Participant re-enrolls | All above in reverse — new invoice, new attendance expectations, increment count. |
| Participant graduates | Attendance: stop generating. Certificate: eligible for generation. Historical records preserved. |
