# Phân Hệ Quản Lý Danh Sách Học Viên Đến Lịch Thi (Web Exam Schedule)
## 02. Quy Tắc Nghiệp Vụ & Logic Chi Tiết (Business Logic & Rules)

---

### 1. Phân Loại Yêu Cầu Lịch Thi (`requirement_type`)

Hệ thống phân định rõ 3 loại yêu cầu lịch thi để quản lý đúng luồng nghiệp vụ của học viên:

| Giá trị (`requirement_type`) | Tên hiển thị | Ý nghĩa & Hành vi nghiệp vụ |
| :---: | :--- | :--- |
| **0** | **Thi đúng lộ trình** | Lịch thi chuẩn theo kế hoạch học tập ban đầu (thường được đồng bộ tự động từ hệ thống Zeus/ICL sang). Không yêu cầu cấp thêm lượt làm bài. |
| **1** | **Thi lại** | Học viên có nhu cầu thi lại một bài kiểm tra/mocktest đã hoàn thành trước đó. Hệ thống **bắt buộc kích hoạt logic cấp thêm lượt làm bài** (`processRetakeAllowance`) trên Moodle/LMS Core. |
| **2** | **Thi sớm** | Học viên được đặc cách mở bài thi trước thời hạn lộ trình thông thường. Hệ thống thiết lập lịch thi mới theo thời gian chỉ định mà không tăng lượt thi lại. |

---

### 2. Quản Lý Trạng Thái Lịch Thi & Phiên Bản (`status`)

Cột `status` trong bảng `student_exam_schedules` dùng để kiểm soát tính hiệu lực của lịch thi:

| Trạng thái (`status`) | Ý nghĩa | Hành vi hệ thống |
| :---: | :--- | :--- |
| **1** | **Mở (Active)** | Lịch thi đang có hiệu lực. Học viên được phép vào phòng thi và làm bài khi trong khung thời gian hợp lệ. |
| **0** | **Đóng (Inactive / Closed)** | Lịch thi đã bị vô hiệu hóa (do hành động **Đổi lịch thi** đóng bản ghi cũ). Bản ghi này được giữ lại cho mục đích đối soát và audit trail. |

---

### 3. Logic Xác Định Thời Gian Mở & Đóng Bài Thi Thực Tế (Effective Time Calculation)

Do hệ thống quản lý song song hai nguồn thời gian:
1. **Thời gian gốc từ Zeus**: `zeus_start_date` và `zeus_deadline` (do hệ thống Zeus đồng bộ về).
2. **Thời gian điều chỉnh nội bộ LMS**: `start_date` và `deadline` (do cán bộ vận hành thiết lập khi tạo/đổi/gia hạn lịch).

Khi truy vấn lọc và hiển thị, thời gian thực tế áp dụng cho học viên được tính toán theo công thức:

- **Thời gian mở bài thực tế (`effective_start_date`)**:
  $$\text{effective\_start\_date} = \begin{cases} \min(\text{zeus\_start\_date}, \text{start\_date}) & \text{khi cả 2 trường đều có dữ liệu} \\ \text{COALESCE}(\text{zeus\_start\_date}, \text{start\_date}) & \text{khi 1 trong 2 trường NULL} \end{cases}$$

- **Thời gian đóng bài thực tế (`effective_deadline`)**:
  $$\text{effective\_deadline} = \begin{cases} \max(\text{zeus\_deadline}, \text{deadline}) & \text{khi cả 2 trường đều có dữ liệu} \\ \text{COALESCE}(\text{zeus\_deadline}, \text{deadline}) & \text{khi 1 trong 2 trường NULL} \end{cases}$$

*Mã SQL thực tế:*
```sql
-- Lọc theo thời gian mở bài
CASE 
    WHEN ses.zeus_start_date IS NOT NULL AND ses.start_date IS NOT NULL 
    THEN LEAST(ses.zeus_start_date, ses.start_date) 
    ELSE COALESCE(ses.zeus_start_date, ses.start_date) 
END

-- Lọc theo thời gian đóng bài
CASE 
    WHEN ses.zeus_deadline IS NOT NULL AND ses.deadline IS NOT NULL 
    THEN GREATEST(ses.zeus_deadline, ses.deadline) 
    ELSE COALESCE(ses.zeus_deadline, ses.deadline) 
END
```

---

### 4. Logic Bất Biến & Lưu Vết Khi Đổi Lịch Thi (Immutability Pattern in Re-schedule)

Khi cán bộ vận hành thực hiện **Đổi lịch thi** (`updateExamSchedule`):
1. **Không cập nhật đè (No In-place Overwrite)**: Bản ghi lịch thi cũ không bị ghi đè thời gian mà chỉ được cập nhật `status = 0`.
2. **Khởi tạo bản ghi lịch mới**: Tạo một bản ghi mới trong `student_exam_schedules` với `status = 1`, mang thời gian `start_date` và `deadline` mới, đồng thời reset trạng thái thi về ban đầu:
   - `exam_status = 0` (Chờ nộp)
   - `submission_status = 0` (Đúng hạn)
   - `score_processing_status = 0` (Đang xử lý)
3. **Ràng buộc kiểm tra tính hợp lệ về mốc thời gian**:
   $$\text{new\_start\_date} > \text{timeClose cũ } (\text{deadline} \text{ hoặc } \text{zeus\_deadline})$$
   Nếu `new_start_date <= timeClose cũ`, hệ thống từ chối cập nhật và trả về HTTP 400 kèm thông báo: *"Ngày bắt đầu phải lớn hơn thời gian đóng của lịch thi hiện tại (dd/mm/yyyy hh:ii)"*.
4. **Ghi nhận lịch sử (Audit Trail)**: Khởi tạo bản ghi trong `student_exam_schedule_histories` ghi rõ:
   - `action = 'update'`
   - `user_id = actorId` (Người thực hiện)
   - `old_start_date`, `old_deadline`, `old_requirement_type`, `old_status`
   - `new_start_date`, `new_deadline`, `new_requirement_type`, `new_status`

---

### 5. Logic Gia Hạn Thời Hạn Nộp Bài (Extend Deadline Rules)

Khi cán bộ vận hành thực hiện **Gia hạn** (`extendExamSchedule`):
1. **Cập nhật tại chỗ (In-place Update)**: Cập nhật trực tiếp `deadline = $newDeadline` và đảm bảo `status = 1` trên bản ghi hiện tại. Giữ nguyên `start_date` ban đầu.
2. **Ràng buộc mốc thời gian**:
   $$\text{new\_deadline} \ge \text{timeClose cũ } (\text{deadline} \text{ hoặc } \text{zeus\_deadline})$$
   Nếu `new_deadline < timeClose cũ`, hệ thống báo lỗi HTTP 400: *"Hạn đóng mới không được nhỏ hơn thời gian đóng hiện tại"*.
3. **Ghi lịch sử thao tác**: Tạo bản ghi trong `student_exam_schedule_histories` với `action = 'extend'`.
4. **Đồng bộ sang Zeus**: Kích hoạt hàm `sendMocktestRemindingManualWithLog` để cập nhật thời hạn mới trên hệ thống Zeus.

---

### 6. Logic Cấp Thêm Lượt Thi Khi "Thi Lại" (`processRetakeAllowance`)

Khi một lịch thi được tạo hoặc đổi sang trạng thái **Thi lại** (`requirement_type = 1`), hệ thống tự động mở quyền thi lại trong bảng quản lý lượt làm bài của Moodle (`user_quiz_attempts`):

```mermaid
flowchart TD
    A[Bắt đầu processRetakeAllowance] --> B{requirementType == 1 và studentIds không rỗng?}
    B -- Không --> Exit[Kết thúc, không làm gì]
    B -- Có --> C[Tìm tất cả Quiz con thuộc Section trong bảng api_moodle]
    C --> D[Lấy danh sách moodle_id của các Quiz]
    D --> E{Có Quiz nào không?}
    E -- Không --> Exit
    E -- Có --> F[Truy vấn bảng user_quiz_attempts theo quiz_id và student_id]
    F --> G[Lọc các bản ghi có attempt_count > 0]
    G --> H[Thực hiện increment reset_allowance +1]
    H --> I[Cập nhật updated_at = NOW]
    I --> Exit
```

*Mã nguồn thực tế:*
```php
private function processRetakeAllowance(int $requirementType, int $sectionId, array $studentIds, string $tenantConnection): void
{
    if ($requirementType != 1 || empty($studentIds)) {
        return;
    }

    $quizMoodleIds = ApiMoodle::on($tenantConnection)
        ->where('parent_id', $sectionId)
        ->where('moodle_type', 'quiz')
        ->pluck('moodle_id')
        ->toArray();

    if (!empty($quizMoodleIds)) {
        UserQuizAttempt::on($tenantConnection)
            ->whereIn('quiz_id', $quizMoodleIds)
            ->whereIn('student_id', $studentIds)
            ->where('attempt_count', '>', 0)
            ->increment('reset_allowance', 1, [
                'updated_at' => Carbon::now('Asia/Ho_Chi_Minh')
            ]);
    }
}
```

---

### 7. Logic Phân Biệt Học Viên VIP vs Học Viên Thường

Hệ thống xác định một học viên là VIP trong ngữ cảnh một khóa học cụ thể thông qua quan hệ giữa `students` và `course_student`:
- **Học viên VIP (`is_vip = 1`)**: Có bản ghi trong bảng `course_student` khớp `student_id` và `course_id` với cờ `is_abnormal = 1` và `deleted_at IS NULL`.
- **Học viên Thường (`is_vip = 0`)**: Không tồn tại bản ghi `course_student` có `is_abnormal = 1` cho khóa học đó.

*Mã truy vấn lọc:*
```sql
-- Điều kiện lọc VIP
WHERE EXISTS (
    SELECT 1 FROM course_student as cs_vip 
    WHERE cs_vip.student_id = ses.student_id 
      AND cs_vip.course_id = ses.course_id 
      AND cs_vip.is_abnormal = 1 
      AND cs_vip.deleted_at IS NULL
)
```

---

### 8. Chiến Lược Khử Chuẩn Hóa Dữ Liệu (Denormalization Strategy)

Để đảm bảo hiệu năng tải trang và lọc dữ liệu tức thì cho hàng triệu bản ghi lịch thi, bảng `student_exam_schedules` được thiết kế theo mô hình **bảng phẳng (Flat table)**:

| Cột khử chuẩn | Nguồn dữ liệu gốc | Mục đích lưu trữ phẳng |
| :--- | :--- | :--- |
| `product_id`, `product_name` | `api_moodle` (Category cấp cao nhất) | Lọc và hiển thị theo sản phẩm không cần đệ quy cây Moodle |
| `course_name` | `api_moodle` (moodle_type = 'course') | Hiển thị tên khóa học trực tiếp |
| `exam_name` | `api_moodle` (moodle_type = 'section') | Hiển thị tên bài thi/section trực tiếp |
| `exam_status` | `student_score.exam_status` | 0: Chờ nộp, 1: Đã nộp, 2: Không nộp |
| `submission_status` | `student_score.submission_status` | 0: Đúng hạn, 1: Trễ hạn |
| `score_processing_status`| `student_score.score_processing_status`| 0: Đang xử lý, 1: Hoàn tất, 2: Giao chấm lại, 3: Đã hủy |

*Lợi ích:* Giảm số lượng `JOIN` từ 6 bảng (`api_moodle` x3 cấp + `student_score` + `quizzes` + `students`) xuống chỉ còn duy nhất 1 phép `JOIN` với bảng `students`.

---

### 9. Logic Tích Hợp Đồng Bộ Zeus / ICL & Ghi Log Nhắc Thi

Mỗi khi tạo lịch mới hoặc gia hạn, hệ thống tự động phát lệnh gửi nhắc nhở thi sang hệ thống Zeus:
1. **Điều kiện gửi**: Học viên có liên kết `zeus_id` trong bảng `students`.
2. **Payload chuẩn gửi sang Zeus**:
   ```json
   {
       "user_id": 12345,
       "lms_course_section_id": 6789,
       "mocktest_title": "IELTS Mocktest 01",
       "type": "manual",
       "end": "2026-10-15T23:59:59+07:00",
       "lms_course_id": 999
   }
   ```
3. **Lưu vết (Log ICL)**: Mọi lượt gửi (thành công hay thất bại) đều được ghi vào bảng `student_exam_schedule_icl_logs` với `payload`, `response`, và `status` (1 = Thành công, 0 = Lỗi) để phục vụ tra cứu và retry.

---

### 10. Phân Quyền & Quy Tắc An Toàn Khi Xóa Lịch Thi

Thao tác xóa lịch thi (`deleteExamSchedule`) có ảnh hưởng sâu rộng đến dữ liệu điểm và tiến độ học tập, vì vậy hệ thống áp dụng cơ chế bảo vệ 2 lớp:
1. **Lớp 1 - Middleware & Route Permission**: Cán bộ cần vượt qua middleware `tenant.access:web` và `check.permission`.
2. **Lớp 2 - Email Whitelist Hardcoded**: Chỉ cho phép các tài khoản có email trong danh sách `$allowedDeleteEmails` thực hiện:
   ```php
   $allowedDeleteEmails = [
       "test@hocmai.vn",
       "trantunghn196@gmail.com",
       "tungtt@ctv.hocmai.vn",
       "kienpn@ctv.hocmai.vn",
       "kienp2901@gmail.com",
       "thangnt@hocmai.vn",
   ];
   ```
3. **Quy trình Xóa Liên Hoàn (Atomic Cascade Delete)** trong DB Transaction:
   - Xóa `zeus_session_completions` (Xóa hoàn thành trên Zeus).
   - Xóa `student_exam_schedule_histories` (Lịch sử thao tác).
   - Xóa `student_exam_schedule_icl_logs` (Log gửi ICL).
   - Xóa chính bản ghi `student_exam_schedules`.
