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

## 1. Tổng quan (Overview)

`LmsExamWorkflowService` quản lý toàn bộ vòng đời lịch thi của học viên: tạo/cập nhật lịch thi, đóng lịch, hủy kết quả, cấp thêm lượt làm bài và mở lịch thi theo luồng Section Availability. Service giao tiếp với hệ thống LMS qua API đồng bộ và ghi log lịch sử mọi thao tác.

---

## 2. Hàm Bổ Trợ Dùng Chung

### `LmsExamWorkflowService::syncStudentListAndExamToLms(int $examId, array $studentIds)`

**Phạm vi:** `private`

Đồng bộ danh sách học viên và thông tin bài thi sang hệ thống LMS. Được gọi đầu tiên trong các hàm thao tác lượt thi trước khi xử lý nghiệp vụ nội bộ.

---

## 3. Các Luồng Nghiệp Vụ Chính

### 3.1. Tạo Lịch Thi Hàng Loạt

**Hàm:** `LmsExamWorkflowService::generateSchedulesForStudents(int $sectionId, array $studentIds)`

Tạo hoặc cập nhật lịch thi cho danh sách học viên theo một section. Sau khi hoàn thành tự động cấp thêm lượt làm bài.

**Luồng xử lý:**

1. **Kiểm tra đầu vào:** Nếu `studentIds` rỗng thì thoát ngay.
2. **Xác định tenant connection** qua `HelperTenant::getCurrentTenantConnection()`. Ném exception nếu không tìm được.
3. **Truy xuất cây Moodle:**
   - Tìm `section` trong `ApiMoodle` theo `sectionId` và `moodle_type = 'section'`.
   - Tìm `course` cha qua `section->parent_id` và `moodle_type = 'course'`.
   - Ném exception nếu không tìm thấy section hoặc course hợp lệ.
4. **Chuẩn bị thông tin chung:**
   - `start_date` = thời điểm hiện tại (timezone `Asia/Ho_Chi_Minh`).
   - `deadline` = `start_date + 7 ngày`.
   - `requirement_type` = `0` (fix cứng).
   - Lấy `actorId` từ user CMS đang đăng nhập qua `HelperTenant::getUserFromTenant()`.
5. **Vòng lặp từng học viên:**
   - Kiểm tra bản ghi `StudentExamSchedule` cũ theo `(student_id, course_id, section_id)`.
   - Ghi nhớ giá trị cũ (`old_start_date`, `old_deadline`, `old_requirement_type`, `old_status`) để lưu history. Xác định `action = 'create'` hoặc `'update'`.
   - Thực hiện `updateOrCreate` vào `StudentExamSchedule` với `status = 1` (mở).
   - Ghi 1 dòng lịch sử vào `StudentExamScheduleHistory`.
6. **Cấp thêm lượt làm bài:** Gọi `grantQuizAttempts(sectionId, studentIds, tenantConnection)`.

---

### 3.2. Cấp Thêm Lượt Làm Bài

**Hàm:** `LmsExamWorkflowService::grantQuizAttempts(int $sectionId, array $studentIds, string $tenantConnection)`

Cộng dồn `reset_allowance` cho những học viên đã từng thi (`attempt_count > 0`). Chỉ áp dụng cho các bài quiz là con trực tiếp của section.

**Luồng xử lý:**

1. Nếu `studentIds` rỗng thì thoát.
2. Truy vấn `ApiMoodle` lấy tất cả `moodle_id` của các bài quiz con thuộc `sectionId` (`moodle_type = 'quiz'`). Nếu không có quiz nào thì thoát.
3. Thực thi **1 lệnh UPDATE duy nhất** trên `UserQuizAttempt`:
   - Lọc: `quiz_id IN (quizMoodleIds)`, `student_id IN (studentIds)`, `attempt_count > 0`.
   - Hành động: `INCREMENT reset_allowance + 1`, cập nhật `updated_at`.

> **Lưu ý:** Phiên bản cũ (đã comment) xử lý insert/update từng bản ghi riêng lẻ — đã được thay thế bằng 1 lệnh `increment` hàng loạt để tối ưu hiệu năng.

---

### 3.3. Đóng Lịch Thi & Hủy Kết Quả

**Hàm:** `LmsExamWorkflowService::closeActiveAttemptAndCancelResults(int $sectionId, array $studentIds, bool $cancelResult = false, $studentScoreId = null)`

Điểm entry-point để đóng lịch thi đang mở. Có thể kết hợp hủy kết quả tùy theo cờ `$cancelResult`.

**Luồng xử lý:**

1. Nếu `studentIds` rỗng thì thoát.
2. Xác định `tenantConnection`.
3. Gọi `closeActiveAttempt()` để đóng lịch.
4. Nếu `cancelResult == true`: Gọi tiếp `cancelResults()` để xóa mềm điểm số.

---

#### 3.3.1. Đóng Lịch Thi

**Hàm:** `LmsExamWorkflowService::closeActiveAttempt(int $sectionId, array $studentIds, string $tenantConnection)`

1. Lấy `actorId` từ user CMS đang đăng nhập.
2. Truy vấn `StudentExamSchedule` theo `(section_id, student_id)` với điều kiện `status != 0` (chỉ lấy lịch đang mở, tránh ghi log trùng).
3. Với mỗi lịch tìm thấy:
   - Ghi nhớ `old_status`.
   - Update `status = 0` (đóng).
   - Ghi 1 dòng lịch sử vào `StudentExamScheduleHistory` với `action = 'close'`. Giữ nguyên các trường ngày tháng.

---

#### 3.3.2. Hủy Kết Quả (Xóa Mềm)

**Hàm:** `LmsExamWorkflowService::cancelResults(int $sectionId, array $studentIds, string $tenantConnection, array $excludeScoreIds = [])`

1. Truy vấn `ApiMoodle` lấy tất cả `moodle_id` quiz con của section. Nếu không có thì thoát.
2. Thực hiện **xóa mềm** (`delete()`) trên `StudentScore`:
   - Lọc: `quiz_id IN (quizIds)`, `student_id IN (studentIds)`, `deleted_at IS NULL`.
   - Nếu có `excludeScoreIds`: bỏ qua các bản ghi đó (`whereNotIn`).

---

### 3.4. Mở Lịch Thi Theo Section Availability (Idempotent)

**Hàm:** `LmsExamWorkflowService::generateScheduleForStudentWithEms(int $destinationSectionId, int $studentId, int $apiMoodleEmsId, string $tenantConnection)`

Mở đúng **1 lịch thi** cho **1 học viên** trong section đích, theo kết quả routing từ score range. Khác với `generateSchedulesForStudents` mở đồng loạt cho nhiều học viên.

**Luồng xử lý:**

1. Truy xuất cây Moodle: tìm `section` và `course` cha. Trả về `false` nếu không hợp lệ.
2. **Kiểm tra idempotency:** Nếu đã tồn tại `StudentExamSchedule` với `(student_id, course_id, section_id, status = 1)` thì log và trả về `false` — không làm gì thêm.
3. Chuẩn bị: `start_date` = now, `deadline` = now + 7 ngày, lấy `actorId`.
4. `updateOrCreate` vào `StudentExamSchedule` với `status = 1`, `requirement_type = 0`.
5. Ghi lịch sử vào `StudentExamScheduleHistory` với `action = 'availability_open'`.
6. Gọi `grantQuizAttempts()` cho học viên này.
7. Log thành công và trả về `true`.

---

## 4. Các Bảng Dữ Liệu (Database Tables)

### 4.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 |
|---|---|---|---|
| `api_moodle` | Ref | Cây cấu hình Moodle (course, section, quiz). Tra cứu cha-con để xác định scope bài thi. Lấy `moodle_id` của các quiz con. | Tự quan hệ: `parent_id → id`; 1 → N `student_exam_schedules` (qua section); 1 → N `user_quiz_attempts` (qua quiz) |
| `student_exam_schedules` | Core | Lịch thi của học viên theo từng section. Lưu `start_date`, `deadline`, `requirement_type`, `status`. Entry-point đóng/mở lịch. | N → 1 `api_moodle` (section); N → 1 `api_moodle` (course); N → 1 `students`; 1 → N `student_exam_schedule_histories` |
| `student_exam_schedule_histories` | History | Audit log mọi thao tác lên lịch thi: create, update, close, availability_open. Ghi `old_*` và `new_*` cho mọi trường. | N → 1 `student_exam_schedules`; N → 1 `users` (actor) |
| `user_quiz_attempts` | Core | Quản lý số lượt làm bài của học viên theo quiz. Trường `reset_allowance` được cộng dồn khi cấp thêm lượt. Chỉ cập nhật khi `attempt_count > 0`. | N → 1 `api_moodle` (quiz, qua `quiz_id = moodle_id`); N → 1 `students` |
| `student_score` | Core | Bảng điểm của học viên. Bị xóa mềm (`deleted_at`) khi hủy kết quả. Lọc qua `quiz_id` thuộc section cần hủy. | N → 1 `api_moodle` (quiz); N → 1 `students` |

### 4.2. Luồng đọc / ghi theo từng hàm

| Hàm | Bảng đọc (READ) | Bảng ghi (WRITE) |
|---|---|---|
| `generateSchedulesForStudents` | `api_moodle` (section, course), `student_exam_schedules` (kiểm tra cũ) | `student_exam_schedules` (upsert), `student_exam_schedule_histories` (insert), `user_quiz_attempts` (increment) |
| `grantQuizAttempts` | `api_moodle` (quiz), `user_quiz_attempts` | `user_quiz_attempts` (increment `reset_allowance`) |
| `closeActiveAttempt` | `student_exam_schedules` | `student_exam_schedules` (update status=0), `student_exam_schedule_histories` (insert) |
| `cancelResults` | `api_moodle` (quiz), `student_score` | `student_score` (soft delete) |
| `generateScheduleForStudentWithEms` | `api_moodle` (section, course), `student_exam_schedules` (idempotency check) | `student_exam_schedules` (upsert), `student_exam_schedule_histories` (insert), `user_quiz_attempts` (increment) |