# Thiết kế: `exam_types`, `exams`, `exam_sections`, `exam_question_items`

Tài liệu mô tả **schema đã migration** (`2026_04_04_100000_exam_types_and_exam_enhancements.php`) và **pattern service** theo loại kỳ thi.

---

## 1. Quan hệ tổng quan

```
exam_types (danh mục loại: testsite, speakwell, …)
    └── exams (một kỳ thi / đề cụ thể)
            └── exam_sections (part: Reading, Listening, …)
                    └── exam_question_items (nối question_id + thứ tự)
                            └── questions (kho câu — App\Models\EmsNew\Question)
```

- **Một `exam`** thuộc **một `exam_type`** (`exam_type_id`) — quyết định **handler mặc định**.
- **Mỗi part** = một **`exam_sections`**; có thể **ghi đè `handler_key`** riêng (ví dụ Speakwell: phần Writing gọi API khác).
- **Mỗi câu trong đề** = **`exam_question_items`**: `question_id` + `section_id` + `order` + tuỳ chọn `settings`.

---

## 2. Bảng `exam_types`

| Cột | Kiểu | Ý nghĩa |
|-----|------|---------|
| `id` | PK | |
| `code` | VARCHAR(64) UNIQUE | `TESTSITE`, `SPEAKWELL`, `IELTS_FULL`, … |
| `name` | VARCHAR | Tên hiển thị |
| `description` | TEXT nullable | |
| `handler_key` | VARCHAR(100) | Khóa dùng **resolve service** (xem §5) |
| `default_settings` | JSON nullable | Mặc định cho mọi exam thuộc type (thời gian, rule chung) |
| `is_active` | BOOL | |
| `timestamps` | | |

**Ví dụ `handler_key`:** `testsite`, `speakwell`, `ielts_full` — không nhất thiết trùng tên class; map trong `config` hoặc container.

---

## 3. Bảng `exams` (bổ sung)

Cột **mới** (các cột cũ: `title`, `duration`, `settings`, `description`, `category` giữ nguyên):

| Cột | Kiểu | Ý nghĩa |
|-----|------|---------|
| `exam_type_id` | FK → `exam_types`, nullable | Loại kỳ thi; null = đề “legacy” chưa gán type |
| `slug` | VARCHAR(128) UNIQUE nullable | URL / mã thân thiện |
| `status` | VARCHAR(32), default `draft` | `draft`, `published`, `archived`, … |
| `is_active` | BOOL default true | |

Index: `(exam_type_id, is_active)`.

---

## 4. Bảng `exam_sections` (bổ sung)

| Cột | Kiểu | Ý nghĩa |
|-----|------|---------|
| `duration_seconds` | UNSIGNED INT nullable | Thời gian riêng part (giây) |
| `skill_code` | VARCHAR(32) nullable | `READING`, `LISTENING`, `WRITING`, `SPEAKING`, … |
| `handler_key` | VARCHAR(100) nullable | **Override**: nếu có → dùng handler này cho **cả part**; null → dùng `exam_types.handler_key` |

`settings` (JSON) vẫn dùng cho cấu hình nhỏ (shuffle, instruction, …).

---

## 5. Bảng `exam_question_items` (bổ sung)

| Cột | Kiểu | Ý nghĩa |
|-----|------|---------|
| `settings` | JSON nullable | Ghi đè từng câu trong đề: weight band, cờ “chấm bằng service X”, … |

Các cột cũ: `exam_id`, `section_id`, `question_id`, `order`, `weight`.

---

## 6. Pattern: mỗi loại exam → một service (handler)

**Ý tưởng:** Không nhét logic khác nhau (testsite 1 part, speakwell gọi API writing) vào một controller; **một dòng `exam_types.handler_key`** (và tuỳ chọn **`exam_sections.handler_key`**) trỏ tới **một class / binding** xử lý.

### 6.1. Thứ tự resolve handler (gợi ý)

1. Nếu `exam_section.handler_key` **có giá trị** → dùng handler đó cho **mọi thao tác trong part** (nộp bài part, chấm, gọi API ngoài).
2. Ngược lại → dùng `exam_types.handler_key` của exam.
3. Nếu vẫn null → handler fallback `default`.

### 6.2. Interface (gợi ý triển khai)

```php
// Giả lập — tạo file khi triển khai thật
interface ExamTypeHandlerContract
{
    public function handleSectionSubmit(Exam $exam, ExamSection $section, Submission $submission, array $payload): void;
    // hoặc tách: startExam, gradeQuestion, callExternalWritingApi, ...
}
```

Đăng ký trong `AppServiceProvider`:

```php
$this->app->bind('exam.handler.testsite', \App\Services\ExamTypes\TestsiteExamHandler::class);
$this->app->bind('exam.handler.speakwell', \App\Services\ExamTypes\SpeakwellExamHandler::class);
```

`ExamType::handler_key` = `testsite` → `app('exam.handler.'.$key)`.

### 6.3. Ví dụ nghiệp vụ

| Loại | Part | Ghi chú |
|------|------|--------|
| Testsite | 1 hoặc nhiều section | `handler_key = testsite`; logic đơn giản trong `TestsiteExamHandler` |
| Speakwell | Writing | `exam_sections.handler_key = speakwell_writing` → handler gọi API chấm/ gửi bài; các part khác `speakwell` chung |

---

## 7. Model Eloquent

- `App\Models\ExamType` — `exams()->hasMany`
- `App\Models\Exam` — `examType()->belongsTo`, `sections()`, `examQuestionItems()`, …
- `App\Models\ExamSection` — `fillable` gồm `skill_code`, `duration_seconds`, `handler_key`
- `App\Models\ExamQuestionItem` — `settings` cast array

---

## 8. Seed gợi ý `exam_types` (chạy tay hoặc Seeder)

```sql
INSERT INTO exam_types (code, name, description, handler_key, default_settings, is_active, created_at, updated_at) VALUES
('TESTSITE', 'Test site', 'Thi linh hoạt 1 hoặc nhiều part', 'testsite', NULL, 1, NOW(), NOW()),
('SPEAKWELL', 'Speakwell', 'Writing có thể gọi API riêng', 'speakwell', NULL, 1, NOW(), NOW()),
('IELTS_FULL', 'IELTS Full mock', '4 kỹ năng', 'ielts_full', NULL, 1, NOW(), NOW());
```

---

## 9. Tóm tắt câu hỏi của bạn

- **Có**, nên thiết kế **mỗi loại exam** (và tuỳ chọn **mỗi part**) với **`handler_key`** để **đi vào service riêng** — không phụ thuộc bảng `questions`.
- **Kho câu** (`questions`) vẫn trung lập; **đề thi** (`exams` + sections + items) chỉ **trỏ** `question_id` và gắn **loại xử lý** qua `exam_types` / `exam_sections`.
