---

# Tài liệu Luồng Xử Lý: IclScoreService

## 1. Tổng quan (Overview)

`IclScoreService` chịu trách nhiệm quản lý luồng chấm điểm, bóc tách điểm chi tiết từ các kỹ năng (Writing, Reading, Listening, Speaking), tính toán điểm tổng (Overall Score) dựa trên các thang điểm (Scoring Scale / Contest Type) và đồng bộ dữ liệu sang bảng trung gian (`student_score_icl`) để hệ thống ICL quét.

---

## 2. Luồng Điều Phối Chính (Main Routing Workflow)

**Hàm:** `IclScoreService::processGradingRouting(int $quizId, int $studentId, int $apiMoodleEmsId, string $tenantConnection)`

Đây là điểm entry-point để quyết định bài thi sẽ đi theo nhánh xử lý nào.

1. **Bắt đầu:** Tìm kiếm bản ghi `StudentScore` mới nhất dựa vào `quiz_id` và `student_id`.
2. **Kiểm tra tồn tại:**
   * **NẾU** không tìm thấy -> Log info & Kết thúc (`return false`).
   * **NẾU** tìm thấy -> Chuyển sang bước 3.

3. **Phân luồng dựa trên loại bài thi (`is_virtual`):**
   * **Trường hợp A (`is_virtual == 1`):** Hệ thống ảo hoàn toàn -> Gọi luồng **[Luồng No EMS]** (`IclScoreService::gradeNoEMS($studentScore, $tenantConnection)`).
   * **Trường hợp B (`is_virtual != 1`):** Hệ thống thật (có thể kẹp ảo) -> Gọi luồng **[Luồng Real EMS]** (`IclScoreService::gradeRealEMS($studentScore, $apiMoodleEmsId, $tenantConnection)`).

### Sơ đồ luồng điều phối chính

![Sơ đồ luồng IclScoreService](schema-db.png)

---

## 3. Chi tiết các luồng rẽ nhánh (Sub-Workflows)

### 3.1. Luồng No EMS (Toàn bộ kỹ năng ảo)

**Hàm:** `IclScoreService::gradeNoEMS($studentScore, $tenantConnection)`

1. **Chốt chặn:** Gọi `IclScoreService::checkAllSkillsGraded($studentScoreId, $tenantConnection)`.
   * *Điều kiện:* Tất cả các dòng trong `StudentExamHistory` chưa có điểm LMS (`lms_score == null`) thì **PHẢI** nằm trong danh sách đã được giáo viên chấm (`StudentExamHistoryTeacher` có `overall_rescored`).
   * **NẾU** không thỏa mãn (có dòng chưa ai chấm) -> Kết thúc (`return false`).

2. **Tính toán điểm:** Gọi `IclScoreService::calculateNoEMSScore($studentScore, $tenantConnection)`.
   * Lấy điểm giáo viên chấm hoặc lms_score.
   * Tạo mảng `skills_detail`.
   * Giữ nguyên `overall_score` từ `StudentScore`.

3. **Đồng bộ ICL:** Gọi `IclScoreService::saveToIclScoreTable($studentScore, $skillsDetail, $overallScore, $tenantConnection)` để lưu `skills_detail` và `overall_score` sang ICL.
4. **Kết thúc:** Trả về `true`.

### 3.2. Luồng Real EMS (Router cho hệ thống thật)

**Hàm:** `IclScoreService::gradeRealEMS($studentScore, $apiMoodleEmsId, $tenantConnection)`

1. **Phân tích yêu cầu:** Giải mã chuỗi JSON `required_skills` từ `StudentScore`.
2. **Kiểm tra kỹ năng kẹp ảo (`ref_`):** Quét danh sách kỹ năng tìm `idBaiKiemTra` bắt đầu bằng `ref_`.
3. **Phân luồng:**
   * **Trường hợp A (Không có `ref_`):** Bài thi 100% qua EMS -> Gọi chốt chặn `IclScoreService::checkEnoughRequiredSkills($studentScore, $tenantConnection)`.
     * *NẾU PASS:* Gọi **[Luồng Full Máy]** (`IclScoreService::gradeAutoAll($studentScore, $tenantConnection)`).
     * *NẾU FAIL:* Kết thúc (`return false`).
   * **Trường hợp B (Có `ref_`):** Bài thi Mix (Nửa người nửa máy) -> Gọi **[Luồng Nửa Người Nửa Máy]** (`IclScoreService::gradeAutoManual($studentScore, $apiMoodleEmsId, array $refSkills, string $tenantConnection)`).

### 3.3. Luồng Full Máy (100% Real EMS)

**Hàm:** `IclScoreService::gradeAutoAll($studentScore, $tenantConnection)`

1. **Tính toán điểm:** Gọi `IclScoreService::calculateRealEMSScore($studentScore, $tenantConnection)`. *(Xem chi tiết tại mục 4.1)*
2. **Đồng bộ ICL:** Gọi `IclScoreService::saveToIclScoreTable($studentScore, $skillsDetail, $overallScore, $tenantConnection)`.
3. **Kết thúc:** Trả về `true`.

### 3.4. Luồng Nửa Người Nửa Máy (Hybrid - Trộn kỹ năng ngoài)

**Hàm:** `IclScoreService::gradeAutoManual($studentScore, $apiMoodleEmsId, array $refSkills, string $tenantConnection)`

1. **Đồng bộ dữ liệu bài ảo:**
   * Tìm `api_moodle_id`.
   * Map `test_format` với `idBaiKiemTra` (`ref_...`).
   * Truy vấn bảng `QuiznoemsSubmission` xem học viên đã nộp đủ số môn ảo yêu cầu chưa.
   * **NẾU CHƯA ĐỦ:** Log info & Kết thúc (`return false`).
   * **NẾU ĐỦ:** Cập nhật/Thêm mới (`updateOrCreate`) các dòng này vào bảng `StudentExamHistory`.

2. **Tính toán điểm chi tiết:** Gọi `IclScoreService::calculateRealEMSScore($studentScore, $tenantConnection)` quét toàn bộ `StudentExamHistory` (bao gồm dòng gốc & dòng `ref_` vừa insert). Trả về `skills_detail`.
3. **Tính điểm tổng (Rule-Based):** Lấy mảng điểm thuần túy và gọi `IclScoreService::calculateOverallScoreBasedOnRules(array $skillScores, int $quizId, int $contestTypeId, string $tenantConnection)` *(Xem chi tiết tại mục 4.2)*. Cập nhật `overrall_score` mới vào `StudentScore`.
4. **Đồng bộ ICL:** Gọi `IclScoreService::saveToIclScoreTable($studentScore, $skillsDetail, $overallScore, $tenantConnection)`.
5. **Kết thúc:** Trả về `true`.

---

## 4. Các Logic Xử Lý Cốt Lõi (Core Logics)

### 4.1. Logic Bóc Tách Điểm (IclScoreService::calculateRealEMSScore($studentScore, $tenantConnection))

1. **Phân loại dữ liệu:** Truy xuất toàn bộ lịch sử thi (`StudentExamHistory`) và điểm do giáo viên chấm (`StudentExamHistoryTeacher`). Tách riêng bài Writing (`EMS_SKILL_WRITING`) và Các môn khác.
2. **Xử lý Writing:**
   * **NẾU** có giáo viên chấm: Lấy `overall_rescored` và `rescored_comment`.
   * **NẾU KHÔNG CÓ** giáo viên chấm: Dựa vào hệ thống. Gom điểm Task 1, Task 2 -> Gọi `IclScoreService::calculateWritingScore($contestType, $scoreT1, $scoreT2)` (Trung bình cộng, ưu tiên T1/T2, hoặc công thức IELTS 27). Ghép nối `student_report`.

3. **Xử lý môn khác:** Duyệt từng kỹ năng, lấy điểm giáo viên (ưu tiên) hoặc điểm LMS.
4. **Tạo Key duy nhất:** Xử lý trùng lặp kỹ năng bằng hàm `IclScoreService::generateUniqueSkillKey($baseSkillName, &$skillKeyCounts)` (vd: `reading`, `reading_2`).
5. **Lấy Overall Score:** Mặc định lấy từ `StudentScore`, nhưng ưu tiên ghi đè nếu tồn tại `data_ems_after_submit` trong `StudentScoreExt`.

### 4.2. Logic Tính Điểm Tổng theo Rules (IclScoreService::calculateOverallScoreBasedOnRules(array $skillScores, int $quizId, int $contestTypeId, string $tenantConnection))

1. **Tính cơ bản:** Tính điểm trung bình cộng (Average) của tất cả kỹ năng.
2. **Tra cứu Scoring Scale:** Tìm `scoring_scale_id` từ `ApiMoodleEms`.
   * **NẾU** thuộc nhóm [17, 18, 19, 20, 21, 22]: Đưa qua `ScoringScaleFactory` để tính công thức riêng.

3. **Tra cứu Contest Type (Nếu không có Scoring Scale):**
   * Types [19, 21, 23]: Lấy điểm Average làm tròn.
   * Type 35 (Placement): Gọi `ContestService::roundPlacementOverall()`.
   * Type 25 (IELTS thang 9): Gọi `ContestService::roundIELTSScore()`.
   * Type 27 (IELTS quy tắc phức tạp): Gọi `ContestService::roundIeltsScore()`.

4. **Fallback:** Nếu không lọt vào rule nào, lấy điểm trung bình cộng làm tròn 2 chữ số.

### 4.3. Logic Cập Nhật Trạng Thái ICL (IclScoreService::checkAndUpdateIclStatus($idHistoryContest, $tenantConnection))

Hàm này chạy độc lập để xét duyệt trạng thái học tập.

1. Tìm `StudentScore` và danh sách `StudentExamHistory`.
2. **Set `icl_score_status`:**
   * Bằng `1` (True) **NẾU** tất cả các kỹ năng đều có `lms_score`. Ngược lại là `0`.
3. **Set `icl_grading_status`:**
   * **TH1 (Có bài thi Clone - không có idHistoryContest):** Bằng `1` nếu tất cả clone có `lms_score`, ngược lại `0`.
   * **TH2 (Không có clone):** Quét bảng `StudentExamHistoryTeacher`. Bằng `0` nếu tồn tại bản ghi đã gán cho giáo viên (`user_id != 0`) nhưng chưa chấm (`overall_rescored == null`). Ngược lại là `1`.

4. **Lưu:** Update vào DB.

### 4.4. Logic Lưu Trữ ICL (IclScoreService::saveToIclScoreTable($studentScore, $skillsDetail, $overallScore, $tenantConnection))

1. Lấy thông tin `zeus_id` từ bảng `Students`.
2. Truy vết ngược cây Moodle: `quizId` -> `Parent (Section)` -> `Parent (Course)` để lấy `section_id` và `course_id`.
3. Chuẩn bị mảng data gồm: IDs, `overall_score`, `skill_scores` (dạng JSON).
4. **Insert** 1 dòng mới vào bảng lịch sử: `StudentScoreICLHistory`.
5. **Update/Insert** (Upsert) vào bảng chính: `StudentScoreICL` (để ICL luôn quét dòng mới nhất).

---

## 5. Danh Sách Bảng Dữ Liệu (Database Tables)

### 5.1. Bảng tên, vai trò & mối quan hệ

| Tên bảng | Nhóm | Vai trò trong service | Quan hệ với bảng khác |
|---|---|---|---|
| `student_score` | Core | Bản ghi điểm chính của học viên. Entry-point của toàn bộ luồng (`quiz_id`, `student_id`). Lưu `overall_score`, `required_skills`, `is_virtual`. | 1 → N `student_exam_history`; 1 → 1 `student_score_ext`; 1 → N `student_score_icl`; FK → `api_moodle_ems` |
| `student_exam_history` | History | Lịch sử thi từng kỹ năng (Writing, Reading, Listening, Speaking). Chứa `lms_score`, `test_format`, `idBaiKiemTra`. Cơ sở để bóc tách điểm chi tiết. | N → 1 `student_score`; 1 → N `student_exam_histories_teachers`; clone → `idHistoryContest` |
| `student_exam_histories_teachers` | History | Điểm giáo viên chấm. Lưu `overall_rescored`, `rescored_comment`, `user_id`. Có ưu tiên cao hơn `lms_score`. | N → 1 `student_exam_history` |
| `quiznoems_submissions` | Ref | Bài nộp của học viên ở môn ảo (`ref_...`) trong luồng Hybrid. Kiểm tra đã nộp đủ số môn ảo yêu cầu chưa. | N → 1 `student_score` (qua `api_moodle_id`) |
| `api_moodle_ems` | Ref | Cấu hình bài thi EMS thật. Tra cứu `scoring_scale_id` và `contest_type` để tính overall score. | 1 → N `student_score`; FK → `ScoringScale` |
| `students` | Ref | Thông tin học viên. Lấy `zeus_id` dùng khi ghi sang bảng ICL. | 1 → N `student_score` |
| `student_score_ext` | Ext | Dữ liệu mở rộng của điểm. Lưu `data_ems_after_submit` — nếu có sẽ ghi đè `overall_score` lấy từ `student_score`. | 1 → 1 `student_score` |
| `student_score_icl` | ICL | Bảng chính ICL quét. Upsert sau mỗi lần tính điểm xong. Chứa `overall_score` và `skill_scores` JSON mới nhất. | N → 1 `student_score`; FK → `students` (zeus_id); FK → Section/Course (Moodle) |
| `student_score_icl_histories` | ICL | Lịch sử từng lần đồng bộ sang ICL. Insert 1 dòng mới mỗi lần `saveToIclScoreTable()` chạy — không bao giờ update. | N → 1 `student_score_icl` |

### 5.2. Luồng đọc / ghi theo từng sub-workflow

| Luồng | Bảng đọc (READ) | Bảng ghi (WRITE) |
|---|---|---|
| `gradeNoEMS` — Toàn bộ ảo | `student_score`, `student_exam_history`, `student_exam_histories_teachers` | `student_score_icl`, `student_score_icl_histories` |
| `gradeAutoAll` — 100% Real EMS | `student_score`, `student_exam_history`, `student_exam_histories_teachers`, `api_moodle_ems`, `student_score_ext` | `student_score_icl`, `student_score_icl_histories` |
| `gradeAutoManual` — Hybrid | `student_score`, `student_exam_history`, `student_exam_histories_teachers`, `quiznoems_submissions`, `api_moodle_ems`, `student_score_ext`, `students` | `student_exam_history` (upsert ref_), `student_score` (overall), `student_score_icl`, `student_score_icl_histories` |
| `checkAndUpdateIclStatus` — Cập nhật trạng thái | `student_score`, `student_exam_history`, `student_exam_histories_teachers` | `student_score` (`icl_score_status`, `icl_grading_status`) |

