# Tài liệu Database Schema — Bảng nghiệp vụ chính

Tài liệu thống kê ~42 bảng nghiệp vụ chính của LMS Hocmai: cấu trúc cột, ý nghĩa, quan hệ giữa các bảng.

**Nguồn:** Eloquent models (`app/Models/`), migrations (`database/migrations/`), tài liệu liên quan (`docs/iclScore/`, `docs/reports/sql/`).

**Phạm vi loại trừ:** OA (`oa_*`), SpeakUp tracking, EMS import, testsite/AIHub, log tables, LCMS mirror (`lcms_*`), infrastructure (`jobs`, `cache`, `sessions`), và các bảng `student_class`, `courses`, `student_activity_progress`.

---

## Mục lục

1. [Kiến trúc multi-tenant](#1-kiến-trúc-multi-tenant)
2. [Tenant & Registry](#2-tenant--registry-5-bảng)
3. [Auth & RBAC](#3-auth--rbac-5-bảng)
4. [Người dùng & Lớp học](#4-người-dùng--lớp-học-7-bảng)
5. [Moodle / Cấu trúc khóa học](#5-moodle--cấu-trúc-khóa-học-6-bảng)
6. [Thi & Chấm điểm](#6-thi--chấm-điểm-9-bảng)
7. [Zeus Integration](#7-zeus-integration-2-bảng)
8. [QuizNoEMS](#8-quiznoems--chấm-kỹ-năng-ngoài-ems-5-bảng)
9. [ICL & Score Conversion](#9-icl--score-conversion-6-bảng)
10. [Sơ đồ ER tổng hợp](#10-sơ-đồ-er-tổng-hợp)
11. [Ghi chú schema](#11-ghi-chú-schema)

---

## 1. Kiến trúc multi-tenant

```mermaid
flowchart TB
    subgraph central [Central DB - mysql]
        HocmaiTenant[hocmai_tenants]
        HocmaiTenantDomain[hocmai_tenant_domains]
        UserTenants[user_tenants]
    end
    subgraph tenantDB [Tenant DB - db_name per tenant]
        Students[students]
        ApiMoodle[api_moodle]
        StudentScore[student_score]
        ZeusSession[zeus_session_completions]
    end
    HocmaiTenant -->|db_name| tenantDB
    UserTenants --> HocmaiTenant
```

| Lớp | Connection | Bảng | Pattern |
|-----|------------|------|---------|
| **Central registry** | Default (`mysql`) | `hocmai_tenants`, `hocmai_tenant_domains`, `hocmai_tenant_groups`, `hocmai_tenant_courses`, `user_tenants` | Không dùng `UsesTenantConnection` |
| **Tenant business** | Per-tenant (`db_name`) | students, exam, moodle, zeus, quiznoems… | Trait `UsesTenantConnection` + `Model::on($tenantConnection)` |

- **Hub trung tâm:** `api_moodle` — cây phân cấp `category → course → section → quiz/activity`
- **Lưu ý:** `course_id` trong các bảng enrollment (`course_student`, `course_teacher`, …) thường trỏ tới `api_moodle.id` (node course), không phải bảng `courses` (đã loại trừ khỏi tài liệu này)
- Hầu hết quan hệ là **logical FK** (Eloquent), không bắt buộc có DB foreign key constraint

---

## 2. Tenant & Registry (5 bảng)

Domain quản lý danh sách tenant, domain, nhóm quyền API, mapping khóa học và gán user ↔ tenant.

### `hocmai_tenants`

**Model:** `App\Models\HocmaiTenant` | **Soft delete:** Có  
**Mô tả:** Đăng ký tenant (một tenant = một database nghiệp vụ).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| code | varchar unique | NO | Mã tenant (vd: `ICC`, `OMO`) |
| tenant_key | varchar | YES | Khóa định danh/API bổ sung |
| name | varchar | NO | Tên hiển thị |
| db_name | varchar | NO | Tên database/connection của tenant |
| description | text | YES | Mô tả |
| status | tinyint (def 1) | NO | `1` active · `0` inactive |
| created_at / updated_at | timestamp | YES | Thời gian tạo/cập nhật |
| deleted_at | timestamp | YES | Xóa mềm |

**Quan hệ:** `hasMany` domains, groups; `hasMany` tenantCourses (qua `tenant_code`); `belongsToMany` User qua `user_tenants`.

---

### `hocmai_tenant_domains`

**Model:** `App\Models\HocmaiTenantDomain` | **Soft delete:** Có  
**Mô tả:** Domain/host gắn với tenant (dùng để resolve tenant từ request).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| name | varchar | NO | Domain/host |
| tenant_id | bigint | NO | → `hocmai_tenants.id` |
| tenant_code | varchar | NO | Mã tenant (denormalized) |
| db_name | varchar | NO | DB tương ứng domain |
| api_key | varchar | YES | API key xác thực theo domain |
| status | tinyint (def 1) | NO | `1` active · `0` inactive |
| created_at / updated_at / deleted_at | timestamp | YES | |

**Quan hệ:** `belongsTo` HocmaiTenant.

---

### `hocmai_tenant_groups`

**Model:** `App\Models\HocmaiTenantGroup` | **Soft delete:** Không  
**Mô tả:** Nhóm quyền API của tenant (map `config/tenant_groups.php`, middleware `EnsureTenantHasAccess`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| tenant_id | bigint | NO | → `hocmai_tenants.id` |
| tenant_code | varchar | NO | Mã tenant |
| db_name | varchar | NO | Tên DB tenant |
| group_key | varchar | NO | Key nhóm (vd: `api_student`, `api_course`) |
| created_at / updated_at | timestamp | YES | |

---

### `hocmai_tenant_courses`

**Model:** `App\Models\HocmaiTenantCourse` | **Soft delete:** Không  
**Mô tả:** Mapping khóa học Moodle thuộc tenant (central registry).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| tenant_code | varchar | NO | Mã tenant |
| db_name | varchar | NO | DB tenant chứa khóa học |
| api_moodle_id | bigint | NO | → `api_moodle.id` (trên tenant DB) |
| moodle_id | bigint | NO | ID course trên Moodle |
| moodle_type | varchar | NO | Loại entity Moodle |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ApiMoodle (`api_moodle_id`).

---

### `user_tenants`

**Model:** `App\Models\UserTenant` | **Soft delete:** Không  
**Mô tả:** Pivot gán user với tenant. Unique `(user_id, tenant_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| user_id | bigint | NO | → `users.id` |
| tenant_id | bigint | NO | → `hocmai_tenants.id` |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` User, HocmaiTenant.

---

## 3. Auth & RBAC (5 bảng)

Domain đăng nhập và phân quyền (thường trên **tenant DB**).

### `users`

**Model:** `App\Models\User` | **Soft delete:** Không  
**Mô tả:** Tài khoản đăng nhập LMS.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| moodle_user_id | bigint | YES | ID user Moodle (đồng bộ) |
| name | varchar | NO | Họ tên hiển thị |
| username | varchar | YES | Tên đăng nhập |
| email | varchar unique | NO | Email đăng nhập |
| email_verified_at | timestamp | YES | Thời điểm xác minh email |
| password | varchar | NO | Mật khẩu đã hash |
| type | tinyint (def 0) | NO | `0` User · `1` Admin · `2` Manager |
| remember_token | varchar(100) | YES | Token “remember me” |
| google_id | varchar | YES | ID Google OAuth |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsToMany` Roles qua `user_role`; `hasOne` Teachers; `hasManyThrough` Classes.

---

### `roles`

**Model:** `App\Models\Roles` | **Soft delete:** Có  
**Mô tả:** Vai trò (teacher, student, admin…).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| name | varchar | YES | Tên role |
| description | text | YES | Mô tả |
| short_name | varchar | YES | Alias ngắn |
| created_at / updated_at / deleted_at | timestamp | YES | |

**Quan hệ:** `belongsToMany` User, Permission.

---

### `permission`

**Model:** `App\Models\Permission` | **Soft delete:** Không | **Table:** `permission`  
**Mô tả:** Quyền gắn route/action.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| name | varchar | YES | Tên quyền |
| route_name | varchar | YES | Tên route Laravel |
| action | varchar | YES | Action/controller method |
| method | varchar | YES | HTTP method |
| description | text | YES | Mô tả |
| created_at / updated_at | timestamp | YES | |

---

### `user_role`

**Model:** `App\Models\UserRole` | **Soft delete:** Có  
**Mô tả:** Pivot user ↔ role.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| role_id | bigint | YES | → `roles.id` |
| user_id | bigint | YES | → `users.id` |
| created_at / updated_at / deleted_at | timestamp | YES | |

---

### `role_permission`

**Model:** `App\Models\RolePermission` | **Soft delete:** Có  
**Mô tả:** Pivot role ↔ permission.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| role_id | bigint | YES | → `roles.id` |
| permission_id | bigint | YES | → `permission.id` |
| created_at / updated_at / deleted_at | timestamp | YES | |

---

## 4. Người dùng & Lớp học (7 bảng)

Domain hồ sơ học viên/giáo viên, lớp và ghi danh khóa học.

```mermaid
erDiagram
    users ||--o| students : user_id
    users ||--o| teachers : user_id
    students ||--o{ course_student : enrolls
    teachers ||--o{ course_teacher : teaches
    classes ||--o{ course_student : class_id
    classes ||--o{ course_teacher : class_id
    classes ||--o{ class_course : links
    classes ||--o{ class_user : assigns
    api_moodle ||--o{ course_student : course_id
    api_moodle ||--o{ course_teacher : course_id
    api_moodle ||--o{ class_course : course_id
```

### `students`

**Model:** `App\Models\Students` | **Soft delete:** Có  
**Mô tả:** Hồ sơ học viên trên tenant DB. `zeus_id` là khóa đồng bộ với Zeus/ICL.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính hồ sơ HV |
| user_id | bigint | YES | → `users.id` (tài khoản đăng nhập) |
| zeus_id | varchar | YES | ID user bên Zeus |
| pos_id | varchar | YES | ID phía POS / Speakup mapping |
| name | varchar | YES | Họ tên |
| username | varchar | YES | Username |
| email | varchar unique | NO | Email |
| sso_name / sso_id | varchar | YES | Thông tin SSO (ICanID) |
| moodle_id | bigint | YES | ID user Moodle hiện tại |
| dob / gender / phone | varchar | YES | Thông tin cá nhân |
| isUpdate | tinyint (def 0) | YES | Cờ đã sync/update hồ sơ từ Zeus |
| avatar | text | YES | URL/path ảnh đại diện |
| class_code / school_code | varchar | YES | Mã lớp / mã trường (OMO) |
| password_plain | varchar | YES | Mật khẩu gốc (provision OMO) |
| created_at / updated_at / deleted_at | timestamp | YES | |

**Quan hệ:** `belongsToMany` ApiMoodle qua `course_student`; `hasManyThrough` Classes.

---

### `teachers`

**Model:** `App\Models\Teachers` | **Soft delete:** Có  
**Mô tả:** Hồ sơ giáo viên.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| user_id | bigint | YES | → `users.id` |
| sso_name / sso_id | varchar | YES | SSO |
| name / username | varchar | YES | Họ tên / username |
| email | varchar unique | NO | Email |
| phone | varchar | YES | SĐT |
| moodle_id | bigint | YES | ID user Moodle |
| created_at / updated_at / deleted_at | timestamp | YES | |

**Quan hệ:** `belongsToMany` ApiMoodle qua `course_teacher`.

---

### `classes`

**Model:** `App\Models\Classes` | **Soft delete:** Có  
**Mô tả:** Lớp học LMS.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| name | varchar | YES | Tên lớp |
| code | varchar | YES | Mã lớp |
| year | varchar | YES | Năm học / niên khóa |
| status | tinyint (def 0) | YES | Trạng thái lớp |
| created_at / updated_at / deleted_at | timestamp | YES | |

**Quan hệ:** `belongsToMany` ApiMoodle qua `class_course`.

---

### `course_student`

**Model:** `App\Models\CourseStudent` | **Soft delete:** Có  
**Mô tả:** Ghi danh học viên vào khóa học (+ lớp).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính enrollment |
| course_id | bigint | NO | → `api_moodle.id` (node course) |
| student_id | bigint | NO | → `students.id` |
| class_id | bigint | NO | → `classes.id` |
| course_end | varchar | YES | Thời điểm hết hạn khóa (từ Zeus `time_end`) |
| is_abnormal | boolean (def false) | NO* | Đánh dấu enrollment bất thường (từ bên thứ 3) |
| created_at / updated_at / deleted_at | timestamp | YES | |

\*Có thể chưa có trên mọi tenant DB (alter migration).

**Quan hệ:** `belongsTo` ApiMoodle, Students, Classes.

---

### `course_teacher`

**Model:** `App\Models\CourseTeacher` | **Soft delete:** Có  
**Mô tả:** Phân công giáo viên dạy khóa/lớp.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| course_id | bigint | NO | → `api_moodle.id` |
| teacher_id | bigint | NO | → `teachers.id` |
| class_id | bigint | YES | → `classes.id` |
| user_id | bigint | YES | Denormalized `users.id` (một số tenant) |
| created_at / updated_at / deleted_at | timestamp | YES | |

---

### `class_course`

**Model:** `App\Models\ClassCourse` | **Soft delete:** Có  
**Mô tả:** Pivot lớp ↔ khóa học (`api_moodle`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| course_id | bigint | YES | → `api_moodle.id` |
| class_id | bigint | YES | → `classes.id` |
| created_at / updated_at / deleted_at | timestamp | YES | |

---

### `class_user`

**Model:** `App\Models\ClassUser` | **Soft delete:** Không  
**Mô tả:** Gán user (thường GV/manager) với lớp. Unique `(class_id, user_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| class_id | bigint | NO | → `classes.id` |
| user_id | bigint | NO | → `users.id` |
| created_at / updated_at | timestamp | YES | |

---

## 5. Moodle / Cấu trúc khóa học (6 bảng)

Domain cây khóa học Moodle mirror trên LMS, liên kết EMS, completion và rule mở section.

### Cây `api_moodle`

```
category
  └── course          (parent_id = category.id)
        └── section   (parent_id = course.id; section_type 1–4)
              └── quiz / resource / …  (parent_id = section.id)
```

| `moodle_type` | Vai trò |
|---------------|---------|
| `category` | Product/category root |
| `course` | Khóa học |
| `section` | Topic/section |
| `quiz` | Activity quiz |
| `resource` | Tài nguyên |
| *(mod khác)* | Activity Moodle khác |

| `section_type` | Ý nghĩa |
|----------------|---------|
| 1 | Bài giảng |
| 2 | Bài tập về nhà (BTVN) |
| 3 | Bài kiểm tra (BKT) |
| 4 | Tài nguyên |

---

### `api_moodle`

**Model:** `App\Models\ApiMoodle` | **Soft delete:** Không  
**Mô tả:** Mirror cấu trúc Moodle (course/section/activity). **Hub trung tâm** của hệ thống.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính LMS |
| moodle_id | bigint | YES | ID trên Moodle (course/section/cm) |
| moodle_name | varchar | YES | Tên |
| description | text | YES | Mô tả |
| moodle_type | varchar | YES | `category` / `course` / `section` / `quiz` / … |
| parent_id | bigint | YES | Self-ref → `api_moodle.id` |
| level | bigint | YES | Độ sâu cây |
| code | varchar | YES | Mã nội bộ |
| creator / modifier | varchar | YES | Người tạo / sửa |
| visible | tinyint (def 1) | NO | `1` hiện · `0` ẩn |
| section_type | int | YES | Chỉ meaningful khi type=`section` |
| skip_completion | tinyint (def 0) | YES | Bỏ qua completion |
| is_final_test | tinyint (def 0) | NO | Final Test (section_type=3) |
| course_type_id | bigint | YES | → course_types |
| rubric_template_id | bigint | YES | → rubric_templates |
| conversion_group_id | bigint | YES | → `conversion_groups.id` |
| quiz_settings_type | bigint | YES | Loại học liệu / contest type |
| quiz_template | int | YES | Template quiz |
| quiz_result_template | int | YES | Template kết quả |
| quiz_submitearly | bigint | YES | Cho phép nộp sớm |
| quiz_submitbuttontime | bigint | YES | Phút hiện nút submit |
| quiz_allquestions | bigint | YES | Nộp sau khi làm hết câu |
| quiz_requiredquestions | bigint | YES | Nộp theo số câu tối thiểu |
| quiz_requiredquestionsPass | bigint | YES | Số câu cần hoàn thành |
| is_component_points | tinyint (def 0) | YES | Chấm theo component |
| listening/reading/writing/speaking_score | varchar | YES | Điểm / weight component |
| score_percent | varchar | YES | % điểm tối thiểu |
| cm_h5p_tool_name / cm_h5p_tool_id | varchar/bigint | YES | H5P tool |
| is_import | boolean (def 0) | NO | Flag import |
| max_attempts_per_question | varchar | YES | Giới hạn lần trả lời |
| penalty_percentage | varchar | YES | % phạt mỗi lần sai |
| show_answer_after_submit | tinyint (def 0) | NO | Hiện đáp án sau nộp |
| generation_type | varchar | YES | `random` / `repeat` |
| total_rounds / last_round | int | YES | Số vòng / vòng cuối |
| grading_type | tinyint (def 1) | NO | `1` AI · `2` Chấm tay |
| is_new_attempt | tinyint (def 0) | NO | Eligible new attempt |
| google_slide_url | text | YES | Embed Google Slide |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `parent()` / `children()` self; `hasMany` ApiMoodleEms; `hasOne` ApiMoodleConfig; `belongsTo` ConversionGroup; `belongsToMany` Classes/Teachers/Students; `hasMany` MoodleUserCompletion.

---

### `api_moodle_ems`

**Model:** `App\Models\ApiMoodleEms` | **Soft delete:** Không  
**Mô tả:** Liên kết activity (`api_moodle`) với đề EMS (`api_ems`). Hỗ trợ EMS thật + ảo (`is_virtual`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| api_moodle_id | bigint | YES | → `api_moodle.id` (thường quiz) |
| api_system_id | bigint | YES | → `api_ems.id` |
| api_system_name | varchar | YES | Tên EMS cached |
| skills_config | json | YES | Cấu hình skill/round |
| scoring_scale_id | tinyint | YES | Thang điểm; NULL = default ContestType |
| is_virtual | tinyint (def 0) | NO | `1` Virtual EMS (quiznoems) · `0` Real |
| linked_ems_id | bigint | YES | EMS liên kết (idMockContest) |
| linked_ems_name | varchar | YES | Tên EMS liên kết |
| linked_ems_skill | text | YES | JSON skills của linked EMS |
| score_above / score_below | decimal(5,2) | YES | Ngưỡng mở EMS theo avg |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ApiMoodle, ApiEms.

---

### `api_moodle_configs`

**Model:** `App\Models\ApiMoodleConfig` | **Soft delete:** Không  
**Mô tả:** Cache 1:1 dữ liệu Moodle API (module data, availability, plugin config). Unique `api_moodle_id`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| api_moodle_id | bigint unique | NO | → `api_moodle.id` |
| moodle_idnumber | varchar | YES | Category idnumber |
| moodle_description | longtext | YES | Mô tả category |
| moodle_coursecount | int (def 0) | YES | Số course trong category |
| moodle_shortname / moodle_fullname | varchar | YES | Tên course |
| moodle_summary | longtext | YES | Summary course/section |
| moodle_startdate / moodle_enddate | datetime | YES | Thời gian course |
| moodle_format | varchar(50) | YES | Course format |
| moodle_numsections | int | YES | Số section |
| moodle_enablecompletion | tinyint (def 0) | YES | Bật completion |
| moodle_overviewfiles | json | YES | Overview files |
| moodle_sectionnum | int | YES | Số thứ tự section |
| moodle_sequence | text | YES | Sequence CM ids |
| moodle_availability | json | YES | Availability rules |
| moodle_module_data | json | YES | Full CM object (completion rules…) |
| moodle_plugin_config | json | YES | Plugin config |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ApiMoodle. Dùng bởi `ActivityCompletionService` để đọc `completionpassgrade`, `gradepass`, …

---

### `api_ems`

**Model:** `App\Models\ApiEms` | **Soft delete:** Có  
**Mô tả:** Catalog đề thi EMS (central/tenant tùy deploy).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| ems_id | int | YES | ID hệ thống EMS ngoài |
| ems_name | varchar | YES | Tên đề |
| ems_type_id | bigint | YES | → ems_types (contest type) |
| rubric_template_id | bigint | YES | → rubric_templates |
| skill | bigint | YES | Skill mặc định (EMS skill id) |
| created_at / updated_at / deleted_at | timestamp | YES | |

---

### `relationship_models`

**Model:** `App\Models\RelationshipModel` | **Soft delete:** Không  
**Mô tả:** Rule liên kết / mở khóa section theo điểm trung bình.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| source_id | bigint | NO | `api_moodle.id` nguồn |
| destination_id | bigint | NO | `api_moodle.id` đích |
| type | enum(`course`,`section`) | NO | `course`=UI link · `section`=rule điểm |
| min_avg_score | decimal(5,2) | YES | Ngưỡng TB (type=section) |
| is_active | tinyint (def 1) | NO | Active flag |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ApiMoodle (source, destination).

---

### `moodle_user_completions`

**Model:** `App\Models\MoodleUserCompletion` | **Soft delete:** Không  
**Mô tả:** Trạng thái hoàn thành activity trên Moodle (sync local). Unique `(api_moodle_id, moodle_user_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| api_moodle_id | bigint | NO | → `api_moodle.id` |
| moodle_user_id | int | NO | Moodle user id (= `students.moodle_id`) |
| completed | boolean (def 0) | NO | `1` hoàn thành · `0` chưa |
| synced_at | timestamp | YES | Lần sync cuối |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ApiMoodle. Được cập nhật bởi `ActivityCompletionService::evaluateAndSync`.

---

## 6. Thi & Chấm điểm (9 bảng)

Domain lượt thi, điểm từng kỹ năng, lịch thi, attempt, chấm tay GV.

```mermaid
erDiagram
    students ||--o{ student_score : attempts
    student_score ||--o| student_score_ext : payload
    student_score ||--o{ student_exam_histories : skill_rows
    student_exam_histories ||--o| student_exam_history_ext : detail
    student_score ||--o{ student_exam_histories_teachers : teacher_grading
    student_exam_histories_teachers ||--o{ teacher_assigned_histories : assigned
    students ||--o{ user_quiz_attempts : attempts
    students ||--o{ student_exam_schedules : schedules
    students ||--o{ student_section_availability : unlocks
    api_moodle ||--o{ student_score : "quiz_id ~ moodle_id"
```

### `student_score`

**Model:** `App\Models\StudentScore` | **Soft delete:** Có | **Table:** `student_score`  
**Mô tả:** Bản ghi điểm tổng của một lượt thi (hub exam attempt).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| quiz_id | bigint | YES | Thường = `api_moodle.moodle_id` của quiz *(một số join dùng `.id` — xem ghi chú)* |
| student_id | bigint | YES | → `students.id` |
| idMockContest | varchar | YES | EMS contest / mock id |
| idHistoryContest | varchar | YES | EMS history id tổng |
| overrall_score | varchar | YES | Điểm tổng (`-1` = chưa có điểm) |
| exam_status | tinyint (def 0) | NO | Xem enum bên dưới |
| submission_status | tinyint (def 0) | NO | Xem enum |
| fe_submission_status | tinyint (def 0) | NO | Xem enum |
| score_processing_status | tinyint (def 0) | NO | Xem enum |
| manual_grading_status | tinyint (def 0) | NO | Xem enum |
| icl_score_status | tinyint (def 0) | NO | ICL: `0` đang XL · `1` hoàn tất |
| icl_grading_status | tinyint (def 0) | NO | ICL: `0` chờ · `1` đã chấm · `2` quá hạn |
| is_abnormality_checked | tinyint (def 0) | NO | Đã check bất thường |
| is_deadline_checked | tinyint (def 0) | NO | Đã chốt hạn |
| tab_switch_count | int | YES | Số lần chuyển tab |
| is_email_sent | tinyint (def 0) | NO | Đã gửi email kết quả |
| required_skills | json | YES | Skills bắt buộc (kèm `ref_` cho skill ảo) |
| score_status | int | YES | Pass/Fail tổng |
| is_virtual | tinyint (def 0) | NO | `1` Virtual EMS · `0` Real |
| abnormal_reasons | json | YES | Lý do bất thường |
| created_at / updated_at / deleted_at | timestamp | YES | |

#### Enum trạng thái `student_score`

| Field | Giá trị |
|-------|---------|
| **exam_status** | `0` Chờ nộp · `1` Đã nộp · `2` Không nộp |
| **submission_status** | `0` Trong hạn · `1` Trễ hạn |
| **fe_submission_status** | `0` Chưa nộp (FE) · `1` Dừng bài · `2` Đã nộp (FE) |
| **score_processing_status** | `0` Đang xử lý · `1` Hoàn tất · `2` Giao chấm lại · `3` Đã hủy |
| **manual_grading_status** | `0` Không xác định · `1` Chờ chấm · `2` Đã chấm · `3` Quá hạn · `4` Đã chuyển |
| **score_status** | `1` Pass/Đạt · `0` / `NULL` Fail/Chưa đạt |
| **is_virtual** | `0` Real EMS · `1` Virtual (GV chấm skill ngoài / quiznoems) |

---

### `student_score_ext`

**Model:** `App\Models\StudentScoreExt` | **Soft delete:** Không  
**Mô tả:** Payload EMS gốc / sau nộp gắn với `student_score`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_score_id | bigint | NO | → `student_score.id` |
| data_ems | longtext | YES | Payload EMS gốc |
| data_ems_after_submit | longtext | YES | Payload sau nộp / sau GV chấm |
| created_at / updated_at | timestamp | YES | |

---

### `student_exam_histories`

**Model:** `App\Models\StudentExamHistory` | **Soft delete:** Không  
**Mô tả:** Điểm từng kỹ năng / part trong một lượt thi.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_score_id | bigint | YES | → `student_score.id` |
| student_id | bigint | YES | → `students.id` |
| quiz_id | bigint | YES | Cùng ngữ nghĩa quiz với student_score |
| idbaikiemtra | varchar | YES | EMS test-part id **hoặc** `ref_…` (skill ảo kẹp) |
| ref_id | varchar | YES | Helper map điểm (legacy; dần thay bằng `ref_` trong idbaikiemtra) |
| skill | string/bigint | YES | Mã kỹ năng EMS (xem bảng dưới) |
| student_report / content / topic | text | YES | Báo cáo / nội dung / đề |
| idMockContest / idHistoryContest | varchar | YES | Contest / history id |
| ems_score | text | YES | Điểm/raw EMS |
| lms_score | varchar | YES | Điểm LMS đã quy đổi |
| contest_type_id | int | YES | Contest type (19, 21, …) |
| task_number | tinyint | YES | Writing task 1/2 |
| number_exam | int | YES | Số thứ tự exam/round |
| totalCorrectQuestions / totalAllQuestions | bigint | YES | Đúng / tổng câu |
| score_status | int | YES | Pass/Fail per skill (`1`/`0`) |
| is_virtual | tinyint (def 0) | NO | Virtual skill row |
| created_at / updated_at | timestamp | YES | |

#### Mã `skill` (EMS)

| Giá trị | Kỹ năng |
|---------|---------|
| 12 | Vocabulary |
| 13 | Listening |
| 14 | Reading |
| 15 | Writing |
| 16 | Speaking |
| 17 | Exercise |

**Lưu ý:** Skill ảo kẹp nửa thật nửa ảo dùng `idbaikiemtra` dạng `ref_...`. Skill thật có `idHistoryContest` và `idbaikiemtra` số.

---

### `student_exam_history_ext`

**Model:** `App\Models\StudentExamHistoryExt` | **Soft delete:** Không  
**Mô tả:** Payload chi tiết gắn từng dòng history.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_exam_history_id | bigint | NO | → `student_exam_histories.id` |
| status | tinyint (def 0) | NO | Trạng thái xử lý ext |
| data_ems / data_exam | longtext | YES | Payload EMS / exam detail |
| created_at / updated_at | timestamp | YES | |

---

### `student_exam_schedules`

**Model:** `App\Models\StudentExamSchedule` | **Soft delete:** Không  
**Mô tả:** Lịch thi / deadline section theo học viên (đồng bộ Zeus + vận hành).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| course_id | bigint | NO | `api_moodle.id` (course) |
| course_name | varchar | YES | Denormalized |
| section_id | bigint | NO | `api_moodle.id` (section) |
| exam_name | varchar | YES | Denormalized section name |
| student_id | bigint | NO | → `students.id` |
| product_id | bigint | YES | Category `api_moodle.id` |
| product_name | varchar | YES | Denormalized |
| zeus_start_date / zeus_deadline | datetime | YES | Mốc gốc từ Zeus |
| start_date / deadline | datetime | YES | Mốc vận hành (có thể sửa) |
| requirement_type | tinyint (def 0) | NO | `0` đúng lộ trình · `1` thi lại · `2` thi sớm |
| status | tinyint (def 1) | NO | `1` Mở · `0` Khóa lịch |
| exam_status / submission_status / score_processing_status | tinyint | YES | Mirror từ student_score |
| is_24h_reminded / is_48h_reminded | tinyint (def 0) | NO | Cờ đã nhắc lịch |
| created_at / updated_at | timestamp | YES | |

---

### `user_quiz_attempts`

**Model:** `App\Models\UserQuizAttempt` | **Soft delete:** Không  
**Mô tả:** Đếm số lần làm quiz / retake. Unique `(student_id, quiz_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_id | bigint | NO | → `students.id` |
| quiz_id | bigint | NO | `api_moodle.moodle_id` của quiz |
| attempt_count | int (def 0) | NO | Số lần đã làm |
| reset_allowance | int (def 0) | NO | Lượt admin cho làm lại |
| reset_used | int | YES | Đã dùng bao nhiêu reset |
| deadline | datetime | YES | Hạn retake |
| open_date / close_date | datetime | YES | Cửa sổ làm bài |
| is_migrated | tinyint (def 0) | NO | Flag migrate |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` Students. Full virtual quiznoems tăng attempt ở job; nửa thật nửa ảo do `mockContestSessionHistory`.

---

### `student_exam_histories_teachers`

**Model:** `App\Models\StudentExamHistoryTeacher` | **Soft delete:** Không  
**Mô tả:** Bản ghi chấm tay / phúc khảo của GV theo skill.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_score_id | bigint | YES | → `student_score.id` |
| student_exam_history_id | bigint | YES | → `student_exam_histories.id` |
| idHistoryContest | varchar | YES | Unique key EMS history |
| user_id | bigint (def 0) | NO | Teacher user_id |
| skill | bigint | YES | Skill được gán chấm |
| data_rescored | longtext | YES | JSON rubric chấm lại |
| rescored_file | varchar | YES | File GV upload |
| rescored_comment | text | YES | Nhận xét |
| overall_rescored | float | YES | Điểm GV |
| converted_score | float | YES | Điểm quy đổi |
| teacher_assign_at | timestamp | YES | Thời điểm gán |
| teacher_assessment_finish_at | timestamp | YES | Thời điểm xong |
| teacher_assessment_status | tinyint | YES | `0/1` chờ · `2` đã chấm |
| deadline | timestamp | YES | Hạn chấm |
| created_at / updated_at | timestamp | YES | |

---

### `teacher_assigned_histories`

**Model:** `App\Models\TeacherAssignedHistory` | **Soft delete:** Không  
**Mô tả:** Lịch sử gán bài cho GV (pending / completed / transfer).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_score_id | bigint | YES | → `student_score.id` |
| student_exam_histories_teacher_id | bigint | YES | → `student_exam_histories_teachers.id` |
| user_id | bigint (def 0) | NO | Người được gán |
| assigned_at / expires_at | timestamp | YES | Gán / hết hạn |
| is_active | tinyint (def 1) | NO | Lượt active hiện tại |
| status | tinyint (def 1) | NO | `1` Pending · `2` Completed · `3` Expired · `4` Transferred |
| assign_type | tinyint (def 1) | NO | `1` Chấm thường · `2` Phúc khảo |
| teacher_assessment_finish_at | datetime | YES | Xong chấm |
| transferred_at | datetime | YES | Thời điểm chuyển GV |
| data_rescored / rescored_file / rescored_comment | longtext/varchar/text | YES | Snapshot chấm |
| overall_rescored / converted_score | float | YES | Điểm |
| created_at / updated_at | timestamp | YES | |

---

### `student_section_availability`

**Model:** `App\Models\StudentSectionAvailability` | **Soft delete:** Không  
**Mô tả:** Section đã mở cho học viên sau khi đạt rule điểm. Unique `(student_id, destination_section_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_id | bigint | NO | → `students.id` |
| destination_section_id | bigint | NO | `api_moodle.id` section đích |
| avg_score | decimal(5,2) | NO | TB lúc pass rule |
| ems_id | bigint | YES | `api_moodle_ems.id` đã mở |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` Students, ApiMoodle, ApiMoodleEms.

---

## 7. Zeus Integration (2 bảng)

Domain đồng bộ session / attendance từ Zeus.

### `zeus_session_completions`

**Model:** `App\Models\ZeusSessionCompletion` | **Soft delete:** Không  
**Mô tả:** Ghi nhận giao bài / hoàn thành session Zeus theo course+section+student. Dùng cho deadline BTVN và metrics homework.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| course_id | bigint | NO | `api_moodle.id` (course) |
| section_id | bigint | NO | `api_moodle.id` (section) |
| zeus_id | bigint | YES | Zeus session/user id từ payload |
| student_id | bigint | NO | → `students.id` |
| completion_time | string (cast datetime) | NO | Thời điểm hoàn thành session |
| start_date | datetime | YES | Session start từ Zeus |
| deadline | datetime | YES | Hạn nộp (thường start + 7 ngày trong logic BTVN) |
| requirement_type | tinyint (def 0) | NO | `0` đúng lộ trình · `1` thi lại · `2` thi sớm |
| completion_state | tinyint (def 0) | NO | `0` chưa 100% · `1` fully completed |
| payload_data / response_data | longText/JSON | YES | Raw payload Zeus / response LMS |
| is_cron | tinyint (def 0) | NO | `1` đã xử lý bởi cron |
| created_at / updated_at | timestamp | YES | |

---

### `zeus_section_participants`

**Model:** `App\Models\ZeusSectionParticipant` | **Soft delete:** Không  
**Mô tả:** Danh sách điểm danh Zeus theo section. Unique `(zeus_id, section_moodle_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| zeus_id | string(64) | NO | Zeus user id |
| section_moodle_id | bigint | NO | Moodle section id |
| course_moodle_id | bigint | NO | Moodle course id |
| status | tinyint (def 0) | NO | `0` vắng · `1` có mặt |
| student_id | bigint | YES | → `students.id` (null nếu chưa map) |
| student_not_found | tinyint (def 0) | NO | `1` nếu zeus_id không có trong students |
| not_enrolled | tinyint (def 0) | NO | `1` nếu chưa enroll `course_student` |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` Students.

---

## 8. QuizNoEMS — Chấm kỹ năng ngoài EMS (5 bảng)

Domain giáo viên nhập điểm kỹ năng ngoài luồng EMS auto-grade (full virtual hoặc kẹp ảo).

```mermaid
erDiagram
    api_moodle ||--o| quiznoems_activity_config : "1:1"
    api_moodle ||--o{ quiznoems_active_skills : configures
    quiznoems_active_skills ||--o{ quiznoems_skill_field_configs : fields
    quiznoems_active_skills ||--o{ quiznoems_submissions : receives
    quiznoems_submissions }o--|| quiznoems_submission_histories : last_history_id
    api_moodle_ems ||--o{ quiznoems_active_skills : api_moodle_ems_id
```

### `quiznoems_activity_config`

**Model:** `App\Models\QuiznoemsActivityConfig` | **Soft delete:** Không  
**Mô tả:** Config 1:1 scoring scale cho activity. Unique `activity_id`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| activity_id | bigint unique | NO | → `api_moodle.id` |
| scoring_scale_id | int | YES | Thang điểm |
| created_at / updated_at | timestamp | YES | |

---

### `quiznoems_active_skills`

**Model:** `App\Models\QuiznoemsActiveSkill` | **Soft delete:** Không  
**Mô tả:** Instance kỹ năng được cấu hình trên activity (có thể nhiều instance cùng `ems_skill_id`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Skill instance PK |
| activity_id | bigint | NO | → `api_moodle.id` |
| api_moodle_ems_id | bigint | YES | → `api_moodle_ems.id`; NULL = legacy |
| ems_skill_id | int | NO | Mã skill EMS (12–16) |
| skill_label | varchar | NO | Nhãn hiển thị |
| sort_order | int (def 0) | NO | Thứ tự |
| writing_tasks | json | YES | Writing: `null` \| `[1]` \| `[2]` \| `[1,2]` |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ApiMoodle; `hasMany` fieldConfigs, submissions.

---

### `quiznoems_skill_field_configs`

**Model:** `App\Models\QuiznoemsSkillFieldConfig` | **Soft delete:** Không  
**Mô tả:** Form field config theo skill instance (score, feedback…).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| activity_id | bigint | NO | → `api_moodle.id` (denormalized) |
| skill_instance_id | bigint | NO | → `quiznoems_active_skills.id` |
| order | int (def 0) | NO | Thứ tự field |
| input_type | string(30) | NO | text/number/textarea/select/… |
| field_name | varchar | YES | UI label |
| field_key | varchar | YES | JSON key trong submission `data` |
| field_id / field_class | varchar | YES | HTML id / CSS |
| placeholder | varchar | YES | Placeholder |
| is_required | tinyint (def 0) | NO | Bắt buộc |
| required_message / default_value | varchar | YES | Validation / default |
| field_options | jsonb | YES | Options select/radio/checkbox |
| created_at / updated_at | timestamp | YES | |

---

### `quiznoems_submissions`

**Model:** `App\Models\QuiznoemsSubmission` | **Soft delete:** Không  
**Mô tả:** Điểm hiện tại theo student × skill instance. Unique `(activity_id, skill_instance_id, zeus_student_id)`.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| activity_id | bigint | NO | → `api_moodle.id` |
| api_moodle_ems_id | bigint | YES | EMS link |
| skill_instance_id | bigint | NO | → active skill |
| ems_skill_id | bigint | YES | Denormalized skill |
| skill_label | varchar | YES | Denormalized label |
| zeus_student_id | string(100) | NO | `students.zeus_id` |
| zeus_teacher_id | string(100) | NO | Teacher Zeus id |
| data | json | NO | Field values theo `field_key` |
| submitted_at | timestamp | YES | Thời điểm submit |
| last_history_id | bigint | YES | → latest history |
| created_at / updated_at | timestamp | YES | |

---

### `quiznoems_submission_histories`

**Model:** `App\Models\QuiznoemsSubmissionHistory` | **Soft delete:** Không  
**Mô tả:** Snapshot mỗi lần GV submit (đa skill + overall_score).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| activity_id | bigint | NO | Activity |
| zeus_student_id / zeus_teacher_id | string(100) | NO | Student / teacher Zeus id |
| skills_snapshot | json | NO | Snapshot toàn bộ skills |
| overall_score | decimal(5,2) | YES | Điểm trung bình (đã round theo scale nếu có) |
| scoring_scale_id | int | YES | Scale snapshot |
| submitted_at | timestamp | YES | Thời điểm submit |
| created_at / updated_at | timestamp | YES | |

---

## 9. ICL & Score Conversion (6 bảng)

Domain đẩy điểm sang ICL và quy đổi thang điểm (IELTS band…).

```mermaid
erDiagram
    student_score ||--o| student_score_icl : snapshot
    student_score ||--o{ student_score_icl_histories : history
    score_bands ||--o{ score_range_details : ranges
    score_range_details ||--o{ score_conversions : maps
    conversion_groups ||--o{ score_conversions : group
    api_moodle }o--|| conversion_groups : conversion_group_id
```

### `student_score_icl`

**Model:** `App\Models\StudentScoreICL` | **Soft delete:** Không | **Table:** `student_score_icl`  
**Mô tả:** Snapshot điểm hiện tại gửi/đồng bộ ICL (upsert theo `student_score_id`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_score_id | bigint | NO | → `student_score.id` |
| course_id | bigint | NO | `api_moodle.id` course |
| section_id | bigint | NO | `api_moodle.id` section |
| student_id | bigint | YES | → `students.id` |
| zeus_id | bigint | NO | Zeus id (0 nếu thiếu) |
| overall_score | string | YES | Điểm tổng cho ICL |
| skill_scores | json | YES | Chi tiết từng skill |
| created_at / updated_at | timestamp | YES | |

Ghi bởi `IclScoreService::saveToIclScoreTable` (bỏ qua khi overall null/`-1`).

---

### `student_score_icl_histories`

**Model:** `App\Models\StudentScoreICLHistory` | **Soft delete:** Không  
**Mô tả:** Lịch sử append-only khi điểm ICL thay đổi.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| student_score_id | bigint | NO | → `student_score.id` |
| student_id | bigint | YES | → `students.id` |
| zeus_id | bigint (def 0) | YES | Zeus id snapshot |
| course_id / section_id | bigint | YES | Course / section snapshot |
| overall_score | string | YES | Điểm tại thời điểm ghi |
| skill_scores | json | YES | Skill detail snapshot |
| created_at / updated_at | timestamp | YES | |

---

### `conversion_groups`

**Model:** `App\Models\ConversionGroup` | **Soft delete:** Không  
**Mô tả:** Nhóm quy đổi điểm (gắn `api_moodle.conversion_group_id`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| name | varchar | NO | vd: `MT1`, `MT2-MT3`, `FN` |
| category | varchar | NO | vd: `Introduction`, `Foundation` |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `hasMany` ScoreConversion.

---

### `score_bands`

**Model:** `App\Models\ScoreBand` | **Soft delete:** Không  
**Mô tả:** Thang điểm gốc (vd: Thang 10).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| name | varchar | NO | Tên thang |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `hasMany` ScoreRangeDetail.

---

### `score_range_details`

**Model:** `App\Models\ScoreRangeDetail` | **Soft delete:** Không  
**Mô tả:** Khoảng điểm trên một band (`min_score`–`max_score`).

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| score_band_id | bigint | NO | → `score_bands.id` |
| min_score | decimal(8,2) | NO | Cận dưới (inclusive) |
| max_score | decimal(8,2) | NO | Cận trên (inclusive) |
| created_at / updated_at | timestamp | YES | |

**Quan hệ:** `belongsTo` ScoreBand; `hasMany` ScoreConversion.

---

### `score_conversions`

**Model:** `App\Models\ScoreConversion` | **Soft delete:** Không  
**Mô tả:** Map khoảng điểm → `converted_value` theo group.

| Cột | Kiểu | Nullable | Ý nghĩa |
|-----|------|----------|---------|
| id | bigint PK | NO | Khóa chính |
| range_detail_id | bigint | NO | → `score_range_details.id` |
| group_id | bigint | NO | → `conversion_groups.id` |
| converted_value | decimal(8,2) | NO | Điểm quy đổi |
| created_at / updated_at | timestamp | YES | |

**Logic:** tìm row `group_id` + `min ≤ overall ≤ max`; không match thì giữ điểm gốc (`CommonEms::getConvertedOverallScore`).

---

## 10. Sơ đồ ER tổng hợp

### 10.1 Enrollment

```mermaid
erDiagram
    users ||--o| students : user_id
    users ||--o| teachers : user_id
    students ||--o{ course_student : enrolls
    course_student }o--|| api_moodle : course_id
    course_student }o--|| classes : class_id
    teachers ||--o{ course_teacher : teaches
    classes ||--o{ class_course : links
    class_course }o--|| api_moodle : course_id
    hocmai_tenants ||--o{ user_tenants : has
    user_tenants }o--|| users : user_id
```

### 10.2 Exam & grading

```mermaid
erDiagram
    students ||--o{ student_score : attempts
    student_score ||--o{ student_exam_histories : skills
    student_score ||--o| student_score_ext : payload
    student_score ||--o{ student_exam_histories_teachers : grading
    student_exam_histories_teachers ||--o{ teacher_assigned_histories : assign
    student_score ||--o| student_score_icl : icl_snapshot
    api_moodle ||--o{ student_score : quiz
    api_moodle ||--o{ api_moodle_ems : ems_links
    api_moodle_ems }o--|| api_ems : catalog
```

### 10.3 Zeus homework + QuizNoEMS

```mermaid
erDiagram
    students ||--o{ zeus_session_completions : sessions
    api_moodle ||--o{ zeus_session_completions : course_section
    api_moodle ||--o{ quiznoems_active_skills : skills
    quiznoems_active_skills ||--o{ quiznoems_submissions : scores
    quiznoems_submissions }o--o| student_exam_histories : "ref_ sync via ICL grading"
    students ||--o{ moodle_user_completions : completion
    api_moodle ||--o{ moodle_user_completions : activity
```

### 10.4 Luồng nghiệp vụ chính (tóm tắt)

1. **Enrollment:** User/Students → `course_student` → `api_moodle` (course)
2. **Exam Real EMS:** Nộp bài EMS → `student_score` + `student_exam_histories` → (optional) chấm tay → ICL
3. **Exam Virtual / QuizNoEMS:** GV submit → `quiznoems_submissions` → Job → `student_score` / `processGradingRouting` (ref_ skills)
4. **Zeus BTVN:** Zeus webhook → `zeus_session_completions` → deadline / metrics
5. **Completion:** Score + rules (`api_moodle_configs.moodle_module_data`) → `moodle_user_completions` + Moodle API

---

## 11. Ghi chú schema

1. **Tenant drift:** Schema đầy đủ nhất thường ở tenant như `lms_tenant_icc`. Default DB / tenant khác có thể thiếu cột/bảng (alter chưa chạy).
2. **`quiz_id` semantics:** Phần lớn code dùng `api_moodle.moodle_id`; một số join dùng `api_moodle.id`. Kiểm tra theo call site.
3. **Logical FK:** Hầu hết quan hệ không có DB foreign key — ràng buộc ở tầng ứng dụng.
4. **Schema gaps:** Một số cột (vd. `score_status` trên `student_exam_histories`) có trên DB/model nhưng migration ADD COLUMN có thể không đầy đủ trong repo.
5. **`student_exam_histories_teachers`:** Không thấy CREATE migration rõ trong repo — có thể tạo ngoài / legacy; chỉ còn ALTER migrations.
6. **Bảng loại trừ theo yêu cầu:** `student_class`, `courses`, `student_activity_progress` — cấu trúc khóa học lấy từ cây `api_moodle`.

---

*Cập nhật theo models + migrations tại thời điểm tạo tài liệu. Khi thêm cột/migration mới, cần đồng bộ lại file này.*
