# RULE FILE 01 — Multi-Tenancy & Identity Rules

---

## Multi-Tenancy: The ONE Rule That Governs Everything

Every query, every insert, every update MUST be scoped to the current academy. A data leak across tenants is a business-ending bug.

### The Mechanism

```php
// BelongsToAcademy trait — applied to EVERY tenant-scoped model
trait BelongsToAcademy
{
    public static function bootBelongsToAcademy(): void
    {
        // Auto-set academy_id on create
        static::creating(function ($model) {
            if (!$model->academy_id && app()->bound('current_academy')) {
                $model->academy_id = app('current_academy')->id;
            }
        });

        // Global scope — ALWAYS filter by academy
        static::addGlobalScope('academy', function ($builder) {
            if (app()->bound('current_academy')) {
                $builder->where(
                    $builder->getModel()->getTable() . '.academy_id',
                    app('current_academy')->id
                );
            }
        });
    }

    public function academy(): BelongsTo
    {
        return $this->belongsTo(Organization::class, 'academy_id');
    }
}
```

### Tables That DO NOT Have `academy_id`

These are platform-level or the tenant table itself:
- `organizations` (this IS the tenant table)
- `migrations`, `jobs`, `failed_jobs`, `cache`, `sessions` (Laravel system)
- `system_settings` where `academy_id` is nullable (platform-level settings)
- `password_reset_tokens`

### Tables That ALWAYS Have `academy_id`

Everything else. Every single domain table. No exceptions.

### SuperAdmin Access

SuperAdmin bypasses the global scope:
```php
// In middleware for super-admin routes
Organization::withoutGlobalScope('academy')->get(); // sees all academies
```

Regular users NEVER bypass this scope. Ever.

### Cross-Tenant Data Integrity Rules

1. **FK references across tables MUST be within the same academy** — a payment in Academy A cannot reference an invoice in Academy B
2. **Unique constraints MUST include academy_id** — `slug` is unique PER academy, not globally
3. **Seeded data (chart of accounts, default roles) is duplicated PER academy** — not shared
4. **Reports NEVER aggregate across academies** (except SuperAdmin platform dashboard)

---

## User Identity

### The `users` Table Contract

```
id, uuid, academy_id, name, name_ar, email, phone, national_id,
date_of_birth, gender, password, avatar_path, status,
email_verified_at, last_login_at, login_count,
failed_login_attempts, locked_until,
remember_token, created_at, updated_at, deleted_at
```

**Critical**: `users` table has `academy_id` — a user belongs to ONE academy. No cross-tenant users (except SuperAdmin pattern handled differently).

### User Status Values

```
CHECK (status IN ('active', 'inactive', 'suspended', 'pending'))
```

- `active` — can login and use system
- `inactive` — account disabled by admin, cannot login
- `suspended` — temporarily blocked (auto or manual)
- `pending` — registered but not yet activated

### Authentication Flow Rules

1. Login checks: email+password → status must be `active` → `locked_until` must be null or past
2. Failed login: increment `failed_login_attempts`. At 5 → set `locked_until = now + 30min`
3. Successful login: reset `failed_login_attempts = 0`, `locked_until = null`, update `last_login_at`, increment `login_count`
4. Suspended user: show Arabic message "حسابك معلق — تواصل مع الإدارة"
5. Inactive user: show "هذا الحساب غير مفعل"

### Password Rules

- Min 8 characters (enforcement in FormRequest)
- Hashed with bcrypt (Laravel default)
- Password reset via email token (standard Laravel flow)
- No password history / forced rotation (overkill for sports academies)

---

## People Registry

### The `people` Table — The Universal Person

```
id, uuid, academy_id, user_id (nullable), 
name, name_ar, email, phone, phone_secondary,
national_id, date_of_birth, gender, nationality,
address, city, governorate, 
photo_path, blood_type, medical_notes,
emergency_contact_name, emergency_contact_phone, emergency_contact_relation,
classification, source, notes,
metadata, created_by, created_at, updated_at, deleted_at
```

**Critical relationship**: A `person` MAY have a `user_id` (login capability). Not all people are users — a 6-year-old participant has a `people` record but no `users` record. Their guardian has both.

### Person ↔ User Link Rules

1. `people.user_id` is nullable — most participants (children) don't have login accounts
2. When a person gets login access, create a `users` record and link: `people.user_id = users.id`
3. `users.academy_id` MUST match `people.academy_id` — never cross-tenant
4. Deleting a user does NOT delete the person — soft-delete user, person remains
5. One person = ONE user max. One user = ONE person (1:1, enforced by unique `user_id` on people)

### National ID (Egyptian) — Parsing Rules

Format: 14 digits → `CYYMMDDSSSSRG`

```
C   = Century (2=1900s, 3=2000s)
YY  = Year
MM  = Month
DD  = Day
SSSS = Sequence in governorate
R   = Gender (odd=male, even=female)
G   = Check digit
```

**Parsing service extracts**: birth_date, gender, governorate_code
**Validation**: Luhn-like check digit verification
**Storage**: stored as string(14), displayed masked: `312011XXXXXXXX`

### Gender Values

```
CHECK (gender IN ('male', 'female'))
```

No other values. Arabic display: male→'ذكر', female→'أنثى'

### Classification Values

```
CHECK (classification IN ('regular', 'vip', 'scholarship', 'staff_child', 'trial'))
```

- `regular` — pays normal rates
- `vip` — special treatment, may get priority
- `scholarship` — reduced/free (pricing engine handles amount)
- `staff_child` — employee's kid (discount via pricing rule)
- `trial` — temporary, converts to regular or leaves

---

## Branches

### The `branches` Table

```
id, uuid, academy_id, name, name_ar, code, 
address, city, governorate, phone, email,
latitude, longitude, is_main, is_active,
manager_id (FK users), operating_hours (jsonb),
created_at, updated_at, deleted_at
```

### Branch Rules

1. ONE branch per academy must be `is_main = true` — enforced by partial unique index
2. `code` is unique per academy (e.g., 'HQ', 'MAADI', 'NASR')
3. Branch cannot be deleted if it has active participants, groups, or open cash sessions
4. `manager_id` references a user within the SAME academy (enforced by app logic + trigger)
5. `operating_hours` JSONB structure:
```json
{
    "0": {"open": "08:00", "close": "22:00"},
    "1": {"open": "08:00", "close": "22:00"},
    "5": null,
    "6": {"open": "10:00", "close": "18:00"}
}
```
null = closed that day. Keys are day_of_week (0=Sunday).

---

## Guardians

### The `guardians` Table

```
id, uuid, academy_id, person_id (FK people), user_id (FK users, nullable),
relationship_type, occupation, workplace,
can_pickup, is_emergency_contact, is_financial_responsible,
notes, created_at, updated_at
```

### Guardian ↔ Participant Pivot: `guardian_participant`

```
id, guardian_id, participant_id, relationship,
is_primary, can_pickup, financial_responsibility_percent,
created_at
```

### Guardian Rules

1. A guardian is ALSO a person (has `person_id` in `people` table)
2. A guardian MAY have a user account (for parent portal login)
3. `is_primary = true` — exactly ONE per participant (enforce in service)
4. `financial_responsibility_percent` — for split billing between divorced parents (total must = 100 per participant)
5. One guardian can be linked to MULTIPLE participants (siblings)
6. When guardian pays → payment tracks which participant it's for
7. `relationship` on pivot: `'father', 'mother', 'grandfather', 'grandmother', 'uncle', 'aunt', 'sibling', 'legal_guardian', 'other'`
8. Guardian deletion: RESTRICT if they have active financial responsibility. Soft-delete only.
