# Modèle de données — EFEKTIVACADEMIE (back)

> **DOCUMENT GÉNÉRÉ — NE PAS ÉDITER À LA MAIN.** Toute correction apportée ici
> disparaît à la régénération suivante. Ce qui est faux dans ce document est
> faux dans le code ou dans le générateur : c'est là qu'il faut le corriger.
>
> **Régénérer :**
>
> ```bash
> php artisan modele:documenter
> ```
>
> **Vérifier qu'il est à jour, sans l'écrire** (sortie 1 s'il a dérivé) :
>
> ```bash
> php artisan modele:documenter --verifier
> ```
>
> Source de cette édition : schéma `efektiv_local` (pilote `mysql`) + modèles Eloquent de `app/Models` + ressources JSON de `app/Http/Resources`.

## Comment lire ce document

Trois plans, à ne surtout pas confondre — leur confusion a produit deux
incidents en une semaine (BACK#558, BACK#567) :

| Plan | Ce que c'est | Où le lire ici |
|---|---|---|
| **La base** | Les colonnes réellement présentes dans le schéma. Seule vérité de stockage. | Tableau « Colonnes » de chaque table |
| **Les modèles** | Les liens que le code sait suivre (`$quiz->questions`). Un lien peut exister sans clé étrangère, et une clé étrangère peut n'avoir aucune relation déclarée. | Tableau « Relations déclarées » |
| **L'API** | Les clés servies dans la charge utile JSON. Une clé exposée ne prouve **pas** qu'une colonne existe derrière. | Section « Base ↔ API » |

**Ce que le générateur ne voit pas**, et qu'il vaut mieux savoir que deviner :

- les charges utiles construites à la main dans un contrôleur (hors ressource JSON) ;
- les relations construites dynamiquement, ou déclarées dans une méthode qui exige un argument ;
- les clés issues d'une fusion (`$this->merge([…])`) ou d'un épandage : la ressource est alors marquée **analyse partielle** ;
- **l'intention** : pourquoi une table existe, ce qu'une valeur veut dire. Cela ne s'introspecte pas — voir l'annexe rédigée à la main, en fin de document.

## Domaines

1. **Cours : structure et contenu** — 12 table(s)
2. **Parcours** — 2 table(s)
3. **Inscription, progression et résultats** — 6 table(s)
4. **Traces d'usage** — 4 table(s)
5. **Utilisateurs, rôles et profils** — 18 table(s)
6. **Gamification** — 4 table(s)
7. **Social, messagerie et notifications** — 7 table(s)
8. **Abonnements et paramétrage** — 4 table(s)
9. **Infrastructure Laravel** — 9 table(s)
10. **Tables non classées** — 1 table(s)

Puis : **Base ↔ API** (écarts et clés exposées), **Correspondances** (tables sans modèle, modèles sans table), **Annexe rédigée à la main**.

## 1. Cours : structure et contenu

> Le référentiel pédagogique : un cours porte des sections, une section porte des leçons ET des quiz, un quiz porte des questions, une question porte des options.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    categories ||--o{ subcategories : "category_id"
    course_types ||--o{ courses : "course_type_id"
    courses ||--o{ course_files : "course_id"
    courses ||--o{ course_tag : "course_id"
    courses ||--o{ lessons : "course_id"
    courses ||--o{ sections : "course_id"
    lessons ||--o{ course_files : "lesson_id"
    questions ||--o{ options : "question_id"
    quizzes ||--o{ questions : "quiz_id"
    sections ||--o{ lessons : "section_id"
    sections ||--o{ quizzes : "section_id"
    subcategories ||--o{ courses : "subcategory_id"
    support_types ||--o{ lessons : "support_type_id"
    tags ||--o{ course_tag : "tag_id"
    users ||--o{ courses : "manager_id"
    users ||--o{ courses : "teacher_id"
```

### `courses` — modèle `App\Models\Course`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `subcategory_id` | `bigint unsigned` | non | — | → `subcategories.id` |
| `name` | `varchar(125)` | non | — |  |
| `slug` | `varchar(125)` | oui | — |  |
| `status` | `enum('0','1')` | non | `0` |  |
| `fees` | `decimal(10,2)` | oui | — |  |
| `description` | `text` | oui | — |  |
| `course_type_id` | `bigint unsigned` | oui | — | → `course_types.id` |
| `teacher_id` | `bigint unsigned` | oui | — | → `users.id` |
| `manager_id` | `bigint unsigned` | oui | — | → `users.id` |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `image` | `varchar(125)` | oui | — |  |
| `duration_hours` | `tinyint unsigned` | non | `0` |  |
| `duration_minutes` | `tinyint unsigned` | non | `0` |  |
| `pedagogical_duration_seconds` | `int unsigned` | oui | — |  |
| `resource_file` | `varchar(125)` | oui | — |  |
| `order_no` | `int` | oui | `0` |  |

**Unicité** : `slug`

**Pointe vers**

- `course_type_id` → `course_types.id` *(à la suppression : set null)*
- `manager_id` → `users.id` *(à la suppression : set null)*
- `subcategory_id` → `subcategories.id` *(à la suppression : restrict)*
- `teacher_id` → `users.id` *(à la suppression : set null)*

**Référencée par**

- `completed_courses.course_id` *(à la suppression : no action)*
- `course_files.course_id` *(à la suppression : cascade)*
- `course_tag.course_id` *(à la suppression : cascade)*
- `courses_bookmark.course_id` *(à la suppression : cascade)*
- `enrollments.course_id` *(à la suppression : cascade)*
- `learning_path_courses.course_id` *(à la suppression : cascade)*
- `lessons.course_id` *(à la suppression : cascade)*
- `sections.course_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Course`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->bookmarks` | HasMany | `App\Models\CourseBookmark` | `courses_bookmark` | `course_id ↔ id` |
| `->completedCourses` | HasMany | `App\Models\CompletedCourse` | `completed_courses` | `course_id ↔ id` |
| `->courseType` | BelongsTo | `App\Models\CourseType` | `course_types` | `course_type_id ↔ id` |
| `->course_tags` | HasMany | `App\Models\CourseTag` | `course_tag` | `course_id ↔ id` |
| `->enrollments` | HasMany | `App\Models\Enrollment` | `enrollments` | `course_id ↔ id` |
| `->learners` | HasMany | `App\Models\Enrollment` | `enrollments` | `course_id ↔ id` |
| `->learningPaths` | BelongsToMany | `App\Models\LearningPath` | `learning_paths` | `learning_path_courses.course_id ↔ learning_path_courses.learning_path_id` |
| `->lessons` | HasManyThrough | `App\Models\Lesson` | `lessons` | `section_id ↔ id` |
| `->manager` | BelongsTo | `App\Models\User` | `users` | `manager_id ↔ id` |
| `->quizzes` | HasManyThrough | `App\Models\Quiz` | `quizzes` | `section_id ↔ id` |
| `->sections` | HasMany | `App\Models\Section` | `sections` | `course_id ↔ id` |
| `->subcategory` | HasOne | `App\Models\Subcategory` | `subcategories` | `id ↔ subcategory_id` |
| `->tags` | BelongsToMany | `App\Models\Tag` | `tags` | `course_tag.course_id ↔ course_tag.tag_id` |
| `->teacher` | BelongsTo | `App\Models\User` | `users` | `teacher_id ↔ id` |

- **Attributs ajoutés à la charge utile** (`$appends`, absents de la base) : `is_complete`
- **Accesseurs** (calculés, servis seulement s'ils sont demandés) : `category`, `duration_seconds`
- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\CourseResource`, `App\Http\Resources\PublicCourseResource` — clés détaillées à la section « Base ↔ API ».

### `sections` — modèle `App\Models\Section`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `name` | `varchar(125)` | non | — |  |
| `description` | `text` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `start_date` | `date` | oui | — |  |
| `frequency` | `enum('week','day')` | oui | — |  |
| `start_after_enroll` | `int` | oui | — |  |
| `order_no` | `int` | non | `0` |  |
| `status` | `enum('0','1')` | non | `0` |  |

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : cascade)*

**Référencée par**

- `lessons.section_id` *(à la suppression : cascade)*
- `quizzes.section_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Section`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->lessons` | HasMany | `App\Models\Lesson` | `lessons` | `section_id ↔ id` |
| `->quizzes` | HasMany | `App\Models\Quiz` | `quizzes` | `section_id ↔ id` |

- **Attributs ajoutés à la charge utile** (`$appends`, absents de la base) : `duration_seconds`, `is_lock`
- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\SectionResource` — clés détaillées à la section « Base ↔ API ».

### `lessons` — modèle `App\Models\Lesson`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `slug` | `varchar(125)` | oui | — |  |
| `status` | `enum('0','1')` | non | `0` |  |
| `section_id` | `bigint unsigned` | non | — | → `sections.id` |
| `support_type_id` | `bigint unsigned` | oui | — | → `support_types.id` |
| `pedagogical_role` | `varchar(32)` | oui | — |  |
| `duration_seconds` | `int unsigned` | oui | — |  |
| `duration_source` | `varchar(10)` | oui | — |  |
| `pedagogical_duration_seconds` | `int unsigned` | oui | — |  |
| `course_id` | `bigint unsigned` | oui | — | → `courses.id` |
| `video_url` | `varchar(125)` | oui | — |  |
| `file_url` | `varchar(125)` | oui | — |  |
| `image_url` | `varchar(125)` | oui | — |  |
| `description` | `text` | oui | — |  |
| `youtube_url` | `varchar(125)` | oui | — |  |
| `wistia_url` | `varchar(125)` | oui | — |  |
| `vimeo_url` | `varchar(125)` | oui | — |  |
| `loom_url` | `varchar(125)` | oui | — |  |
| `is_required` | `enum('yes','no')` | non | `no` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `order_no` | `int` | non | `0` |  |

**Unicité** : `slug`

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : cascade)*
- `section_id` → `sections.id` *(à la suppression : cascade)*
- `support_type_id` → `support_types.id` *(à la suppression : set null)*

**Référencée par**

- `course_files.lesson_id` *(à la suppression : set null)*
- `learner_lessons.lesson_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Lesson`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->courseDocuments` | HasMany | `App\Models\CourseResource` | `course_files` | `lesson_id ↔ id` |
| `->learnerLessons` | HasMany | `App\Models\LearnerLesson` | `learner_lessons` | `lesson_id ↔ id` |
| `->section` | BelongsTo | `App\Models\Section` | `sections` | `section_id ↔ id` |
| `->supportType` | BelongsTo | `App\Models\SupportType` | `support_types` | `support_type_id ↔ id` |

- **Attributs ajoutés à la charge utile** (`$appends`, absents de la base) : `is_complete`, `support_type`
- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\LessonResource` — clés détaillées à la section « Base ↔ API ».

### `quizzes` — modèle `App\Models\Quiz`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `slug` | `varchar(125)` | oui | — |  |
| `description` | `text` | oui | — |  |
| `section_id` | `bigint unsigned` | non | — | → `sections.id` |
| `duration_seconds` | `int unsigned` | oui | — |  |
| `duration_source` | `varchar(10)` | oui | — |  |
| `status` | `enum('1','0')` | non | `0` |  |
| `is_required` | `enum('yes','no')` | non | `no` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `passing_percentage` | `int` | non | `80` |  |
| `type` | `varchar(32)` | oui | — |  |
| `show_answer_comments` | `tinyint(1)` | non | `0` |  |
| `is_standalone_enabled` | `tinyint(1)` | non | `0` |  |
| `standalone_notified_at` | `timestamp` | oui | — |  |
| `order_no` | `int` | non | `0` |  |

**Unicité** : `slug`

**Pointe vers**

- `section_id` → `sections.id` *(à la suppression : cascade)*

**Référencée par**

- `learner_lessons.quiz_id` *(à la suppression : cascade)*
- `questions.quiz_id` *(à la suppression : cascade)*
- `quiz_scores.quiz_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Quiz`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->learnerLessons` | HasMany | `App\Models\LearnerLesson` | `learner_lessons` | `quiz_id ↔ id` |
| `->questions` | HasMany | `App\Models\Question` | `questions` | `quiz_id ↔ id` |
| `->section` | BelongsTo | `App\Models\Section` | `sections` | `section_id ↔ id` |

- **Attributs ajoutés à la charge utile** (`$appends`, absents de la base) : `effective_duration_seconds`, `is_complete`
- **Conversions** (`$casts`) : `id → int`, `is_standalone_enabled → boolean`, `show_answer_comments → boolean`, `standalone_notified_at → datetime`

**Exposée par l'API via** : `App\Http\Resources\QuizResource` — clés détaillées à la section « Base ↔ API ».

### `questions` — modèle `App\Models\Question`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `question` | `text` | non | — |  |
| `image` | `varchar(125)` | oui | — |  |
| `quiz_id` | `bigint unsigned` | non | — | → `quizzes.id` |
| `type` | `enum('mcq_single','mcq_multiple')` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `order_no` | `int` | oui | — |  |

**Pointe vers**

- `quiz_id` → `quizzes.id` *(à la suppression : cascade)*

**Référencée par**

- `options.question_id` *(à la suppression : cascade)*
- `quiz_scores.question_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Question`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->options` | HasMany | `App\Models\Option` | `options` | `question_id ↔ id` |
| `->quiz` | BelongsTo | `App\Models\Quiz` | `quizzes` | `quiz_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\QuestionResource` — clés détaillées à la section « Base ↔ API ».

### `options` — modèle `App\Models\Option`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `question_id` | `bigint unsigned` | non | — | → `questions.id` |
| `option_text` | `text` | non | — |  |
| `is_correct` | `tinyint(1)` | non | `0` |  |
| `answer_comment` | `text` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `question_id` → `questions.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Option`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->question` | BelongsTo | `App\Models\Question` | `questions` | `question_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\Learner\HintResource`, `App\Http\Resources\OptionResource` — clés détaillées à la section « Base ↔ API ».

### `course_files` — modèle `App\Models\CourseResource`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `lesson_id` | `bigint unsigned` | oui | — | → `lessons.id` |
| `document_type` | `varchar(20)` | non | `autre` |  |
| `original_name` | `varchar(125)` | non | — |  |
| `stored_path` | `varchar(125)` | non | — |  |
| `size_kb` | `int` | non | — |  |
| `mime_type` | `varchar(125)` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : cascade)*
- `lesson_id` → `lessons.id` *(à la suppression : set null)*

**Relations déclarées** (`App\Models\CourseResource`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->lesson` | BelongsTo | `App\Models\Lesson` | `lessons` | `lesson_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `course_tag` — modèle `App\Models\CourseTag`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `tag_id` | `bigint unsigned` | non | — | → `tags.id` |

**Unicité** : `course_id` + `tag_id`

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : cascade)*
- `tag_id` → `tags.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\CourseTag`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->tag` | BelongsTo | `App\Models\Tag` | `tags` | `tag_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `tags` — modèle `App\Models\Tag`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `name`

**Référencée par**

- `course_tag.tag_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Tag`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->courses` | BelongsToMany | `App\Models\Course` | `courses` | `course_tag.tag_id ↔ course_tag.course_id` |

- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\TagResource` — clés détaillées à la section « Base ↔ API ».

### `course_types` — modèle `App\Models\CourseType`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `status` | `enum('0','1')` | non | `1` |  |
| `fees` | `decimal(10,2)` | oui | — |  |
| `description` | `text` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Référencée par**

- `courses.course_type_id` *(à la suppression : set null)*

**Relations déclarées** (`App\Models\CourseType`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `categories` — modèle `App\Models\Category`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `slug` | `varchar(125)` | oui | — |  |
| `status` | `tinyint(1)` | non | `1` |  |
| `order_no` | `int` | non | `0` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `slug`

**Référencée par**

- `subcategories.category_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Category`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->subcategories` | HasMany | `App\Models\Subcategory` | `subcategories` | `category_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `subcategories` — modèle `App\Models\Subcategory`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `slug` | `varchar(125)` | oui | — |  |
| `status` | `tinyint(1)` | non | `1` |  |
| `category_id` | `bigint unsigned` | non | — | → `categories.id` |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `slug`

**Pointe vers**

- `category_id` → `categories.id` *(à la suppression : cascade)*

**Référencée par**

- `courses.subcategory_id` *(à la suppression : restrict)*
- `learning_paths.subcategory_id` *(à la suppression : set null)*

**Relations déclarées** (`App\Models\Subcategory`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->category` | BelongsTo | `App\Models\Category` | `categories` | `category_id ↔ id` |
| `->courses` | HasMany | `App\Models\Course` | `courses` | `subcategory_id ↔ id` |
| `->learningPaths` | HasMany | `App\Models\LearningPath` | `learning_paths` | `subcategory_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

## 2. Parcours

> Un parcours agrège des cours existants, par référence.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    courses ||--o{ learning_path_courses : "course_id"
    learning_paths ||--o{ learning_path_courses : "learning_path_id"
    subcategories ||--o{ learning_paths : "subcategory_id"
```

### `learning_paths` — modèle `App\Models\LearningPath`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `title` | `varchar(125)` | non | — |  |
| `slug` | `varchar(125)` | oui | — |  |
| `course_type_id` | `bigint unsigned` | oui | — |  |
| `description` | `text` | oui | — |  |
| `thumbnail` | `varchar(125)` | oui | — |  |
| `subcategory_id` | `bigint unsigned` | oui | — | → `subcategories.id` |
| `status` | `enum('draft','published')` | non | `published` |  |
| `locked_step_visibility` | `varchar(20)` | non | `visible_locked` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `slug`

**Pointe vers**

- `subcategory_id` → `subcategories.id` *(à la suppression : set null)*

**Référencée par**

- `learning_path_courses.learning_path_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\LearningPath`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->courses` | BelongsToMany | `App\Models\Course` | `courses` | `learning_path_courses.learning_path_id ↔ learning_path_courses.course_id` |
| `->steps` | HasMany | `App\Models\LearningPathCourse` | `learning_path_courses` | `learning_path_id ↔ id` |
| `->subcategory` | BelongsTo | `App\Models\Subcategory` | `subcategories` | `subcategory_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\LearningPathResource` — clés détaillées à la section « Base ↔ API ».

### `learning_path_courses` — modèle `App\Models\LearningPathCourse`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `learning_path_id` | `bigint unsigned` | non | — | → `learning_paths.id` |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `order` | `int` | non | `1` |  |
| `trigger_mode` | `varchar(20)` | non | `enrollment` |  |
| `trigger_offset_days` | `smallint unsigned` | oui | — |  |
| `trigger_date` | `date` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : cascade)*
- `learning_path_id` → `learning_paths.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\LearningPathCourse`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->learningPath` | BelongsTo | `App\Models\LearningPath` | `learning_paths` | `learning_path_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `order → integer`, `trigger_date → date`, `trigger_offset_days → integer`

## 3. Inscription, progression et résultats

> Le lien entre un apprenant et un contenu : qui est inscrit, où il en est, ce qu'il a réussi.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    courses ||--o{ completed_courses : "course_id"
    courses ||--o{ courses_bookmark : "course_id"
    courses ||--o{ enrollments : "course_id"
    lessons ||--o{ learner_lessons : "lesson_id"
    questions ||--o{ quiz_scores : "question_id"
    quizzes ||--o{ learner_lessons : "quiz_id"
    quizzes ||--o{ quiz_scores : "quiz_id"
    users ||--o{ completed_courses : "learner_id"
    users ||--o{ courses_bookmark : "user_id"
    users ||--o{ enrollments : "assigned_by"
    users ||--o{ enrollments : "decided_by"
    users ||--o{ enrollments : "learner_id"
    users ||--o{ learner_lessons : "learner_id"
    users ||--o{ media_progress : "user_id"
    users ||--o{ quiz_scores : "learner_id"
```

### `enrollments` — modèle `App\Models\Enrollment`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `learner_id` | `bigint unsigned` | non | — | → `users.id` |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `status` | `enum('Approved','Pending','Rejected')` | non | `Pending` |  |
| `assigned_by` | `bigint unsigned` | oui | — | → `users.id` |
| `assigned_at` | `timestamp` | oui | — |  |
| `decided_by` | `bigint unsigned` | oui | — | → `users.id` |
| `decided_at` | `timestamp` | oui | — |  |
| `is_complete` | `tinyint(1)` | non | `0` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `assigned_by` → `users.id` *(à la suppression : set null)*
- `course_id` → `courses.id` *(à la suppression : cascade)*
- `decided_by` → `users.id` *(à la suppression : set null)*
- `learner_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Enrollment`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->assignedBy` | BelongsTo | `App\Models\User` | `users` | `assigned_by ↔ id` |
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->decidedBy` | BelongsTo | `App\Models\User` | `users` | `decided_by ↔ id` |
| `->learner` | BelongsTo | `App\Models\User` | `users` | `learner_id ↔ id` |

- **Conversions** (`$casts`) : `assigned_at → datetime`, `decided_at → datetime`, `id → int`

### `learner_lessons` — modèle `App\Models\LearnerLesson`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `learner_id` | `bigint unsigned` | oui | — | → `users.id` |
| `lesson_id` | `bigint unsigned` | oui | — | → `lessons.id` |
| `quiz_id` | `bigint unsigned` | oui | — | → `quizzes.id` |
| `is_complete` | `enum('1','0')` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `learner_id` → `users.id` *(à la suppression : cascade)*
- `lesson_id` → `lessons.id` *(à la suppression : cascade)*
- `quiz_id` → `quizzes.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\LearnerLesson`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->lesson` | BelongsTo | `App\Models\Lesson` | `lessons` | `lesson_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `learner_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `completed_courses` — modèle `App\Models\CompletedCourse`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `learner_id` | `bigint unsigned` | non | — | → `users.id` |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `completed_at` | `timestamp` | non | — |  |
| `certificate_path` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `learner_id` + `course_id`

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : no action)*
- `learner_id` → `users.id` *(à la suppression : no action)*

**Relations déclarées** (`App\Models\CompletedCourse`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `learner_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

**Exposée par l'API via** : `App\Http\Resources\Learner\CertificateResource` — clés détaillées à la section « Base ↔ API ».

### `quiz_scores` — modèle `App\Models\QuizScore`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `learner_id` | `bigint unsigned` | non | — | → `users.id` |
| `quiz_id` | `bigint unsigned` | non | — | → `quizzes.id` |
| `question_id` | `bigint unsigned` | non | — | → `questions.id` |
| `is_correct` | `tinyint(1)` | non | — |  |
| `attempted_option_ids` | `json` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `learner_id` → `users.id` *(à la suppression : cascade)*
- `question_id` → `questions.id` *(à la suppression : cascade)*
- `quiz_id` → `quizzes.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\QuizScore`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->learner` | BelongsTo | `App\Models\User` | `users` | `learner_id ↔ id` |

- **Conversions** (`$casts`) : `attempted_option_ids → array`, `id → int`, `is_correct → boolean`

### `media_progress` — modèle `App\Models\MediaProgress`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `lesson_id` | `bigint unsigned` | non | — |  |
| `position_seconds` | `int unsigned` | non | `0` |  |
| `total_seconds` | `int unsigned` | oui | — |  |
| `last_activity_at` | `timestamp` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `user_id` + `lesson_id`

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\MediaProgress`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->lesson` | BelongsTo | `App\Models\Lesson` | `lessons` | `lesson_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `last_activity_at → datetime`, `position_seconds → integer`, `total_seconds → integer`

### `courses_bookmark` — modèle `App\Models\CourseBookmark`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `course_id` | `bigint unsigned` | non | — | → `courses.id` |
| `created_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |

**Pointe vers**

- `course_id` → `courses.id` *(à la suppression : cascade)*
- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\CourseBookmark`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

## 4. Traces d'usage

> Événements nominatifs soumis à durée de conservation (voir la commande `tracking:purge`) — à ne pas confondre avec les données métier de progression.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    users ||--o{ lesson_views : "user_id"
    users ||--o{ login_events : "user_id"
    users ||--o{ quiz_attempts : "user_id"
    users ||--o{ resource_downloads : "user_id"
```

### `login_events` — modèle `App\Models\LoginEvent`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `logged_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |
| `ip_truncated` | `varchar(45)` | oui | — |  |
| `user_agent` | `varchar(512)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\LoginEvent`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `logged_at → datetime`

### `lesson_views` — modèle `App\Models\LessonView`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `lesson_id` | `bigint unsigned` | non | — |  |
| `course_id` | `bigint unsigned` | oui | — |  |
| `viewed_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |
| `duration_seconds` | `int unsigned` | oui | — |  |
| `source` | `varchar(20)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\LessonView`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->lesson` | BelongsTo | `App\Models\Lesson` | `lessons` | `lesson_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `duration_seconds → integer`, `id → int`, `viewed_at → datetime`

### `quiz_attempts` — modèle `App\Models\QuizAttempt`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `quiz_id` | `bigint unsigned` | non | — |  |
| `attempt_no` | `int unsigned` | non | — |  |
| `score_percent` | `decimal(5,2)` | non | — |  |
| `correct_count` | `int unsigned` | non | `0` |  |
| `question_count` | `int unsigned` | non | `0` |  |
| `submitted_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\QuizAttempt`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->quiz` | BelongsTo | `App\Models\Quiz` | `quizzes` | `quiz_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `attempt_no → integer`, `correct_count → integer`, `id → int`, `question_count → integer`, `score_percent → float`, `submitted_at → datetime`

### `resource_downloads` — modèle `App\Models\ResourceDownload`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `course_file_id` | `bigint unsigned` | oui | — |  |
| `course_id` | `bigint unsigned` | oui | — |  |
| `kind` | `varchar(20)` | non | `resource` |  |
| `downloaded_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\ResourceDownload`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `downloaded_at → datetime`, `id → int`

## 5. Utilisateurs, rôles et profils

> Un compte unique (`users`) et un profil par rôle. Les rôles et permissions vivent dans les tables de spatie/laravel-permission.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    companies ||--o{ users : "company_id"
    job_profiles ||--o{ users : "job_profile_id"
    permissions ||--o{ model_has_permissions : "permission_id"
    permissions ||--o{ role_has_permissions : "permission_id"
    roles ||--o{ model_has_roles : "role_id"
    roles ||--o{ role_has_permissions : "role_id"
    users ||--o{ admin_profiles : "user_id"
    users ||--o{ director_profiles : "user_id"
    users ||--o{ invitation_user : "sent_by"
    users ||--o{ learner_profiles : "user_id"
    users ||--o{ manager_general_profiles : "user_id"
    users ||--o{ manager_profiles : "user_id"
    users ||--o{ otp_codes : "user_id"
    users ||--o{ submanager_profiles : "user_id"
    users ||--o{ teacher_profiles : "user_id"
    users ||--o{ user_preferences : "user_id"
    users ||--o{ users : "parent_id"
```

### `users` — modèle `App\Models\User`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `first_name` | `varchar(125)` | non | — |  |
| `last_name` | `varchar(125)` | non | — |  |
| `bio` | `text` | oui | — |  |
| `avatar` | `varchar(125)` | oui | — |  |
| `email` | `varchar(125)` | non | — |  |
| `email_verified_at` | `timestamp` | oui | — |  |
| `password` | `varchar(125)` | non | — |  |
| `is_active` | `tinyint(1)` | non | `1` |  |
| `cta_url` | `varchar(125)` | oui | — |  |
| `parent_id` | `bigint unsigned` | oui | — | → `users.id` |
| `company_id` | `bigint unsigned` | oui | — | → `companies.id` |
| `job_profile_id` | `bigint unsigned` | oui | — | → `job_profiles.id` |
| `late_start_days` | `smallint unsigned` | oui | — |  |
| `abandoned_after_days` | `smallint unsigned` | oui | — |  |
| `excluded_from_stats` | `tinyint(1)` | non | `0` |  |
| `password_changed_at` | `timestamp` | oui | — |  |
| `last_login_at` | `timestamp` | oui | — |  |
| `remember_token` | `varchar(100)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `stripe_customer_id` | `varchar(125)` | oui | — |  |

**Unicité** : `email`

**Pointe vers**

- `company_id` → `companies.id` *(à la suppression : restrict)*
- `job_profile_id` → `job_profiles.id` *(à la suppression : restrict)*
- `parent_id` → `users.id` *(à la suppression : set null)*

**Référencée par**

- `admin_profiles.user_id` *(à la suppression : cascade)*
- `chat_group_members.user_id` *(à la suppression : cascade)*
- `chat_groups.created_by` *(à la suppression : cascade)*
- `completed_courses.learner_id` *(à la suppression : no action)*
- `courses.manager_id` *(à la suppression : set null)*
- `courses.teacher_id` *(à la suppression : set null)*
- `courses_bookmark.user_id` *(à la suppression : cascade)*
- `director_profiles.user_id` *(à la suppression : cascade)*
- `enrollments.assigned_by` *(à la suppression : set null)*
- `enrollments.decided_by` *(à la suppression : set null)*
- `enrollments.learner_id` *(à la suppression : cascade)*
- `invitation_user.sent_by` *(à la suppression : set null)*
- `learner_lessons.learner_id` *(à la suppression : cascade)*
- `learner_profiles.user_id` *(à la suppression : cascade)*
- `lesson_views.user_id` *(à la suppression : cascade)*
- `login_events.user_id` *(à la suppression : cascade)*
- `manager_general_profiles.user_id` *(à la suppression : cascade)*
- `manager_profiles.user_id` *(à la suppression : cascade)*
- `media_progress.user_id` *(à la suppression : cascade)*
- `message_follows.user_id` *(à la suppression : cascade)*
- `message_likes.user_id` *(à la suppression : cascade)*
- `messages.receiver_id` *(à la suppression : cascade)*
- `messages.sender_id` *(à la suppression : cascade)*
- `notifications.auth_id` *(à la suppression : set null)*
- `notifications.user_id` *(à la suppression : cascade)*
- `otp_codes.user_id` *(à la suppression : cascade)*
- `quiz_attempts.user_id` *(à la suppression : cascade)*
- `quiz_scores.learner_id` *(à la suppression : cascade)*
- `resource_downloads.user_id` *(à la suppression : cascade)*
- `submanager_profiles.user_id` *(à la suppression : cascade)*
- `subscriptions.user_id` *(à la suppression : cascade)*
- `teacher_profiles.user_id` *(à la suppression : cascade)*
- `user_preferences.user_id` *(à la suppression : cascade)*
- `users.parent_id` *(à la suppression : set null)*

**Relations déclarées** (`App\Models\User`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->CompletedCourse` | HasMany | `App\Models\CompletedCourse` | `completed_courses` | `learner_id ↔ id` |
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |
| `->adminProfile` | HasOne | `App\Models\AdminProfile` | `admin_profiles` | `user_id ↔ id` |
| `->badges` | HasMany | `App\Models\UserBadge` | `user_badges` | `user_id ↔ id` |
| `->company` | BelongsTo | `App\Models\Company` | `companies` | `company_id ↔ id` |
| `->createdGroups` | HasMany | `App\Models\ChatGroup` | `chat_groups` | `created_by ↔ id` |
| `->directorProfile` | HasOne | `App\Models\DirectorProfile` | `director_profiles` | `user_id ↔ id` |
| `->enrollment` | HasMany | `App\Models\Enrollment` | `enrollments` | `learner_id ↔ id` |
| `->followers` | BelongsToMany | `App\Models\User` | `users` | `user_follows.following_id ↔ user_follows.follower_id` |
| `->following` | BelongsToMany | `App\Models\User` | `users` | `user_follows.follower_id ↔ user_follows.following_id` |
| `->jobProfile` | BelongsTo | `App\Models\JobProfile` | `job_profiles` | `job_profile_id ↔ id` |
| `->learnerLesson` | HasMany | `App\Models\LearnerLesson` | `learner_lessons` | `learner_id ↔ id` |
| `->learnerProfile` | HasOne | `App\Models\LearnerProfile` | `learner_profiles` | `user_id ↔ id` |
| `->loginEvents` | HasMany | `App\Models\LoginEvent` | `login_events` | `user_id ↔ id` |
| `->manager` | BelongsTo | `App\Models\User` | `users` | `parent_id ↔ id` |
| `->managerGeneralProfile` | HasOne | `App\Models\ManagerGeneralProfile` | `manager_general_profiles` | `user_id ↔ id` |
| `->managerProfile` | HasOne | `App\Models\ManagerProfile` | `manager_profiles` | `user_id ↔ id` |
| `->memberGroups` | BelongsToMany | `App\Models\ChatGroup` | `chat_groups` | `chat_group_user.user_id ↔ chat_group_user.group_id` |
| `->notifications` | MorphMany | `Illuminate\Notifications\DatabaseNotification` | `notifications` | `notifiable_id ↔ id` |
| `->permissions` | MorphToMany | `Spatie\Permission\Models\Permission` | `permissions` | `model_has_permissions.model_id ↔ model_has_permissions.permission_id` |
| `->readNotifications` | MorphMany | `Illuminate\Notifications\DatabaseNotification` | `notifications` | `notifiable_id ↔ id` |
| `->roles` | MorphToMany | `Spatie\Permission\Models\Role` | `roles` | `model_has_roles.model_id ↔ model_has_roles.role_id` |
| `->sub_manager_learners` | HasMany | `App\Models\User` | `users` | `parent_id ↔ id` |
| `->submanagerProfile` | HasOne | `App\Models\SubmanagerProfile` | `submanager_profiles` | `user_id ↔ id` |
| `->teacherProfile` | HasOne | `App\Models\TeacherProfile` | `teacher_profiles` | `user_id ↔ id` |
| `->tokens` | MorphMany | `Laravel\Sanctum\PersonalAccessToken` | `personal_access_tokens` | `tokenable_id ↔ id` |
| `->unreadNotifications` | MorphMany | `Illuminate\Notifications\DatabaseNotification` | `notifications` | `notifiable_id ↔ id` |

- **Attributs ajoutés à la charge utile** (`$appends`, absents de la base) : `is_follow`
- **Accesseurs** (calculés, servis seulement s'ils sont demandés) : `full_name`
- **Conversions** (`$casts`) : `email_verified_at → datetime`, `id → int`, `last_login_at → datetime`, `password → hashed`
- **Jamais sérialisés** (`$hidden`) : `password`, `remember_token`

**Exposée par l'API via** : `App\Http\Resources\Admin\UserDetailResource`, `App\Http\Resources\Admin\UserResource`, `App\Http\Resources\Learner\LearnerProfileResource`, `App\Http\Resources\Manager\ManagerProfileResource`, `App\Http\Resources\Submanager\SubmanagerProfileResource`, `App\Http\Resources\Teacher\MyStudentResource`, `App\Http\Resources\Teacher\TeacherProfileResource` — clés détaillées à la section « Base ↔ API ».

### `companies` — modèle `App\Models\Company`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `is_active` | `tinyint(1)` | non | `1` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `name`

**Référencée par**

- `users.company_id` *(à la suppression : restrict)*

**Relations déclarées** (`App\Models\Company`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->users` | HasMany | `App\Models\User` | `users` | `company_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `is_active → boolean`

### `invitation_user` — modèle `App\Models\InvitationUser`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `email` | `varchar(125)` | non | — |  |
| `course_id` | `bigint unsigned` | non | — |  |
| `enrolled` | `tinyint(1)` | non | `0` |  |
| `link_sent` | `tinyint(1)` | non | `0` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `sent_by` | `bigint unsigned` | oui | — | → `users.id` |
| `token` | `varchar(64)` | oui | — |  |
| `expires_at` | `timestamp` | oui | — |  |
| `accepted_at` | `timestamp` | oui | — |  |

**Unicité** : `token`

**Pointe vers**

- `sent_by` → `users.id` *(à la suppression : set null)*

**Relations déclarées** (`App\Models\InvitationUser`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->sendBy` | BelongsTo | `App\Models\User` | `users` | `sent_by ↔ id` |

- **Conversions** (`$casts`) : `accepted_at → datetime`, `expires_at → datetime`, `id → int`

### `user_preferences` — modèle `App\Models\UserPreference`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `screen` | `varchar(64)` | non | — |  |
| `preferences` | `json` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `user_id` + `screen`

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\UserPreference`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `preferences → array`

### `otp_codes` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `otp` | `varchar(125)` | non | — |  |
| `expires_at` | `timestamp` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

### `admin_profiles` — modèle `App\Models\AdminProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `phone` | `varchar(125)` | oui | — |  |
| `gender` | `varchar(125)` | oui | — |  |
| `dob` | `date` | oui | — |  |
| `country` | `varchar(125)` | oui | — |  |
| `state` | `varchar(125)` | oui | — |  |
| `city` | `varchar(125)` | oui | — |  |
| `address` | `varchar(125)` | oui | — |  |
| `bio` | `text` | oui | — |  |
| `avatar` | `varchar(125)` | oui | — |  |
| `social_links` | `json` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\AdminProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`, `social_links → array`

### `director_profiles` — modèle `App\Models\DirectorProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `avatar` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `bio` | `text` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\DirectorProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `job_profiles` — modèle `App\Models\JobProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `is_active` | `tinyint(1)` | non | `1` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `name`

**Référencée par**

- `users.job_profile_id` *(à la suppression : restrict)*

**Relations déclarées** (`App\Models\JobProfile`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->users` | HasMany | `App\Models\User` | `users` | `job_profile_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `is_active → boolean`

### `learner_profiles` — modèle `App\Models\LearnerProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `phone` | `varchar(125)` | oui | — |  |
| `gender` | `varchar(125)` | oui | — |  |
| `dob` | `date` | oui | — |  |
| `country` | `varchar(125)` | oui | — |  |
| `state` | `varchar(125)` | oui | — |  |
| `city` | `varchar(125)` | oui | — |  |
| `address` | `varchar(125)` | oui | — |  |
| `education_level` | `varchar(125)` | oui | — |  |
| `bio` | `text` | oui | — |  |
| `avatar` | `varchar(125)` | oui | — |  |
| `interests` | `json` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\LearnerProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`, `interests → array`

### `manager_general_profiles` — modèle `App\Models\ManagerGeneralProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `avatar` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `bio` | `text` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\ManagerGeneralProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `manager_profiles` — modèle `App\Models\ManagerProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `phone` | `varchar(125)` | oui | — |  |
| `gender` | `varchar(125)` | oui | — |  |
| `dob` | `date` | oui | — |  |
| `country` | `varchar(125)` | oui | — |  |
| `state` | `varchar(125)` | oui | — |  |
| `city` | `varchar(125)` | oui | — |  |
| `address` | `varchar(125)` | oui | — |  |
| `role_title` | `varchar(125)` | oui | — |  |
| `department` | `varchar(125)` | oui | — |  |
| `years_of_experience` | `int` | oui | — |  |
| `bio` | `text` | oui | — |  |
| `avatar` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\ManagerProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `submanager_profiles` — modèle `App\Models\SubmanagerProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `avatar` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `bio` | `text` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\SubmanagerProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `teacher_profiles` — modèle `App\Models\TeacherProfile`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `phone` | `varchar(125)` | oui | — |  |
| `gender` | `varchar(125)` | oui | — |  |
| `dob` | `date` | oui | — |  |
| `country` | `varchar(125)` | oui | — |  |
| `state` | `varchar(125)` | oui | — |  |
| `city` | `varchar(125)` | oui | — |  |
| `address` | `varchar(125)` | oui | — |  |
| `highest_qualification` | `varchar(125)` | oui | — |  |
| `specialization` | `varchar(125)` | oui | — |  |
| `years_of_experience` | `int` | oui | — |  |
| `bio` | `text` | oui | — |  |
| `avatar` | `varchar(125)` | oui | — |  |
| `subjects` | `json` | oui | — |  |
| `availability` | `json` | oui | — |  |
| `social_links` | `json` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\TeacherProfile`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `availability → array`, `id → int`, `social_links → array`, `specialization → array`, `subjects → array`

### `roles` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `guard_name` | `varchar(125)` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `name` + `guard_name`

**Référencée par**

- `model_has_roles.role_id` *(à la suppression : cascade)*
- `role_has_permissions.role_id` *(à la suppression : cascade)*

### `permissions` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `guard_name` | `varchar(125)` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `name` + `guard_name`

**Référencée par**

- `model_has_permissions.permission_id` *(à la suppression : cascade)*
- `role_has_permissions.permission_id` *(à la suppression : cascade)*

### `model_has_roles` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `role_id` | `bigint unsigned` | non | — | → `roles.id` |
| `model_type` | `varchar(125)` | non | — |  |
| `model_id` | `bigint unsigned` | non | — |  |

**Pointe vers**

- `role_id` → `roles.id` *(à la suppression : cascade)*

### `model_has_permissions` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `permission_id` | `bigint unsigned` | non | — | → `permissions.id` |
| `model_type` | `varchar(125)` | non | — |  |
| `model_id` | `bigint unsigned` | non | — |  |

**Pointe vers**

- `permission_id` → `permissions.id` *(à la suppression : cascade)*

### `role_has_permissions` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `permission_id` | `bigint unsigned` | non | — | → `permissions.id` |
| `role_id` | `bigint unsigned` | non | — | → `roles.id` |

**Pointe vers**

- `permission_id` → `permissions.id` *(à la suppression : cascade)*
- `role_id` → `roles.id` *(à la suppression : cascade)*

## 6. Gamification

> Badges et points attribués aux apprenants.

### `badges` — modèle `App\Models\Badge`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `action_key` | `varchar(125)` | non | — |  |
| `role` | `varchar(125)` | oui | — |  |
| `status` | `enum('active','inactive')` | non | `active` |  |
| `order` | `int` | oui | — |  |
| `image` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Relations déclarées** (`App\Models\Badge`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `user_badges` — modèle `App\Models\UserBadge`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — |  |
| `badge_id` | `bigint unsigned` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `user_id` + `badge_id`

**Relations déclarées** (`App\Models\UserBadge`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->badge` | BelongsTo | `App\Models\Badge` | `badges` | `badge_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `point_master` — modèle `App\Models\PointMaster`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `role` | `enum('admin','learner','manager','teacher','sub_manager')` | oui | — |  |
| `title` | `varchar(125)` | non | — |  |
| `action_key` | `varchar(191)` | non | — |  |
| `x_value` | `int` | non | `0` |  |
| `points` | `int` | non | `0` |  |
| `description` | `text` | oui | — |  |
| `status` | `enum('active','inactive')` | non | `active` |  |
| `order` | `int` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `action_key`

**Relations déclarées** (`App\Models\PointMaster`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `user_points` — modèle `App\Models\UserPoint`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — |  |
| `point_master_id` | `bigint unsigned` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `user_id` + `point_master_id`

**Relations déclarées** (`App\Models\UserPoint`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->pointMaster` | BelongsTo | `App\Models\PointMaster` | `point_master` | `point_master_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

## 7. Social, messagerie et notifications

> Fils de discussion, abonnements entre comptes et notifications.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    chat_groups ||--o{ chat_group_members : "chat_group_id"
    chat_groups ||--o{ messages : "chat_group_id"
    messages ||--o{ message_follows : "message_id"
    messages ||--o{ message_likes : "message_id"
    users ||--o{ chat_group_members : "user_id"
    users ||--o{ chat_groups : "created_by"
    users ||--o{ message_follows : "user_id"
    users ||--o{ message_likes : "user_id"
    users ||--o{ messages : "receiver_id"
    users ||--o{ messages : "sender_id"
    users ||--o{ notifications : "auth_id"
    users ||--o{ notifications : "user_id"
```

### `chat_groups` — modèle `App\Models\ChatGroup`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `created_by` | `bigint unsigned` | non | — | → `users.id` |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `created_by` → `users.id` *(à la suppression : cascade)*

**Référencée par**

- `chat_group_members.chat_group_id` *(à la suppression : cascade)*
- `messages.chat_group_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\ChatGroup`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->creator` | BelongsTo | `App\Models\User` | `users` | `created_by ↔ id` |
| `->members` | HasMany | `App\Models\ChatGroupMember` | `chat_group_members` | `chat_group_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `chat_group_members` — modèle `App\Models\ChatGroupMember`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `chat_group_id` | `bigint unsigned` | non | — | → `chat_groups.id` |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `chat_group_id` → `chat_groups.id` *(à la suppression : cascade)*
- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\ChatGroupMember`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `id → int`

### `messages` — modèle `App\Models\Message`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `sender_id` | `bigint unsigned` | non | — | → `users.id` |
| `receiver_id` | `bigint unsigned` | oui | — | → `users.id` |
| `chat_group_id` | `bigint unsigned` | oui | — | → `chat_groups.id` |
| `parent_message_id` | `bigint` | oui | — |  |
| `course_id` | `bigint` | oui | — |  |
| `message` | `text` | oui | — |  |
| `type` | `enum('chat','video_call')` | non | `chat` |  |
| `participants` | `json` | oui | — |  |
| `duration` | `int` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `chat_group_id` → `chat_groups.id` *(à la suppression : cascade)*
- `receiver_id` → `users.id` *(à la suppression : cascade)*
- `sender_id` → `users.id` *(à la suppression : cascade)*

**Référencée par**

- `message_follows.message_id` *(à la suppression : cascade)*
- `message_likes.message_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Message`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->course` | BelongsTo | `App\Models\Course` | `courses` | `course_id ↔ id` |
| `->follows` | HasMany | `App\Models\MessageFollow` | `message_follows` | `message_id ↔ id` |
| `->likes` | HasMany | `App\Models\MessageLike` | `message_likes` | `message_id ↔ id` |
| `->replies` | HasMany | `App\Models\Message` | `messages` | `parent_message_id ↔ id` |
| `->sender` | BelongsTo | `App\Models\User` | `users` | `sender_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `message_follows` — modèle `App\Models\MessageFollow`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `message_id` | `bigint unsigned` | non | — | → `messages.id` |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `message_id` + `user_id`

**Pointe vers**

- `message_id` → `messages.id` *(à la suppression : cascade)*
- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\MessageFollow`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->message` | BelongsTo | `App\Models\Message` | `messages` | `message_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `message_likes` — modèle `App\Models\MessageLike`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `message_id` | `bigint unsigned` | non | — | → `messages.id` |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `message_id` + `user_id`

**Pointe vers**

- `message_id` → `messages.id` *(à la suppression : cascade)*
- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\MessageLike`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->message` | BelongsTo | `App\Models\Message` | `messages` | `message_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `user_follows` — modèle `App\Models\UserFollow`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `follower_id` | `bigint unsigned` | non | — |  |
| `following_id` | `bigint unsigned` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `follower_id` + `following_id`

**Relations déclarées** (`App\Models\UserFollow`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->follower` | BelongsTo | `App\Models\User` | `users` | `follower_id ↔ id` |
| `->following` | BelongsTo | `App\Models\User` | `users` | `following_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `notifications` — modèle `App\Models\Notification`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `auth_id` | `bigint unsigned` | oui | — | → `users.id` |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `title` | `varchar(125)` | non | — |  |
| `message` | `text` | non | — |  |
| `role` | `varchar(125)` | oui | — |  |
| `type` | `varchar(125)` | non | — |  |
| `status` | `enum('unread','read')` | non | `unread` |  |
| `url` | `varchar(125)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Pointe vers**

- `auth_id` → `users.id` *(à la suppression : set null)*
- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Notification`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

## 8. Abonnements et paramétrage

> Offres, souscriptions et réglages d'application.

### Carte des liens (clés étrangères)

```mermaid
erDiagram
    plans ||--o{ subscriptions : "plan_id"
    users ||--o{ subscriptions : "user_id"
```

### `plans` — modèle `App\Models\Plan`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `frequency` | `enum('monthly','half-yearly','quarterly','yearly')` | non | — |  |
| `validity_days` | `int` | non | — |  |
| `stripe_price_id` | `varchar(125)` | non | — |  |
| `price` | `double` | non | — |  |
| `features` | `json` | non | — |  |
| `can_downgrade` | `tinyint(1)` | non | `1` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Référencée par**

- `subscriptions.plan_id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Plan`)

- *aucune relation Eloquent déclarée sur ce modèle*

- **Conversions** (`$casts`) : `features → array`, `id → int`

### `subscriptions` — modèle `App\Models\Subscription`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `user_id` | `bigint unsigned` | non | — | → `users.id` |
| `plan_id` | `bigint unsigned` | non | — | → `plans.id` |
| `stripe_subscription_id` | `varchar(125)` | non | — |  |
| `stripe_customer_id` | `varchar(125)` | non | — |  |
| `status` | `varchar(125)` | non | `active` |  |
| `valid_until` | `date` | non | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |
| `subscription_item_id` | `varchar(125)` | oui | — |  |

**Unicité** : `stripe_subscription_id`

**Pointe vers**

- `plan_id` → `plans.id` *(à la suppression : cascade)*
- `user_id` → `users.id` *(à la suppression : cascade)*

**Relations déclarées** (`App\Models\Subscription`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->plan` | BelongsTo | `App\Models\Plan` | `plans` | `plan_id ↔ id` |
| `->user` | BelongsTo | `App\Models\User` | `users` | `user_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `settings` — modèle `App\Models\Setting`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `key` | `varchar(191)` | oui | — |  |
| `value` | `text` | oui | — |  |
| `input_type` | `enum('text','textarea','number','file')` | non | `text` |  |
| `created_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |
| `updated_at` | `timestamp` | oui | — |  |

**Relations déclarées** (`App\Models\Setting`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->activities` | MorphMany | `Spatie\Activitylog\Models\Activity` | `activity_log` | `subject_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`

### `support_types` — modèle `App\Models\SupportType`

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `key` | `varchar(125)` | non | — |  |
| `label` | `varchar(125)` | non | — |  |
| `icon` | `varchar(125)` | oui | — |  |
| `render_family` | `varchar(125)` | non | — |  |
| `pedagogical_role` | `varchar(32)` | oui | — |  |
| `is_system` | `tinyint(1)` | non | `0` |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `key`

**Référencée par**

- `lessons.support_type_id` *(à la suppression : set null)*

**Relations déclarées** (`App\Models\SupportType`)

| Appel | Type | Modèle lié | Table liée | Clés |
|---|---|---|---|---|
| `->lessons` | HasMany | `App\Models\Lesson` | `lessons` | `support_type_id ↔ id` |

- **Conversions** (`$casts`) : `id → int`, `is_system → boolean`

## 9. Infrastructure Laravel

> Tables techniques du framework : aucune n'appartient au modèle métier.

### `cache` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `key` | `varchar(125)` | non | — |  |
| `value` | `mediumtext` | non | — |  |
| `expiration` | `int` | non | — |  |

### `cache_locks` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `key` | `varchar(125)` | non | — |  |
| `owner` | `varchar(125)` | non | — |  |
| `expiration` | `int` | non | — |  |

### `failed_jobs` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `uuid` | `varchar(125)` | non | — |  |
| `connection` | `text` | non | — |  |
| `queue` | `text` | non | — |  |
| `payload` | `longtext` | non | — |  |
| `exception` | `longtext` | non | — |  |
| `failed_at` | `timestamp` | non | `CURRENT_TIMESTAMP` |  |

**Unicité** : `uuid`

### `job_batches` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` | `varchar(125)` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `total_jobs` | `int` | non | — |  |
| `pending_jobs` | `int` | non | — |  |
| `failed_jobs` | `int` | non | — |  |
| `failed_job_ids` | `longtext` | non | — |  |
| `options` | `mediumtext` | oui | — |  |
| `cancelled_at` | `int` | oui | — |  |
| `created_at` | `int` | non | — |  |
| `finished_at` | `int` | oui | — |  |

### `jobs` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `queue` | `varchar(125)` | non | — |  |
| `payload` | `longtext` | non | — |  |
| `attempts` | `tinyint unsigned` | non | — |  |
| `reserved_at` | `int unsigned` | oui | — |  |
| `available_at` | `int unsigned` | non | — |  |
| `created_at` | `int unsigned` | non | — |  |

### `migrations` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `int unsigned` | non | — |  |
| `migration` | `varchar(125)` | non | — |  |
| `batch` | `int` | non | — |  |

### `password_reset_tokens` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `email` | `varchar(125)` | non | — |  |
| `token` | `varchar(125)` | non | — |  |
| `purpose` | `varchar(20)` | non | `onboarding` |  |
| `created_at` | `timestamp` | oui | — |  |

### `personal_access_tokens` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `tokenable_type` | `varchar(125)` | non | — |  |
| `tokenable_id` | `bigint unsigned` | non | — |  |
| `name` | `varchar(125)` | non | — |  |
| `token` | `varchar(64)` | non | — |  |
| `abilities` | `text` | oui | — |  |
| `last_used_at` | `timestamp` | oui | — |  |
| `expires_at` | `timestamp` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

**Unicité** : `token`

### `sessions` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` | `varchar(125)` | non | — |  |
| `user_id` | `bigint unsigned` | oui | — |  |
| `ip_address` | `varchar(45)` | oui | — |  |
| `user_agent` | `text` | oui | — |  |
| `payload` | `longtext` | non | — |  |
| `last_activity` | `int` | non | — |  |

## 10. Tables non classées

> Présentes en base et rattachées à aucun domaine : à classer dans `App\Support\ModeleDonnees\Domaines` — ou le signe qu'une table vient d'apparaître.

### `activity_log` — *aucun modèle Eloquent*

| Colonne | Type | Null | Défaut | Lien |
|---|---|---|---|---|
| `id` 🔑 | `bigint unsigned` | non | — |  |
| `log_name` | `varchar(125)` | oui | — |  |
| `description` | `text` | non | — |  |
| `subject_type` | `varchar(125)` | oui | — |  |
| `subject_id` | `bigint unsigned` | oui | — |  |
| `event` | `varchar(125)` | oui | — |  |
| `causer_type` | `varchar(125)` | oui | — |  |
| `causer_id` | `bigint unsigned` | oui | — |  |
| `properties` | `json` | oui | — |  |
| `batch_uuid` | `char(36)` | oui | — |  |
| `created_at` | `timestamp` | oui | — |  |
| `updated_at` | `timestamp` | oui | — |  |

## Base ↔ API — ce qui est stocké contre ce qui est servi

Les ressources JSON de `app/Http/Resources` sont lues **statiquement** : on
relève les clés de premier niveau du tableau rendu par `toArray()`, et d'où
vient chaque valeur. Une clé qui lit `$this->quelquechose` sans que ce nom
corresponde à une colonne, une relation, un accesseur ou un attribut ajouté
sort à `null` — **sans erreur, sans trace**. C'est exactement ce qui s'est
produit avec `quizzes.type` (BACK#558).

Une ressource ne DÉCLARE pas le modèle qu'elle enveloppe, et son nom ment
parfois (`Learner\LearnerProfileResource` sert un `User`). La liaison est donc
établie par **`@mixin`** quand il est écrit, sinon par le **nom** s'il explique
toutes les clés, sinon par les **attributs** — le modèle qui en explique le
plus. La règle retenue est affichée sous chaque ressource : une liaison
contestable se conteste, et un `@mixin` posé dans la ressource clôt le débat.

### Écarts constatés

Aucun écart détecté.

### Clés exposées, ressource par ressource

#### `App\Http\Resources\Admin\UserDetailResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **nom**
- ⚠️ **Analyse partielle** : le tableau rendu contient une fusion, un épandage ou une clé calculée — d'autres clés peuvent sortir en plus de celles listées.

*Aucune clé littérale relevée.*

#### `App\Http\Resources\Admin\UserResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `first_name` | `$this->first_name` |
| `last_name` | `$this->last_name` |
| `email` | `$this->email` |
| `cta_url` | `$this->cta_url` |
| `role` | *calculée* |
| `learner_profile` | `$this->learnerProfile` |
| `teacher_profile` | `$this->teacherProfile` |
| `manager_profile` | `$this->managerProfile` |
| `manager_general_profile` | `$this->managerGeneralProfile` |
| `director_profile` | `$this->directorProfile` |
| `admin_profile` | `$this->adminProfile` |
| `submanager_profile` | `$this->submanagerProfile` |
| `sub_manager_learners` | `$this->sub_manager_learners` |
| `is_active` | `$this->is_active` |
| `company_id` | `$this->company_id` |
| `company` | `$this->company` |
| `job_profile_id` | `$this->job_profile_id` |
| `job_profile` | `$this->jobProfile` |
| `parent_id` | `$this->parent_id` |
| `manager` | `$this->manager` |
| `enrollment` | *calculée* |
| `_links` | *calculée* |

#### `App\Http\Resources\CourseResource`

- Modèle lié : `App\Models\Course` (table `courses`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `slug` | `$this->slug` |
| `name` | `$this->name` |
| `status` | `$this->status` |
| `fees` | `$this->fees` |
| `description` | `$this->description` |
| `is_complete` | `$this->is_complete` |
| `course_type` | *calculée* |
| `course_type_id` | *calculée* |
| `teacher` | `$this->teacher` |
| `section` | *calculée* |
| `category` | `$this->category` |
| `tags` | *calculée* |
| `duration_hours` | `$this->duration_hours` |
| `duration_minutes` | `$this->duration_minutes` |
| `duration_seconds` | *calculée* |
| `raw_duration_seconds` | *calculée* |
| `pedagogical_duration_seconds` | *calculée* |
| `duration_display_source` | *calculée* |
| `duration_coherence` | *calculée* |
| `progress` | `$this->when()` |
| `image` | *calculée* |
| `quiz_count` | *calculée* |
| `lessons_count` | *calculée* |
| `is_bookmark` | `$this->isBookmarkedByUser()` |
| `total_enrollment_count` | *calculée* |
| `is_subscribe` | `$this->is_subscribe()` |
| `is_enrolled` | *calculée* |
| `sub_category` | `$this->subcategory` |
| `no_of_enrolled_learner` | *calculée* |
| `learners` | `$this->learners` |
| `resource_file` | *calculée* |
| `counts` | *calculée* |
| `_links` | *calculée* |

#### `App\Http\Resources\Learner\CertificateResource`

- Modèle lié : `App\Models\CompletedCourse` (table `completed_courses`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `completed_at` | `$this->completed_at` |
| `certificate_path` | `$this->certificate_path` |
| `course` | *calculée* |
| `_links` | *calculée* |

#### `App\Http\Resources\Learner\HintResource`

- Modèle lié : `App\Models\Option` (table `options`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `question_id` | `$this->question_id` |
| `option_text` | `$this->option_text` |
| `is_correct` *(ajoutée sous condition)* | `$this->is_correct` |
| `answer_comment` *(ajoutée sous condition)* | `$this->answer_comment` |

#### `App\Http\Resources\Learner\LearnerProfileResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `first_name` | `$this->first_name` |
| `last_name` | `$this->last_name` |
| `email` | `$this->email` |
| `learner_profile` | `$this->learnerProfile` |
| `_links` | *calculée* |

#### `App\Http\Resources\LearningPathResource`

- Modèle lié : `App\Models\LearningPath` (table `learning_paths`) — liaison établie par **nom**
- ⚠️ **Analyse partielle** : le tableau rendu contient une fusion, un épandage ou une clé calculée — d'autres clés peuvent sortir en plus de celles listées.

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `slug` | `$this->slug` |
| `title` | `$this->title` |
| `description` | `$this->description` |
| `thumbnail` | `$this->thumbnail` |
| `course_type_id` | `$this->course_type_id` |
| `subcategory_id` | `$this->subcategory_id` |
| `status` | `$this->status` |
| `locked_step_visibility` | `$this->locked_step_visibility` |
| `subcategory` | `$this->subcategory` |
| `courses` | *calculée* |

#### `App\Http\Resources\LessonResource`

- Modèle lié : `App\Models\Lesson` (table `lessons`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `slug` | `$this->slug` |
| `name` | `$this->name` |
| `is_complete` | `$this->is_complete` |
| `status` | `$this->status` |
| `section_id` | `$this->section_id` |
| `video_url` | `$this->video_url` |
| `image_url` | `$this->image_url` |
| `description` | `$this->description` |
| `youtube_url` | `$this->youtube_url` |
| `wistia_url` | `$this->wistia_url` |
| `vimeo_url` | `$this->vimeo_url` |
| `loom_url` | `$this->loom_url` |
| `file_url` | `$this->file_url` |
| `is_required` | `$this->is_required` |
| `support_type` | `$this->support_type` |
| `pedagogical_role` | *calculée* |
| `duration_seconds` | `$this->duration_seconds` |
| `duration_source` | `$this->duration_source` |
| `pedagogical_duration_seconds` | `$this->pedagogical_duration_seconds` |
| `documents` | *calculée* |
| `is_completed` | `$this->is_completed()` |
| `previous_lesson_completed` | `$this->previous_lesson_completed()` |
| `_links` | *calculée* |

#### `App\Http\Resources\Manager\ManagerProfileResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `first_name` | `$this->first_name` |
| `last_name` | `$this->last_name` |
| `email` | `$this->email` |
| `manager_profile` | `$this->managerProfile` |
| `_links` | *calculée* |

#### `App\Http\Resources\OptionResource`

- Modèle lié : `App\Models\Option` (table `options`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `question_id` | `$this->question_id` |
| `option_text` | `$this->option_text` |
| `answer_comment` *(ajoutée sous condition)* | `$this->answer_comment` |
| `is_correct` *(ajoutée sous condition)* | `$this->is_correct` |
| `_links` *(ajoutée sous condition)* | *calculée* |

#### `App\Http\Resources\PublicCourseResource`

- Modèle lié : `App\Models\Course` (table `courses`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `slug` | `$this->slug` |
| `name` | `$this->name` |
| `description` | `$this->description` |
| `image` | *calculée* |
| `duration_hours` | `$this->duration_hours` |
| `duration_minutes` | `$this->duration_minutes` |
| `duration_seconds` | *calculée* |
| `raw_duration_seconds` | *calculée* |
| `pedagogical_duration_seconds` | *calculée* |
| `duration_display_source` | *calculée* |
| `course_type` | *calculée* |
| `course_type_id` | *calculée* |
| `sub_category` | `$this->subcategory` |

#### `App\Http\Resources\QuestionResource`

- Modèle lié : `App\Models\Question` (table `questions`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `question` | `$this->question` |
| `type` | `$this->type` |
| `options` | *calculée* |
| `image` | `$this->image` |
| `_links` | *calculée* |

#### `App\Http\Resources\QuizResource`

- Modèle lié : `App\Models\Quiz` (table `quizzes`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `slug` | `$this->slug` |
| `name` | `$this->name` |
| `status` | `$this->status` |
| `description` | `$this->description` |
| `is_complete` | `$this->is_complete` |
| `duration_seconds` | `$this->duration_seconds` |
| `effective_duration_seconds` | `$this->effective_duration_seconds` |
| `questions` | *calculée* |
| `passing_percentage` | `$this->passing_percentage` |
| `quiz_type` | *calculée* |
| `_links` | *calculée* |
| `show_answer_comments` | `$this->show_answer_comments` |
| `is_standalone_enabled` | `$this->is_standalone_enabled` |
| `standalone_notified_at` | `$this->standalone_notified_at` |

#### `App\Http\Resources\SectionResource`

- Modèle lié : `App\Models\Section` (table `sections`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `name` | `$this->name` |
| `status` | `$this->status` |
| `is_lock` | `$this->is_lock` |
| `description` | `$this->description` |
| `frequency` | `$this->frequency` |
| `start_after_enroll` | `$this->start_after_enroll` |
| `start_date` | `$this->start_date` |
| `duration_seconds` | `$this->duration_seconds` |
| `items` | `$this->itemsDuSommaire()` |
| `_links` | *calculée* |

#### `App\Http\Resources\Submanager\SubmanagerProfileResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `first_name` | `$this->first_name` |
| `last_name` | `$this->last_name` |
| `email` | `$this->email` |
| `submanager_profile` | `$this->submanagerProfile` |
| `director_profile` | `$this->directorProfile` |
| `general_manager_profile` | `$this->managerGeneralProfile` |
| `_links` | *calculée* |

#### `App\Http\Resources\TagResource`

- Modèle lié : `App\Models\Tag` (table `tags`) — liaison établie par **nom**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `name` | `$this->name` |
| `_links` | *calculée* |

#### `App\Http\Resources\Teacher\MyStudentResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `id` | `$this->id` |
| `first_name` | `$this->first_name` |
| `last_name` | `$this->last_name` |
| `email` | `$this->email` |
| `is_active` | `$this->is_active` |
| `learner_profile` | `$this->learnerProfile` |
| `sub_manager_learners` | `$this->sub_manager_learners` |
| `_links` | *calculée* |

#### `App\Http\Resources\Teacher\TeacherProfileResource`

- Modèle lié : `App\Models\User` (table `users`) — liaison établie par **attributs**

| Clé servie | Origine |
|---|---|
| `first_name` | `$this->first_name` |
| `last_name` | `$this->last_name` |
| `email` | `$this->email` |
| `teacher_profile` | `$this->teacherProfile` |
| `_links` | *calculée* |

## Correspondances tables ↔ modèles

**Tables sans modèle Eloquent** (16) : 
`activity_log`, `cache`, `cache_locks`, `failed_jobs`, `job_batches`, `jobs`, `migrations`, `model_has_permissions`, `model_has_roles`, `otp_codes`, `password_reset_tokens`, `permissions`, `personal_access_tokens`, `role_has_permissions`, `roles`, `sessions`

**Modèles dont la table est absente de ce schéma** (1) : 
`App\Models\Contenu → `contenus``

## Annexe — RÉDIGÉE À LA MAIN

Tout ce qui précède est **généré**. Ce qui suit ne l'est pas : une intention,
une règle métier, un piège connu ne s'introspectent pas. Cette annexe est le
seul endroit du document où une main humaine écrit, et elle vit dans un
fichier séparé — `docs/architecture/MODELE_DONNEES.annexe.md` — que le
générateur recopie ici tel quel.

<!-- début de l'annexe -->

Ce fichier est **le seul du document rédigé à la main**. Il est recopié tel quel
en fin de `MODELE_DONNEES.md` par `php artisan modele:documenter`. On y met ce
qu'aucune introspection ne peut donner : une intention, une règle, un piège.

**Règle d'entretien** : n'écrire ici que ce qui est SOURCÉ (une issue, un test,
une décision datée). Une affirmation invérifiable dans cette annexe redonne au
document le défaut qu'il combat.

---

### Les valeurs qui ne se lisent pas dans le type

- **`courses.status` — `enum('0','1')`, et `'1'` veut dire PUBLIÉ.** Le défaut de
  la colonne valait `'1'` : un cours créé sans que le code nomme `status`
  naissait donc *publié*. Corrigé par BACK#553 ; le test
  `tests/Feature/Back553CoursNaitBrouillonTest.php` garde l'amont. Le même
  encodage `'0'`/`'1'` se retrouve sur `lessons.status` et `quizzes.status`.
- **`is_required` — `enum('yes','no')`, pas un booléen.** Sur `lessons` comme sur
  `quizzes`. Une comparaison à `true` y est toujours fausse.

### `activity_log` — ce que le schéma ne dit pas (BACK#389)

La table du journal d'audit est **celle de `spatie/laravel-activitylog`, à la
colonne près**. Trois choses ne se lisent pas dans son schéma :

- **`description` ne contient pas une phrase, mais une CLÉ canonique** —
  `compte.desactive`, `droit.role_modifie`, `donnee.export_nominatif`. Le
  registre est `App\Support\Audit\TypeAction` ; le libellé lisible vit dans
  `config/audit.php` et n'est jamais stocké. Une phrase figerait la formulation
  du jour dans les lignes du jour.
- **`log_name` porte le DOMAINE métier**, pas le nom d'une table : `users`
  alimente `comptes` quand une identité change et `droits` quand un rôle change.
- **Il n'y a AUCUNE colonne de sensibilité, et c'est une décision** (#186,
  09/08). La sensibilité est un attribut du *type d'action*, lue à l'affichage.
  Figée à l'écriture, un reclassement ne vaudrait que pour l'avenir et la vue
  « simplifiée » mentirait sur l'historique.
  `tests/Feature/Back389ClassificationLueALAffichageTest.php` reclasse un type
  après coup et vérifie que les entrées déjà écrites basculent avec lui.

Deux règles d'écriture ne se déduisent pas non plus du schéma, et leur oubli
rendrait le journal inutilisable :

- **Une opération en masse produit UNE entrée, pas une par ligne.** `Enrollment`
  ne porte donc pas le trait d'audit : inscrire 260 apprenants à 10 formations
  écrirait 2 600 entrées pour une seule action humaine.
- **Aucune table de tracking n'est dupliquée à l'écriture** (`login_events`,
  `lesson_views`, `quiz_attempts`, `media_progress`, `resource_downloads`,
  `completed_courses`). Les croiser au reporting reste libre.

Les deux interdits sont tenus par
`tests/Feature/Back389TrackingNonDupliqueTest.php`, qui les vérifie **par
introspection des traits** autant que par comptage : il échoue au moment où
quelqu'un pose le trait, pas seulement quand la panne se voit.

⚠️ `config/permission.php` → `events_enabled` est passé à `true` pour ce
chantier. Le rôle ne vit dans aucune colonne de `users` : sans ces événements,
un changement de rôle n'est observable nulle part.

### Les deux incidents qui ont fait naître ce document

- **BACK#558 — le champ fantôme.** `QuizResource` exposait `type` alors qu'aucune
  colonne `quizzes.type` n'existait : la clé sortait à `null` depuis des mois,
  sans erreur, et une décision produit est restée inapplicable sans que rien ne
  le signale. La colonne existe depuis. La section « Base ↔ API » du document
  généré détecte désormais ce motif automatiquement.
- **BACK#567 — la collision de clé.** Une fois `quizzes.type` créée, la même clé
  `type` a porté deux notions : *quelle sorte d'item est-ce* (leçon ou quiz, ce
  que lit le sommaire) et *quel type pédagogique de quiz*. Aucune erreur non
  plus : juste un sommaire qui ne reconnaît plus rien. **Leçon à retenir : une
  clé de charge utile porte UNE notion, et le nom d'une clé n'est pas libre dès
  lors qu'un écran s'en sert pour discriminer.**

### Ce qui reste hors du document

- Les **rôles et le vocabulaire** (le piège `manager` / `sub_manager`) : voir
  `docs/roadmap/` et les issues de la file « rôles ».
- Les **durées** (`duration_seconds` / `pedagogical_duration_seconds` /
  `duration_source`) : le modèle est décrit dans BACK#148 et BACK#139.
- Les **règles de périmètre** (qui voit quoi) : elles vivent dans les politiques
  et les portes d'accès, pas dans le schéma.

<!-- fin de l'annexe -->
