# Phase 4 & 5 — Participant Lifecycle & Training Management

## Goal

Model the COMPLETE journey of a participant from first contact to graduation/withdrawal, and the full training delivery system from program definition to session execution.

---

# PART A: PARTICIPANT LIFECYCLE

## Module: Participants

### Entity: `participants`

A Participant is a Person enrolled in the academy's services.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| person_id | FK people | UNIQUE per academy |
| participant_number | string(20) | UNIQUE per academy, auto-generated |
| registration_date | date | When first registered |
| registration_source | string(20) | CHECK: walk_in, online, referral, social_media, event, transfer, other |
| referred_by_id | FK participants | Nullable — referral tracking |
| primary_guardian_id | FK guardians | Nullable — for minors |
| primary_activity_id | FK activities | Nullable — main sport |
| skill_level | string(20) | Nullable — CHECK: beginner, intermediate, advanced, professional |
| medical_clearance | boolean | Default false — has medical cert on file |
| medical_clearance_expires | date | Nullable |
| photo_path | string | Nullable |
| jersey_number | integer | Nullable |
| jersey_size | string(5) | Nullable |
| shoe_size | string(5) | Nullable |
| dominant_hand | string(5) | Nullable — CHECK: left, right, both |
| dominant_foot | string(5) | Nullable — CHECK: left, right, both |
| height_cm | decimal(5,1) | Nullable |
| weight_kg | decimal(5,1) | Nullable |
| status | string(20) | CHECK: see lifecycle below |
| status_changed_at | timestamp | |
| status_reason | text | Nullable — reason for current status |
| membership_type | string(20) | Nullable — CHECK: basic, silver, gold, platinum, custom |
| membership_expires_at | date | Nullable |
| total_paid | bigint | Default 0 — denormalized lifetime payment total |
| outstanding_balance | bigint | Default 0 — denormalized current debt |
| metadata | jsonb | Default '{}' — custom fields per academy |
| notes | text | Nullable |
| created_by | FK users | |
| updated_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

### Participant Status Lifecycle

```
                     ┌─────────────────────┐
                     │     registered      │ ← initial state
                     └──────────┬──────────┘
                                │
                     ┌──────────▼──────────┐
                     │       active        │ ← enrolled in ≥1 program
                     └──────────┬──────────┘
                                │
          ┌─────────────────────┼─────────────────────┐
          │                     │                     │
┌─────────▼─────────┐ ┌────────▼────────┐ ┌─────────▼─────────┐
│      frozen       │ │    suspended    │ │    on_leave       │
│(participant choice)│ │(academy decision)│ │(medical/travel)   │
└─────────┬─────────┘ └────────┬────────┘ └─────────┬─────────┘
          │                     │                     │
          └─────────────────────┼─────────────────────┘
                                │ (reinstate)
                     ┌──────────▼──────────┐
                     │       active        │
                     └──────────┬──────────┘
                                │
          ┌─────────────────────┼─────────────────────┐
          │                     │                     │
┌─────────▼─────────┐ ┌────────▼────────┐ ┌─────────▼─────────┐
│    graduated      │ │   withdrawn     │ │    transferred    │
│(completed program)│ │(left voluntarily)│ │(moved to another) │
└───────────────────┘ └─────────────────┘ └───────────────────┘
                                │
                     ┌──────────▼──────────┐
                     │      archived       │ ← no longer contactable
                     └─────────────────────┘
```

**Status Definitions:**
| Status | Meaning | Can Attend | Can Be Billed | Visible in Lists |
|--------|---------|-----------|--------------|-----------------|
| registered | Signed up, not yet enrolled | No | Registration fee only | Yes |
| active | Currently enrolled and attending | Yes | Yes | Yes |
| frozen | Self-requested pause (vacation, etc.) | No | Depends on freeze policy | Yes |
| suspended | Academy-imposed suspension (behavior, non-payment) | No | Existing debt only | Yes |
| on_leave | Approved leave (medical, travel) | No | Depends on policy | Yes |
| graduated | Completed a program cycle | No (current) | Renewal offers | Yes |
| withdrawn | Left the academy voluntarily | No | Outstanding debt only | Yes (filtered) |
| transferred | Moved to another branch/academy | No (here) | Outstanding debt only | Yes (filtered) |
| archived | Old record, no contact | No | No | No (admin only) |

### Valid Transitions
| From | To | Trigger | Required |
|------|----|---------|----------|
| registered | active | First enrollment confirmed + payment | Payment or plan |
| active | frozen | Participant requests freeze | Freeze form, reason |
| active | suspended | Academy suspends (behavior/non-payment) | Reason, authorized by |
| active | on_leave | Approved leave request | Leave form, dates |
| active | graduated | Completes program (all sessions + requirements) | Auto or manual |
| active | withdrawn | Participant leaves | Exit form, reason |
| active | transferred | Moved to other branch | Transfer form, destination |
| frozen | active | Participant resumes | Resume date, payment if applicable |
| suspended | active | Academy reinstates | Manager approval |
| on_leave | active | Return from leave | Resume date |
| graduated | active | Re-enrolls in new program | New enrollment + payment |
| withdrawn | active | Re-registers | New enrollment + payment |
| any inactive | archived | 2+ years inactive | Admin action |

### Freeze Rules
- Freeze has a maximum duration (configurable: 30 days, 60 days, etc.)
- Freeze may or may not extend the subscription end date (configurable)
- Some academies charge a freeze fee (handled via pricing rule)
- Auto-unfreeze after max duration → notification sent
- Can't freeze if outstanding balance > threshold (configurable)

### Suspension Rules
- Auto-suspend after N days overdue on payment plan (configurable)
- Auto-suspend after N missed sessions without excuse (configurable)
- Suspension blocks attendance and new enrollments
- Reinstatement may require clearing outstanding balance
- Behavior-based suspension: documented incident required

---

## Module: Enrollment

### Entity: `enrollments`

Links participants to training groups.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| participant_id | FK participants | |
| training_group_id | FK training_groups | |
| training_program_id | FK training_programs | Denormalized for reporting |
| enrollment_date | date | |
| start_date | date | When attendance starts (may differ from enrollment_date) |
| end_date | date | Nullable — program end or withdrawal date |
| status | string(20) | CHECK: pending, active, completed, frozen, withdrawn, transferred, cancelled |
| enrolled_by | FK users | |
| invoice_id | FK invoices | Nullable — the invoice that covers this enrollment |
| payment_status | string(20) | CHECK: paid, pending, partial, overdue, waived |
| sessions_attended | integer | Default 0 — denormalized counter |
| sessions_total | integer | Nullable — expected total sessions |
| attendance_percentage | decimal(5,2) | Nullable — auto-calculated |
| completion_percentage | decimal(5,2) | Nullable |
| withdrawal_date | date | Nullable |
| withdrawal_reason | string | Nullable |
| transferred_to_id | FK training_groups | Nullable — if transferred |
| notes | text | Nullable |
| metadata | jsonb | Default '{}' |
| created_at | timestamp | |
| updated_at | timestamp | |

### Entity: `waitlists`

When a group is full.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| academy_id | FK organizations | |
| participant_id | FK participants | |
| training_group_id | FK training_groups | |
| position | integer | Queue position |
| requested_at | timestamp | |
| notified_at | timestamp | Nullable — when spot opened and they were notified |
| responded_at | timestamp | Nullable |
| response | string(20) | Nullable — CHECK: accepted, declined, expired |
| expires_at | timestamp | Notification expires after N hours |
| enrolled_enrollment_id | FK enrollments | Nullable — if they accepted and enrolled |
| notes | text | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |

### Enrollment Business Rules
- One participant can be enrolled in MULTIPLE groups (multi-sport)
- One participant can only be in ONE group per program (can't be in U12-A AND U12-B of same program)
- Enrollment blocked if: participant suspended, group full, prerequisite not met, payment not arranged
- Enrollment creates invoice (or adds item to existing draft invoice)
- Group transfer: old enrollment withdrawn + new enrollment created + attendance carries over
- Program completion: when all required sessions attended + evaluation passed (if applicable)
- Enrollment start_date may be future (pre-registration for next month's cycle)

### Waitlist Business Rules
- When someone withdraws from a full group → next on waitlist gets notified
- Notification has an expiry (24h default) — if no response, skip to next
- Waitlist position is FIFO unless academy manually reorders (VIP priority)
- Participant can be on waitlist for multiple groups simultaneously
- When enrolled from waitlist: position removed, notified_at and response recorded

---

# PART B: TRAINING MANAGEMENT

## Module: Activities

### Entity: `activities`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| name | string | Required — "Football", "Swimming" |
| name_ar | string | Required |
| slug | string | Unique per academy |
| description | text | Nullable |
| icon | string | Nullable — icon name or path |
| color | string(7) | Nullable — hex color for UI |
| category | string(20) | CHECK: sport, art, academic, fitness, other |
| is_active | boolean | Default true |
| sort_order | integer | Default 0 |
| metadata | jsonb | Default '{}' — activity-specific config (field size, equipment needed, etc.) |
| created_at | timestamp | |
| updated_at | timestamp | |

---

## Module: Training Programs

### Entity: `training_programs`

The COMMERCIAL OFFERING — what the academy sells.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| activity_id | FK activities | |
| branch_id | FK branches | Nullable — null = all branches |
| name | string | |
| name_ar | string | |
| slug | string | Unique per academy |
| description | text | Nullable |
| description_ar | text | Nullable |
| objectives | jsonb | Default '[]' — learning outcomes |
| skill_level | string(20) | CHECK: all, beginner, intermediate, advanced, professional |
| age_min | integer | Nullable — minimum age |
| age_max | integer | Nullable — maximum age |
| gender | string(10) | CHECK: all, male, female |
| sessions_per_week | integer | Default 2 |
| session_duration_minutes | integer | Default 60 |
| total_sessions | integer | Nullable — null = ongoing/unlimited |
| program_duration_weeks | integer | Nullable — null = ongoing |
| min_participants | integer | Default 1 — below this, group may cancel |
| max_participants | integer | Default 20 |
| allow_waitlist | boolean | Default true |
| attendance_requirement_percent | integer | Nullable — min % for completion |
| assessment_required | boolean | Default false — exam/eval needed to graduate |
| prerequisites | jsonb | Default '[]' — required prior programs or level |
| facility_type | string(30) | Nullable — preferred facility type |
| equipment_required | jsonb | Default '[]' — equipment list |
| registration_open | boolean | Default true |
| registration_deadline | date | Nullable |
| program_start_date | date | Nullable — for fixed-term programs |
| program_end_date | date | Nullable |
| renewal_policy | string(20) | CHECK: auto_renew, manual_renew, one_time |
| cancellation_policy | text | Nullable |
| refund_policy | text | Nullable |
| status | string(20) | CHECK: draft, active, full, closed, archived |
| featured | boolean | Default false — shown prominently |
| sort_order | integer | Default 0 |
| metadata | jsonb | Default '{}' |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

---

## Module: Training Groups

### Entity: `training_groups`

The OPERATIONAL INSTANCE — where delivery happens.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| program_id | FK training_programs | |
| branch_id | FK branches | |
| name | string | e.g., "U12 Boys - Group A" |
| name_ar | string | |
| code | string(10) | Unique per academy — short code |
| head_trainer_id | FK trainers | Nullable |
| max_capacity | integer | Inherited from program, can override |
| current_count | integer | Default 0 — denormalized enrollment count |
| waitlist_count | integer | Default 0 |
| season | string | Nullable — "Summer 2026", "2025-2026" |
| start_date | date | Nullable |
| end_date | date | Nullable |
| status | string(20) | CHECK: planning, open, running, full, completed, cancelled, archived |
| status_changed_at | timestamp | |
| notes | text | Nullable |
| metadata | jsonb | Default '{}' |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

### Group Status Lifecycle
```
planning → open → running → completed
              ↓        ↓
             full    cancelled
```
- `planning`: group being set up, no enrollment yet
- `open`: accepting enrollments
- `running`: active delivery, may still accept late enrollment
- `full`: max capacity reached (auto-set when current_count = max_capacity)
- `completed`: program cycle finished
- `cancelled`: group cancelled (too few enrollments, no trainer, etc.)

---

## Module: Schedules

### Entity: `training_schedules`

Defines WHEN a group trains (recurring template).

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| training_group_id | FK training_groups | |
| facility_id | FK facilities | |
| day_of_week | integer | 0=Sunday, 6=Saturday |
| start_time | time | |
| end_time | time | |
| trainer_id | FK trainers | Nullable — override group's head trainer |
| assistant_trainer_id | FK trainers | Nullable |
| space_reservation_template | jsonb | Nullable — which cells/lanes to reserve |
| effective_from | date | When this schedule starts |
| effective_until | date | Nullable — null = indefinite |
| is_active | boolean | Default true |
| notes | text | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |

### Schedule Business Rules
- One group can have multiple schedule entries (Mon+Wed+Fri)
- Schedule conflicts: system prevents same facility+time+overlapping spaces
- Trainer conflicts: system prevents same trainer at two places simultaneously
- Schedule change: affects FUTURE sessions only, existing ones preserved
- Effective dates allow schedule changes mid-season without losing history

---

## Module: Training Sessions (Generated)

### Entity: `training_sessions`

The ACTUAL scheduled occurrence. Generated from schedules.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| training_group_id | FK training_groups | |
| schedule_id | FK training_schedules | Which schedule generated this |
| facility_id | FK facilities | |
| session_date | date | |
| start_time | time | |
| end_time | time | |
| trainer_id | FK trainers | Actual trainer (may differ from schedule due to substitution) |
| assistant_trainer_id | FK trainers | Nullable |
| substitute_reason | text | Nullable — why trainer changed |
| session_number | integer | Sequence within the group (1, 2, 3...) |
| topic | string | Nullable — what's being trained |
| objectives | text | Nullable — session goals |
| status | string(20) | CHECK: scheduled, in_progress, completed, cancelled, rescheduled, no_show |
| cancelled_reason | text | Nullable |
| cancelled_by | FK users | Nullable |
| rescheduled_to_id | FK training_sessions | Nullable — if rescheduled, link to new session |
| weather_condition | string(20) | Nullable — outdoor sports: sunny, rainy, hot, cold, windy |
| actual_start_time | time | Nullable — when it actually started |
| actual_end_time | time | Nullable |
| expected_participants | integer | Denormalized from enrollment count |
| actual_participants | integer | Nullable — filled after attendance taken |
| notes | text | Nullable — session notes by trainer |
| incident_notes | text | Nullable — injuries, problems |
| equipment_used | jsonb | Default '[]' |
| metadata | jsonb | Default '{}' |
| created_at | timestamp | |
| updated_at | timestamp | |

### Session Generation Service

`SessionGeneratorService::generate(TrainingGroup $group, Carbon $from, Carbon $to)`

Logic:
1. Get all active schedules for the group within date range
2. For each schedule: generate one session per matching day_of_week
3. Skip dates that are in `holidays` table (academy-wide or branch-specific)
4. Skip dates where facility is marked unavailable/maintenance
5. Check for existing sessions (don't duplicate)
6. Create sessions in bulk
7. For each session: auto-create attendance expectations for enrolled participants + assigned trainers

**When sessions are generated:**
- On group status change to "running" → generate for next 4 weeks (configurable)
- Weekly scheduled job → generate next week's sessions
- Manual trigger by admin → generate for specific date range

### Entity: `holidays`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| academy_id | FK organizations | |
| branch_id | FK branches | Nullable — null = all branches |
| name | string | |
| name_ar | string | |
| date | date | |
| is_recurring | boolean | Default false — same date every year |
| affects | string(20) | CHECK: all, training, facility, staff |
| created_at | timestamp | |

### Session Edge Cases
- **Cancelled session:** Status=cancelled, reason recorded, no attendance taken, trainer not paid for this session
- **Rescheduled session:** Original status=rescheduled, link to new session. New session has different date/time
- **Substitute trainer:** Original trainer unavailable → substitute assigned. Original trainer NOT marked absent (they have a separate assignment)
- **Make-up session:** Extra session added to compensate for cancellation. Not linked to schedule, manual creation
- **Merged sessions:** Two groups train together (joint session). Each group gets its own session record, same facility+time
- **Split session:** Group too large, split into A/B sub-groups alternating. Handled via schedule (half go Mon, half go Wed)
- **Weather cancellation:** Outdoor facility, raining → mass cancel today's sessions for affected facilities

---

## Module: Evaluations (Program Completion)

### Entity: `evaluations`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| participant_id | FK participants | |
| enrollment_id | FK enrollments | |
| evaluator_id | FK trainers | Who evaluated |
| evaluation_date | date | |
| type | string(20) | CHECK: progress, midterm, final, assessment, trial |
| criteria | jsonb | Array of {name, max_score, score, notes} |
| total_score | decimal(5,2) | Nullable — calculated from criteria |
| max_possible_score | decimal(5,2) | Nullable |
| percentage | decimal(5,2) | Nullable |
| grade | string(5) | Nullable — A, B, C, D, F or custom |
| passed | boolean | Nullable |
| recommendation | string(30) | Nullable — CHECK: promote, repeat, graduate, transfer_up, transfer_down |
| feedback | text | Nullable — trainer's written feedback |
| feedback_ar | text | Nullable |
| parent_visible | boolean | Default false — show to guardian |
| status | string(20) | CHECK: draft, submitted, approved, shared |
| shared_at | timestamp | Nullable — when shared with guardian |
| created_at | timestamp | |
| updated_at | timestamp | |

### Evaluation Business Rules
- Evaluations can be templated per program (standard criteria)
- Criteria are flexible: technical skills, fitness, behavior, attendance, teamwork
- Passing score configurable per program
- "promote" recommendation → participant moves to next skill level
- "graduate" recommendation → enrollment.status = completed
- Evaluations feed into certificates (Phase 19 - Documents)
- Parent/guardian can see evaluations if `parent_visible = true` and `status = shared`

---

## Services Layer

### `ParticipantService`
- `register(data, guardian, user)` — creates Person + Participant, handles national ID parsing
- `activate(participant)` — status = active (requires enrollment)
- `freeze(participant, reason, duration, user)` — applies freeze with policy check
- `suspend(participant, reason, user)` — academy suspension
- `reinstate(participant, user)` — resume from frozen/suspended
- `withdraw(participant, reason, user)` — process withdrawal
- `transfer(participant, toGroup, user)` — transfer between groups

### `EnrollmentService`
- `enroll(participant, group, user)` — validates capacity/prereqs/payment, creates enrollment
- `unenroll(enrollment, reason, user)` — withdraw from group
- `transfer(enrollment, toGroup, user)` — move between groups
- `complete(enrollment)` — mark as completed (if requirements met)
- `joinWaitlist(participant, group)` — add to queue
- `processWaitlist(group)` — notify next in line when spot opens

### `SessionGeneratorService`
- `generate(group, from, to)` — bulk create sessions from schedule
- `generateUpcoming()` — scheduled job: generate next week for all running groups
- `cancelSession(session, reason, user)` — cancel with notifications
- `rescheduleSession(session, newDate, newTime, user)` — create new, mark old as rescheduled
- `substituteTrainer(session, newTrainer, reason, user)` — swap trainer for one session

### `EvaluationService`
- `create(enrollment, evaluator, criteria, scores)` — draft evaluation
- `submit(evaluation)` — lock for review
- `approve(evaluation, approver)` — manager approves
- `share(evaluation)` — make visible to guardian, send notification
- `promote(evaluation)` — act on recommendation

---

## Scheduled Jobs (Training)

| Job | Frequency | Action |
|-----|-----------|--------|
| `GenerateUpcomingSessions` | Daily | Generate next 7 days of sessions for running groups |
| `SendSessionReminders` | Daily | Notify participants of tomorrow's sessions |
| `MarkNoShowSessions` | Hourly | Sessions 2h past end_time with no attendance taken → flag for admin |
| `AutoCancelLowEnrollment` | Weekly | Groups in "open" with enrollment < min_participants past deadline → notify admin |
| `ProcessWaitlists` | On withdrawal event | When someone leaves a full group → notify next waitlisted |
| `CheckMedicalClearance` | Daily | Flag participants with expired medical clearance |

---

## Events Dispatched

| Event | Trigger | Consumers |
|-------|---------|-----------|
| `ParticipantRegistered` | New registration | Welcome notification |
| `ParticipantStatusChanged` | Any status transition | Audit log, conditional notifications |
| `EnrollmentCreated` | Participant enrolled in group | Invoice generation, session access |
| `EnrollmentWithdrawn` | Participant leaves group | Waitlist processing, prorated refund calc |
| `SessionCreated` | Session generated | Attendance expectations |
| `SessionCancelled` | Session cancelled | Notify participants + trainer, reschedule prompt |
| `SessionCompleted` | Attendance taken + session closed | Trainer payment calc, attendance stats |
| `EvaluationShared` | Evaluation shared with guardian | Notification to guardian |
| `WaitlistSpotAvailable` | Spot opens in full group | Notification to next in queue |
