> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Class Diagram

> Every Eloquent model of the Laravel backend and how they relate: one global diagram, then a detailed view per domain.

This page maps the **43 Eloquent models** in `LMS_Backend/app/Models` and the relations they declare (`belongsTo`, `hasMany`, `hasOne`, `belongsToMany`). It was built from the backend code on 28/10/2026.

**Table of Contents**

* **How to read the diagrams**
* **1. Global class diagram**
* **2. Domain views**: tenancy & access, catalog & content, quizzes & exams, commerce, classrooms & chatrooms, notifications, landing page & settings
* **3. Domain rules the diagrams don't show**
* **4. Pivot and support tables without a model**
* **5. Keeping this page in sync**

***

## How to read the diagrams

| Notation | Meaning |
| :- | :- |
| `A "1" --> "*" B` | `A` has many `B` (`hasMany`); `B` holds the foreign key `a_id` |
| `A "1" --> "0..1" B` | `A` has one `B` (`hasOne`) |
| `A "*" --> "1" B` | `A` belongs to `B` (`belongsTo`), for lookups and files |
| `A "*" -- "*" B` | Many-to-many (`belongsToMany`) through a pivot table, named on the label |
| `A "*" -- "0..1" B` | A `belongsToMany` whose pivot allows one `B` per `A` (unique index on the `A` side): `Training` / `Quiz` ↔ `Category` |
| `A *-- B` | Composition: `B` cannot live without `A` (the foreign key cascades on delete) |
| `«pivot»` | A model that wraps a pivot table |
| `«lookup»` | Fixed rows seeded once, read through class constants (`OrderStatus::PAID_ONLINE_ID`, `Role::STUDENT`, …) |
| `«soft delete»` | The model uses `SoftDeletes` (archive / restore, see [Archive, delete & restore](https://theteam-13fc7f49.mintlify.site/api-reference/archive-delete-and-restore-flow.md)) |

Every model also has `id`, `created_at` and `updated_at`; the diagrams leave them out. Attributes are the `$fillable` columns (plus the important `$casts`), with the PHP cast as the type.

***

## 1. Global class diagram

All models and all relations, with only the key attributes. Use the zoom controls, or jump to the domain views in §2 for the full attribute lists.

```mermaid theme={null}
classDiagram
    direction LR

    %% ── Tenancy & access ──
    class Company {
        <<soft delete>>
        +int company_id
        +string label
        +string currency
    }
    class User {
        <<soft delete>>
        +int company_id
        +int role_id
        +string email
        +array owned_trainings
        +array owned_exams
        +array classroom_joined_slugs
    }
    class Role {
        <<lookup>>
        +string label
    }
    class Permission {
        <<lookup>>
        +string label
    }
    class PermissionRole {
        <<pivot>>
        +int company_id
        +int role_id
        +int permission_id
    }
    class UserBackgroundItem {
        +int user_id
        +string title
        +string level
    }
    class File {
        <<soft delete>>
        +int type
        +string file_name
        +string url
    }

    %% ── Catalog & content ──
    class Category {
        <<soft delete>>
        +string label
    }
    class Tag {
        <<soft delete>>
        +string label
    }
    class Training {
        <<soft delete>>
        +string slug
        +string label
        +double price
        +bool published
    }
    class Chapter {
        <<soft delete>>
        +string label
        +int position
    }
    class Lesson {
        <<soft delete>>
        +LessonType type
        +int position
        +string content
    }
    class Review {
        <<soft delete>>
        +float rating
        +string comment
    }
    class Certification {
        +string certification_id
    }
    class CertificationConfig {
        +string signatory
        +string title
    }

    %% ── Quizzes & exams ──
    class Quiz {
        <<soft delete>>
        +string slug
        +int training_id
        +int chapter_id
        +int max_attempts
        +float passing_percentage
        +isQuiz() bool
        +isExam() bool
    }
    class QuizQuestion {
        <<soft delete>>
        +string label
    }
    class QuizQuestionType {
        <<lookup>>
        +string label
    }
    class QuizQuestionOption {
        <<soft delete>>
        +string label
        +bool is_correct
    }
    class QuizQuestionOptionItem {
        <<soft delete>>
        +string label
    }
    class QuizStudentAttempt {
        <<pivot>>
        +int attempt
        +float score_percentage
    }
    class QuizStudentReset {
        +int reset_by
        +string note
    }

    %% ── Commerce ──
    class Order {
        <<soft delete>>
        +int type
        +int order_status_id
        +datetime access_granted_at
    }
    class OrderStatus {
        <<lookup>>
        +string label
    }
    class Coupon {
        +string code
        +float discount_percentage
        +int max_usage
    }
    class Voucher {
        <<soft delete>>
        +string code
        +string user_email
        +bool is_voucher_applied
    }
    class VoucherAttempt {
        +int attempts
        +datetime blocked_until
    }
    class KonnectHistory {
        +json webhook_history
    }
    class LoyaltyPointsSettings {
        +string key
        +int value
    }

    %% ── Classrooms & chatrooms ──
    class Classroom {
        <<soft delete>>
        +string classroom_slug
        +string classroom_title
        +datetime expire_at
    }
    class Chatroom {
        +string slug
        +bool learners_can_post
    }
    class ChatroomMember {
        +string role
    }
    class ChatroomMessage {
        +string content
    }
    class ChatroomAttachment {
        +string path
        +int size_bytes
    }

    %% ── Notifications ──
    class Notification {
        <<soft delete>>
        +string title
        +bool notif_read_status
    }
    class NotificationType {
        <<lookup>>
        +string label
    }
    class NotificationDispatch {
        +string scope
        +string status
    }

    %% ── Landing page & settings ──
    class LandingPage {
        <<soft delete>>
        +string header_title
        +string language
    }
    class LandingPage_Links {
        <<soft delete>>
        +string label
        +string url
    }
    class LandingPage_FeaturedCategory {
        +string title
    }
    class LandingPage_PartnerCompany {
        +string name
    }
    class PlatformSetting {
        +string key
        +string value
    }
    class PlatformThemeSetting {
        +string platform_primary_color
    }

    %% ── Relations: tenancy & access ──
    Company "0..1" --> "*" Company : companies (parent company_id)
    Company "1" --> "*" User : users
    Role "1" --> "*" User : users
    Role "*" -- "*" Permission : permission_role (per company)
    PermissionRole "*" --> "1" Company
    PermissionRole "*" --> "1" Role
    PermissionRole "*" --> "1" Permission
    User "1" *-- "*" UserBackgroundItem : backgroundItems
    User "1" --> "*" File : images_uploaded
    Company "1" --> "*" File : files

    %% ── Relations: catalog & content ──
    Company "1" --> "*" Category : categories
    Company "1" --> "*" Tag : tags
    Company "1" --> "*" Training : trainings
    User "1" --> "*" Training : trainingsAsInstructor
    Training "*" -- "0..1" Category : category_training
    Training "*" -- "*" Tag : tag_training
    Training "1" *-- "*" Chapter : chapters
    Chapter "1" *-- "*" Lesson : lessons
    Lesson "*" -- "*" User : student_lesson (done lessons)
    Training "1" --> "*" Review : reviews
    Quiz "1" --> "*" Review : reviews
    User "1" --> "*" Review : reviews
    User "1" --> "*" Certification : certifications
    Training "1" --> "*" Certification
    Quiz "1" --> "*" Certification
    Company "1" --> "0..1" CertificationConfig

    %% ── Relations: quizzes & exams ──
    Company "1" --> "*" Quiz
    Training "1" --> "*" Quiz : quizzes
    Chapter "1" --> "*" Quiz : quizzes
    User "1" --> "*" Quiz : examsAsInstructor
    Quiz "*" -- "0..1" Category : category_quiz
    Quiz "1" *-- "*" QuizQuestion : quiz_questions
    QuizQuestionType "1" --> "*" QuizQuestion
    QuizQuestion "1" *-- "*" QuizQuestionOption : quiz_question_options
    QuizQuestionOption "1" *-- "*" QuizQuestionOptionItem : items
    User "1" --> "*" QuizStudentAttempt : quiz_student_attempts
    Quiz "1" --> "*" QuizStudentAttempt
    QuizStudentAttempt "*" -- "*" QuizQuestionOption : quiz_student_answers
    Quiz "1" --> "*" QuizStudentReset
    User "1" --> "*" QuizStudentReset : student / reset_by

    %% ── Relations: commerce ──
    Company "1" --> "*" Order
    User "1" --> "*" Order : wishlist / cart / orders
    OrderStatus "1" --> "*" Order : orders
    Coupon "0..1" --> "*" Order : orders
    Voucher "0..1" --> "*" Order : orders
    Order "*" -- "*" Training : order_items
    Order "*" -- "*" Quiz : order_items
    Order "1" --> "0..1" KonnectHistory : konnectHistory
    Company "1" --> "*" Coupon : coupons
    Company "1" --> "*" Voucher
    Voucher "*" -- "*" Training : voucher_training
    Voucher "*" -- "*" Quiz : voucher_quiz
    User "1" --> "0..1" VoucherAttempt

    %% ── Relations: classrooms & chatrooms ──
    Classroom "*" -- "*" User : classroom_user
    User "1" --> "*" Classroom : instructor_id
    Classroom .. Chatroom : same slug (no FK)
    Company "1" --> "*" Chatroom
    User "1" --> "*" Chatroom : instructor_id
    Chatroom "1" *-- "*" ChatroomMember : members
    User "1" --> "*" ChatroomMember
    Chatroom "1" *-- "*" ChatroomMessage : messages
    User "1" --> "*" ChatroomMessage : sender_id
    ChatroomMessage "0..1" --> "*" ChatroomAttachment : attachments
    Chatroom "1" *-- "*" ChatroomAttachment : attachments

    %% ── Relations: notifications ──
    Company "1" --> "*" Notification : Notification
    User "1" --> "*" Notification : student_id
    Role "0..1" --> "*" Notification
    NotificationType "1" --> "*" Notification
    NotificationDispatch ..> Notification : fans out to

    %% ── Relations: landing page & settings ──
    Company "1" --> "0..1" LandingPage
    LandingPage "1" *-- "*" LandingPage_Links : links
    LandingPage "*" -- "*" LandingPage_FeaturedCategory : featuredCategories
    LandingPage "*" -- "*" LandingPage_PartnerCompany : partnerCompanies
    LandingPage "*" -- "*" Training : landing_page_training
    LandingPage "*" -- "*" Quiz : landing_page_quiz (exams)
    Company "1" --> "0..1" PlatformThemeSetting
    Company "1" --> "*" PlatformSetting
    Company "1" --> "*" LoyaltyPointsSettings
```

> [!NOTE]
>
> Many models also point at **`File`** for their images and videos (`image_id`, `video_id`, `badge_logo_id`, `company_image_id`, `signature_id`, `cachet_id`, `header_image_id`, …). The global diagram leaves those arrows out to stay readable; each domain view in §2 shows them.

***

## 2. Domain views

Each view shows the full attribute list and the main methods of the models in one domain. Classes from another domain appear with an empty body.

### 2.1 Tenancy, users & access

A **company** is the tenant: almost every table carries `company_id`. A user has exactly one platform **role** (`users.role_id`), and what that role may do is set **per company** in `permission_role`.

```mermaid theme={null}
classDiagram
    direction LR
    class Company {
        <<soft delete>>
        +int company_id
        +string label
        +string description
        +string currency
        +string company_phone_number
        +string company_email
        +int company_image_id
        +getNumberOfRatingsAttribute() int
        +getAverageRatingAttribute() float
    }
    class User {
        <<soft delete>>
        +int company_id
        +int role_id
        +string first_name
        +string last_name
        +string email
        +string phone_number
        +int image_id
        +hashed password
        +date birthday
        +string bio
        +datetime blocked_at
        +double default_revenue_percentage
        +bool two_factor_enabled
        +string two_factor_secret
        +string public_profile_fields
        +bool hide_public_profile
        +int gamification_score
        +array applied_coupons
        +array owned_trainings
        +array owned_exams
        +array classroom_joined_slugs
        +array classroom_admin_slugs
        +getFullNameAttribute() string
        +permissions() Collection
        +isSuperAdmin() bool
        +isAdmin() bool
        +isContentManager() bool
        +isInstructor() bool
        +isStudent() bool
        +isTwoFactorEnabled() bool
        +addLoyaltyPoints(points)
        +canAddToCart(itemId, itemType) bool
        +grantOwnedAssetsFromOrder(Order)
        +sanitizeOwnedAssets()
    }
    class Role {
        <<lookup>>
        +string label
        +string description
        STUDENT = 1
        INSTRUCTOR = 2
        CONTENT_MANAGER = 3
        ADMIN = 4
        SUPER_ADMIN = 5
    }
    class Permission {
        <<lookup>>
        +string label
        +string description
        COMPANIES_VIEWANY = 1 … LANDING_PAGE_FORCE_DELETE = 136
    }
    class PermissionRole {
        <<pivot>>
        +int company_id
        +int role_id
        +int permission_id
        +assign_permissions_to_roles(company_id)$
    }
    class UserBackgroundItem {
        <<soft delete>>
        +int user_id
        +string organization_institute_name
        +string title
        +string level
        +string description
        +date start_date
        +date end_date
        +string status
        +int logo_id
    }
    class File {
        <<soft delete>>
        +int company_id
        +int user_id
        +int type
        +string file_name
        +string url
        TYPE_INTERNAL = 1
        TYPE_EXTERNAL = 2
        TYPE_R2 = 3
    }

    Company "0..1" --> "*" Company : companies
    Company "1" --> "*" User : users
    Company "*" --> "0..1" File : companyImage
    Company "*" -- "*" Role : permission_role
    Role "1" --> "*" User : users
    Role "*" -- "*" Permission : permission_role
    PermissionRole "*" --> "1" Company
    PermissionRole "*" --> "1" Role
    PermissionRole "*" --> "1" Permission
    User "*" --> "0..1" File : image
    User "1" --> "*" File : images_uploaded
    User "1" *-- "*" UserBackgroundItem : backgroundItems
    UserBackgroundItem "*" --> "0..1" File : logo
    Company "1" --> "*" File : files
```

### 2.2 Catalog & content (trainings, chapters, lessons)

A **training** is a course: chapters hold lessons, and a chapter can hold several quizzes. Students mark lessons as done through `student_lesson`.

```mermaid theme={null}
classDiagram
    direction LR
    class Training {
        <<soft delete>>
        +int company_id
        +int instructor_id
        +string slug
        +string label
        +string description
        +string level
        +int duration
        +double price
        +double discounted_price
        +double revenue_percentage
        +int image_id
        +int video_id
        +int badge_logo_id
        +bool is_public
        +bool published
        +int views_count
        +getFinalPriceAttribute() double
        +getAverageRatingAttribute() float
        +getTotalSalesAttribute() int
        +getTotalLessonsCountAttribute() int
        +allQuizzesQuery() Builder
        +generateUniqueSlug(label)$ string
        BEGINNER / INTERMEDIATE / EXPERT
    }
    class Chapter {
        <<soft delete>>
        +int training_id
        +string label
        +int position
    }
    class Lesson {
        <<soft delete>>
        +int chapter_id
        +string label
        +LessonType type
        +int position
        +string content
    }
    class LessonType {
        <<enumeration>>
        document
        video
    }
    class Category {
        <<soft delete>>
        +int company_id
        +string label
    }
    class Tag {
        <<soft delete>>
        +int company_id
        +string label
    }
    class Review {
        <<soft delete>>
        +int company_id
        +int training_id
        +int quiz_id
        +int user_id
        +float rating
        +string comment
    }
    class Certification {
        +string certification_id
        +int student_id
        +int training_id
        +int quiz_id
        +generateCertificationIdentifier() string
    }
    class CertificationConfig {
        +int company_id
        +int signature_id
        +int cachet_id
        +int company_logo_id
        +string signatory
        +string default_badge_logo
        +string title
    }
    class User
    class Quiz
    class File
    class Company

    Company "1" --> "*" Training : trainings
    User "1" --> "*" Training : instructor
    Training "*" -- "0..1" Category : category_training
    Training "*" -- "*" Tag : tag_training
    Training "1" *-- "*" Chapter : chapters
    Chapter "1" *-- "*" Lesson : lessons
    Lesson --> LessonType
    Lesson "*" -- "*" User : student_lesson
    Training "1" --> "0..1" Quiz : quiz (final quiz)
    Training "1" --> "*" Quiz : quizzes
    Chapter "1" --> "*" Quiz : quizzes
    Training "1" --> "*" Review : reviews
    Quiz "1" --> "*" Review : reviews
    User "1" --> "*" Review : reviews
    Training "*" --> "0..1" File : image / video / badge_logo
    User "1" --> "*" Certification : certifications
    Training "1" --> "*" Certification : certified_students
    Quiz "1" --> "*" Certification : certified_students
    Company "1" --> "*" Category
    Company "1" --> "*" Tag
    Company "1" --> "0..1" CertificationConfig
    CertificationConfig "*" --> "0..1" File : signature / cachet / company_logo
```

### 2.3 Quizzes & exams

One `Quiz` model covers both: a **quiz** is attached to a training or a chapter, an **exam** is standalone and sold on its own (see §3).

```mermaid theme={null}
classDiagram
    direction LR
    class Quiz {
        <<soft delete>>
        +int company_id
        +int training_id
        +int chapter_id
        +int instructor_id
        +int image_id
        +string slug
        +string label
        +string description
        +int duration
        +int break_interval
        +int break_duration
        +int max_attempts
        +float passing_percentage
        +bool is_published
        +double price
        +double discounted_price
        +double revenue_percentage
        +isQuiz() bool
        +isExam() bool
        +scopeIsQuiz(query)
        +scopeIsExam(query)
        +getFinalPriceAttribute() double
        +getTotalSalesAttribute() int
    }
    class QuizQuestion {
        <<soft delete>>
        +int quiz_id
        +int quiz_question_type_id
        +int image_id
        +string label
        +string tip
        +string explanation
    }
    class QuizQuestionType {
        <<lookup>>
        +string label
        TRUE_OR_FALSE = 1
        SINGLE_ANSWER = 2
        MULTIPLE_ANSWER = 3
        DRAG_AND_DROP = 4
    }
    class QuizQuestionOption {
        <<soft delete>>
        +int quiz_question_id
        +string label
        +bool is_correct
    }
    class QuizQuestionOptionItem {
        <<soft delete>>
        +int quiz_question_option_id
        +string label
    }
    class QuizStudentAttempt {
        <<pivot>>
        +int student_id
        +int quiz_id
        +int attempt
        +datetime finished_at
        +int correct_answers
        +int uncorrect_answers
        +float score_percentage
        +datetime passed_at
    }
    class QuizStudentReset {
        +int quiz_id
        +int student_id
        +int reset_by
        +string note
    }
    class Training
    class Chapter
    class User
    class Category
    class File

    Training "1" --> "*" Quiz : quizzes
    Chapter "1" --> "*" Quiz : quizzes
    User "1" --> "*" Quiz : instructor (exams)
    Quiz "*" -- "0..1" Category : category_quiz
    Quiz "*" --> "0..1" File : image
    Quiz "1" *-- "*" QuizQuestion : quiz_questions
    QuizQuestionType "1" --> "*" QuizQuestion : quiz_questions
    QuizQuestion "*" --> "0..1" File : image
    QuizQuestion "1" *-- "*" QuizQuestionOption : quiz_question_options
    QuizQuestionOption "1" *-- "*" QuizQuestionOptionItem : quiz_question_option_items
    User "1" --> "*" QuizStudentAttempt : quiz_student_attempts
    Quiz "1" --> "*" QuizStudentAttempt : students
    QuizStudentAttempt "*" -- "*" QuizQuestion : quiz_student_answers
    QuizStudentAttempt "*" -- "*" QuizQuestionOption : quiz_student_answers
    QuizStudentAttempt "*" -- "*" QuizQuestionOptionItem : quiz_student_answers
    Quiz "1" --> "*" QuizStudentReset
    User "1" --> "*" QuizStudentReset : student
    User "1" --> "*" QuizStudentReset : resetBy
```

### 2.4 Commerce (orders, coupons, vouchers, payment)

One `orders` table holds a student's **wishlist** (`type = 1`), **cart** (`type = 2`) and real **orders** (`type = 3`). The items live in `order_items` (a training **or** an exam, with its `price` and `access_revoked`).

```mermaid theme={null}
classDiagram
    direction LR
    class Order {
        <<soft delete>>
        +int company_id
        +int student_id
        +int type
        +int order_status_id
        +int coupon_id
        +int voucher_id
        +string notes
        +string order_system_log
        +string payment_ref
        +datetime access_granted_at
        +getTotalPriceAttribute() float
        +getDiscountedPriceAttribute() float
        +sync_order_items(trainingIds, quizIds)
        +assign_free_order_items(trainingIds, quizIds)
        WISHLIST = 1
        CART = 2
        ORDER = 3
    }
    class OrderStatus {
        <<lookup>>
        +string label
        +string description
        CANCELLED = 1
        PENDING = 2
        PAYMENT_FAILED = 3
        PAID_OFFLINE = 4
        PAID_ONLINE = 5
        PENDING_ONLINE = 6
    }
    class Coupon {
        +int company_id
        +string code
        +string title
        +string description
        +float discount_percentage
        +float discount_value
        +float applicable_if_total_is_above
        +int max_usage
        +bool active
        +datetime starts_at
        +datetime expires_at
        +getIsExpiredAttribute() bool
        +getIsActiveAttribute() bool
    }
    class Voucher {
        <<soft delete>>
        +int company_id
        +string code
        +string title
        +string description
        +string user_email
        +bool mail_sent
        +bool active
        +bool locked_mail
        +date expires_at
        +bool is_voucher_applied
    }
    class VoucherAttempt {
        +int user_id
        +int attempts
        +datetime blocked_until
    }
    class KonnectHistory {
        +int order_id
        +json webhook_history
    }
    class LoyaltyPointsSettings {
        +int company_id
        +string key
        +int value
    }
    class User
    class Training
    class Quiz
    class Company

    User "1" --> "0..1" Order : wishlist (type 1)
    User "1" --> "0..1" Order : cart (type 2)
    User "1" --> "*" Order : orders (type 3)
    Company "1" --> "*" Order
    OrderStatus "1" --> "*" Order : orders
    Coupon "0..1" --> "*" Order : orders
    Voucher "0..1" --> "*" Order : orders
    Order "*" -- "*" Training : order_items (price, access_revoked)
    Order "*" -- "*" Quiz : order_items (price, access_revoked)
    Order "1" --> "0..1" KonnectHistory : konnectHistory
    Company "1" --> "*" Coupon : coupons
    Company "1" --> "*" Voucher
    Voucher "*" -- "*" Training : voucher_training
    Voucher "*" -- "*" Quiz : voucher_quiz
    User "1" --> "0..1" VoucherAttempt
    Company "1" --> "*" LoyaltyPointsSettings
```

See [Orders & payment](https://theteam-13fc7f49.mintlify.site/api-reference/orders-and-payment-endpoints.md), [Coupons](https://theteam-13fc7f49.mintlify.site/api-reference/coupons-endpoints.md) and [Vouchers](https://theteam-13fc7f49.mintlify.site/api-reference/vouchers-endpoints.md).

### 2.5 Classrooms & chatrooms

A **classroom** groups students around an instructor. Its **chatroom** shares the classroom's slug but is its own entity with its own four tables (see [Chatrooms](https://theteam-13fc7f49.mintlify.site/api-reference/chatrooms.md)).

```mermaid theme={null}
classDiagram
    direction LR
    class Classroom {
        <<soft delete>>
        +int company_id
        +int instructor_id
        +string classroom_slug
        +string classroom_title
        +string classroom_description
        +string classroom_color
        +string conversation_url
        +string content
        +datetime expire_at
        +generateUniqueSlug(title)$ string
    }
    class Chatroom {
        +int company_id
        +string slug
        +string title
        +int instructor_id
        +int created_by
        +bool learners_can_post
        +datetime last_message_at
        +generateUniqueSlug(title)$ string
        ROLE_INSTRUCTOR / ROLE_LEARNER / ROLE_ADMIN
    }
    class ChatroomMember {
        +int chatroom_id
        +int user_id
        +string role
        +int last_notification_id
    }
    class ChatroomMessage {
        +int chatroom_id
        +int sender_id
        +string content
    }
    class ChatroomAttachment {
        +int chatroom_id
        +int message_id
        +int uploader_id
        +string disk
        +string path
        +string url
        +string filename
        +string mime_type
        +int size_bytes
        +int width
        +int height
    }
    class User
    class Company

    Classroom "*" -- "*" User : classroom_user (users)
    User "1" --> "*" Classroom : instructor
    Classroom .. Chatroom : classroom_slug = slug
    Company "1" --> "*" Chatroom
    User "1" --> "*" Chatroom : instructor
    Chatroom "1" *-- "*" ChatroomMember : members / learnerMembers
    User "1" --> "*" ChatroomMember : user
    Chatroom "1" *-- "*" ChatroomMessage : messages
    User "1" --> "*" ChatroomMessage : sender
    Chatroom "1" *-- "*" ChatroomAttachment : attachments
    ChatroomMessage "0..1" --> "*" ChatroomAttachment : attachments
```

### 2.6 Notifications

A super admin's broadcast is stored once as a `NotificationDispatch` (who it targets, when to send). When it runs, it writes one `Notification` row per target user.

```mermaid theme={null}
classDiagram
    direction LR
    class Notification {
        <<soft delete>>
        +int company_id
        +int student_id
        +int role_id
        +int notification_type_id
        +string title
        +string message
        +string notif_status
        +bool notif_read_status
        +bool notif_archive_status
        +bool notif_smtp_status
        +string email
        +datetime scheduled_at
    }
    class NotificationType {
        <<lookup>>
        +string label
        SUCCESS = 1 … ENROLLMENT = 13
    }
    class NotificationDispatch {
        +int created_by
        +string scope
        +array company_ids
        +array role_ids
        +array user_ids
        +array target_user_ids
        +string title
        +string message
        +int notification_type_id
        +bool notif_smtp_status
        +datetime scheduled_at
        +string status
        +datetime sent_at
        +string error
    }
    class Company
    class User
    class Role

    Company "1" --> "*" Notification : Notification
    User "1" --> "*" Notification : student
    Role "0..1" --> "*" Notification : role
    NotificationType "1" --> "*" Notification : notificationType
    NotificationDispatch ..> Notification : creates one per target user
    NotificationDispatch ..> NotificationType
```

### 2.7 Landing page & platform settings

```mermaid theme={null}
classDiagram
    direction LR
    class LandingPage {
        <<soft delete>>
        +int company_id
        +bool show_header_section
        +bool show_category_section
        +bool show_training_section
        +bool show_exams_section
        +bool show_partner_company_section
        +bool show_banner
        +string company_name
        +string company_description
        +string landing_page_primary_color
        +string landing_page_secondary_color
        +string header_title
        +string header_subtitle
        +string header_description
        +string header_registration_link
        +string header_call_to_action_link
        +int header_image_id
        +int header_background_image_id
        +string banner_title
        +string banner_call_to_action_link
        +int banner_image_id
        +string footer_fields
        +string language
    }
    class LandingPage_Links {
        <<soft delete>>
        +int landing_page_id
        +string label
        +string url
        +bool is_social_link
        +forceDeleteLink()
    }
    class LandingPage_FeaturedCategory {
        +int landing_page_id
        +string title
        +string description
        +int feat_category_image_id
    }
    class LandingPage_PartnerCompany {
        +int landing_page_id
        +string name
        +int company_logo_id
        +string company_url
        +string company_description
    }
    class PlatformThemeSetting {
        +int company_id
        +string platform_primary_color
        +string platform_secondary_color
        +string platform_logo
        +string auth_page_image
        +string backend_version
    }
    class PlatformSetting {
        +int company_id
        +string key
        +string value
        +bool encrypted
    }
    class Company
    class Training
    class Quiz
    class File

    Company "1" --> "0..1" LandingPage
    LandingPage "1" *-- "*" LandingPage_Links : links
    LandingPage "*" -- "*" LandingPage_FeaturedCategory : featuredCategories (position)
    LandingPage "*" -- "*" LandingPage_PartnerCompany : partnerCompanies (position)
    LandingPage "*" -- "*" Training : landing_page_training
    LandingPage "*" -- "*" Quiz : landing_page_quiz (exams)
    LandingPage "*" --> "0..1" File : header / background / banner image
    LandingPage_FeaturedCategory "*" --> "0..1" File : feat_category_image
    LandingPage_PartnerCompany "*" --> "0..1" File : companyLogo
    Company "1" --> "0..1" PlatformThemeSetting
    Company "1" --> "*" PlatformSetting
```

***

## 3. Domain rules the diagrams don't show

> [accordion] Quiz vs exam: one model, two meanings
>
> `Quiz` is both. When `training_id` or `chapter_id` is set, it is a **quiz** inside a training (`isQuiz()`, scope `isQuiz`). When both are `null`, it is a standalone **exam** (`isExam()`, scope `isExam`) with its own price, instructor, categories, reviews and certificates, sold through `order_items` like a training. A training can have several chapter quizzes; `Training::quiz()` returns the training's own final quiz (no `chapter_id`).

> [accordion] Ownership by slug
>
> Besides the paid `order_items`, a user's access is cached in JSON arrays on the `users` row: `owned_trainings` and `owned_exams` (slugs), `classroom_joined_slugs` and `classroom_admin_slugs`. `User::grantOwnedAssetsFromOrder()` fills them when an order is paid, and `sanitizeOwnedAssets()` cleans them. These arrays are not foreign keys, so the diagrams show them as attributes only.

> [accordion] Tenant isolation
>
> Every company-owned model carries `company_id` (trainings, quizzes, orders, coupons, vouchers, files, notifications, chatrooms, settings…). Policies and queries always filter on the caller's company; only the super admin crosses companies.

> [accordion] Reviews and certificates target a training OR an exam
>
> `Review` and `Certification` both hold a nullable `training_id` and a nullable `quiz_id`: exactly one is set. A certificate's public identifier is `certification_id`, generated on create.

> [accordion] One category per training, exam and quiz
>
> Since 01/11/2026 a training, an exam or a quiz has at most one category. `Training::categories()` and `Quiz::categories()` are still `belongsToMany` through `category_training` / `category_quiz` (and the API keeps the `categories` array for older app builds), but the migration `2026_11_01_000000_keep_one_category_per_item` added a unique index on `training_id` / `quiz_id`, so a second `attach()` fails; change a category with `sync([$id])`. The limit is `Category::MAX_PER_ITEM`. Tags stay many-to-many. See [Trainings → Assign categories](https://theteam-13fc7f49.mintlify.site/api-reference/trainings.md#assign-categories) and [Quizzes & Exams §3.3](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-endpoints.md).

> [accordion] Files
>
> `File.type` says where the file lives: `1` internal disk, `2` external URL, `3` Cloudflare R2. Images, videos, logos, signatures and stamps are all `File` rows referenced by an `*_id` column.

***

## 4. Pivot and support tables without a model

| Table | Joins | Extra columns |
| :- | :- | :- |
| `order_items` | `orders` ↔ `trainings` / `quizzes` | `price`, `access_revoked`, `expires_at` |
| `student_lesson` | `users` ↔ `lessons` | timestamps (lesson done) |
| `quiz_student_answers` | `quiz_student_attempts` ↔ questions / options / option items | the answer given |
| `classroom_user` | `classrooms` ↔ `users` | |
| `category_training`, `tag_training`, `category_quiz` | catalog ↔ categories / tags | timestamps; unique `training_id` on `category_training` and `quiz_id` on `category_quiz` (one category per item, since 01/11/2026) |
| `voucher_training`, `voucher_quiz` | `vouchers` ↔ trainings / exams | |
| `landing_page_training`, `landing_page_quiz` | `landing_pages` ↔ trainings / exams | |
| `landing_page_featured_category_pivot`, `landing_page_partner_company_pivot` | `landing_pages` ↔ featured categories / partners | `position` |
| `history` | audit log: `user_id`, `action`, `table_name`, `row_id` | see [History](https://theteam-13fc7f49.mintlify.site/api-reference/history-activity-log-api.md) |
| `email_logs` | sent e-mails: `recipient_email`, `subject`, `message` | |
| `personal_access_tokens`, `password_reset_tokens`, `jobs` | Laravel framework tables (Sanctum, password reset, queue) | |

***

## 5. Keeping this page in sync

When a backend change adds, removes or renames a model, a relation method or a `$fillable` column in `app/Models`, update the class in the matching §2 view **and** in the global diagram of §1.

> [accordion] Code notes found while building this page (28/10/2026)
>
> * `Company::quizzes()`, `reviews()`, `vouchers()`, `orders()` and `certifications()` return `hasMany(Category::class)`, a copy-paste leftover. The diagrams show the intended relations (to `Quiz`, `Review`, `Voucher`, `Order`, `Certification`); the controllers query those tables by `company_id` directly.
> * `QuizQuestion::students()` points at a `student_quizzes` table that no migration creates. It is left out of the diagrams.
> * `NotificationType::AI_GENERATED_ID` and `ENROLLMENT_ID` both equal `13`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.