# 📘 Tài Liệu Kỹ Thuật: Hệ Thống Moodle Config Sync

> **Mục tiêu**: Tài liệu này mô tả toàn bộ kiến trúc, logic và các thay đổi liên quan đến việc đồng bộ (sync) dữ liệu cấu hình từ Moodle API xuống cơ sở dữ liệu local (`api_moodle_configs`).

---

## 1. Bối Cảnh & Vấn Đề

### Trước khi có hệ thống này

Mỗi khi frontend cần hiển thị thông tin chi tiết của một course/section/activity (ví dụ: ngày bắt đầu, tên đầy đủ, điều kiện availability...), hệ thống phải **gọi thẳng lên Moodle API mỗi request**. Điều này gây ra:

- ⏱️ **Chậm**: Moodle API có độ trễ cao (100–500ms mỗi call).
- 🔁 **Lãng phí**: Cùng một dữ liệu được fetch lại hàng trăm lần.
- 💥 **Risk timeout**: Nếu cần lấy hàng loạt records, request có thể bị timeout.

### Giải pháp: Cache-Aside Pattern

```
Request đến
     │
     ▼
Có cache trong api_moodle_configs?
     │
   YES ──────────────────► Trả về cache (nhanh)
     │
    NO
     │
     ▼
Gọi Moodle API lấy data
     │
     ▼
Lưu vào api_moodle_configs
     │
     ▼
Trả về data cho request
```

Lần sau gọi cùng item → lấy thẳng từ local DB, không gọi Moodle API.

---

## 2. Sơ Đồ Kiến Trúc Tổng Quan

```
┌────────────────────────────────────────────────────────────┐
│                        FRONTEND / API                       │
└──────────────┬─────────────────────────┬───────────────────┘
               │  POST /api/lms/moodle/  │  POST /api/lms/moodle/
               │  sync-configs           │  unsynced-configs
               ▼                         ▼
┌──────────────────────────┐  ┌─────────────────────────────┐
│  ProductController        │  │  ProductController           │
│  syncMoodleConfigs()      │  │  getUnsyncedMoodleConfigs()  │
└──────────┬───────────────┘  └─────────────────────────────┘
           │  exec() (background)
           ▼
┌──────────────────────────────────┐
│  Artisan Command                  │
│  moodle:sync-configs              │
│  MoodleSyncConfigsCommand.php     │
└──────────┬───────────────────────┘
           │  gọi Service methods
           ▼
┌──────────────────────────────────┐
│  MoodleSyncService.php            │
│  - getCategoryConfig()            │
│  - getCourseConfig()              │
│  - getSectionConfig()             │
│  - getSectionConfigBatch() ◄ MỚI  │
│  - getActivityConfig()            │
└──────────┬───────────────────────┘
           │  Cache-Aside Pattern
           ▼
┌──────────────────────────────────┐       ┌──────────────────┐
│  Database: api_moodle_configs     │◄──────│  Database:       │
│  (local cache)                    │       │  api_moodle      │
└──────────────────────────────────┘       │  (source of IDs) │
                                           └──────────────────┘
```

---

## 3. Database Schema

### Bảng `api_moodle` (bảng gốc - không thay đổi)

Lưu danh sách các entity của LMS được mapping với Moodle:

| Cột          | Kiểu    | Mô tả                                               |
|--------------|---------|-----------------------------------------------------|
| `id`         | int     | Primary key (local ID)                              |
| `moodle_id`  | int     | ID tương ứng bên Moodle                             |
| `moodle_type`| string  | `category`, `course`, `section`, `quiz`, `url`, ... |
| `moodle_name`| string  | Tên hiển thị                                        |
| `parent_id`  | int     | FK tới chính bảng `api_moodle` (section → course)  |

### Bảng `api_moodle_configs` (cache table - MỚI)

Lưu data chi tiết fetch từ Moodle API, tránh gọi lại lần sau:

| Cột                   | Kiểu      | Dùng cho      | Mô tả                                      |
|-----------------------|-----------|---------------|--------------------------------------------|
| `id`                  | int       | tất cả        | Primary key                                |
| `api_moodle_id`       | int (FK)  | tất cả        | Trỏ về `api_moodle.id`                     |
| `moodle_idnumber`     | string    | category/course | IDNumber trong Moodle                    |
| `moodle_description`  | text      | category      | Mô tả category                             |
| `moodle_coursecount`  | int       | category      | Số course trong category                   |
| `moodle_shortname`    | string    | course        | Tên ngắn của course                        |
| `moodle_fullname`     | string    | course        | Tên đầy đủ của course                      |
| `moodle_summary`      | text      | course/section| Tóm tắt                                    |
| `moodle_startdate`    | datetime  | course        | Ngày bắt đầu (parse từ Unix timestamp)     |
| `moodle_enddate`      | datetime  | course        | Ngày kết thúc (parse từ Unix timestamp)    |
| `moodle_format`       | string    | course        | Format của course (weeks, topics...)       |
| `moodle_numsections`  | int       | course        | Số section trong course                    |
| `moodle_enablecompletion`| tinyint | course       | Bật/tắt completion tracking               |
| `moodle_overviewfiles`| json      | course        | Ảnh cover của course                       |
| `moodle_sectionnum`   | int       | section       | Số thứ tự của section                      |
| `moodle_sequence`     | string    | section       | Danh sách cm ID trong section              |
| `moodle_availability` | json      | section/activity | Điều kiện mở khóa                       |
| `moodle_module_data`  | json      | activity      | Toàn bộ data cm từ Moodle                  |
| `moodle_plugin_config`| json      | activity      | Plugin config (quiz settings, url link...) |

---

## 4. Các File Đã Thay Đổi / Thêm Mới

```
app/
├── Models/
│   ├── ApiMoodle.php              ← Thêm relations mới
│   └── ApiMoodleConfig.php        ← Model mới (cache table)
│
├── Services/
│   └── MoodleSyncService.php      ← Service mới (core logic)
│
├── Console/
│   └── Commands/
│       └── MoodleSyncConfigsCommand.php  ← Artisan command mới
│
└── Http/Controllers/
    └── ProductController.php      ← Thêm 2 API endpoints mới

routes/
└── api.php                        ← Thêm 2 routes mới
```

---

## 5. Chi Tiết Từng File

### 5.1 `app/Models/ApiMoodle.php` — Thêm Relations

**Thêm mới:**

```php
// Quan hệ 1-1 với bảng api_moodle_configs (cache)
public function moodleConfig(): HasOne
{
    return $this->hasOne(ApiMoodleConfig::class, 'api_moodle_id');
}

// Quan hệ self-referential: section → course cha
public function parent(): BelongsTo
{
    return $this->belongsTo(ApiMoodle::class, 'parent_id', 'id');
}

// Quan hệ self-referential: course → danh sách sections
public function children(): HasMany
{
    return $this->hasMany(ApiMoodle::class, 'parent_id', 'id');
}
```

**Lý do:**
- `moodleConfig`: Để có thể eager load cache trong 1 query (`->with('moodleConfig')`).
- `parent`: Section cần biết moodle_id của course cha để gọi `get_sections` API. Batch sync dùng relation này để group sections theo course.

---

### 5.2 `app/Models/ApiMoodleConfig.php` — Model mới

```php
class ApiMoodleConfig extends Model
{
    protected $table = 'api_moodle_configs';
    protected $guarded = []; // Cho phép mass-assignment tất cả cột

    protected $casts = [
        'moodle_overviewfiles'  => 'array',   // JSON tự động parse
        'moodle_availability'   => 'array',   // JSON tự động parse
        'moodle_module_data'    => 'array',   // JSON tự động parse
        'moodle_plugin_config'  => 'array',   // JSON tự động parse
        'moodle_startdate'      => 'datetime',
        'moodle_enddate'        => 'datetime',
    ];

    public function apiMoodle(): BelongsTo
    {
        return $this->belongsTo(ApiMoodle::class, 'api_moodle_id');
    }
}
```

---

### 5.3 `app/Services/MoodleSyncService.php` — Core Logic

Service này là trung tâm của hệ thống, implement pattern **Cache-Aside** cho từng loại entity.

#### Pattern chung (Get or Create Cache):

```
1. loadMissing('moodleConfig')
       │
       ▼ Đã có cache?
      YES ──► return $item (không gọi Moodle)
       │
      NO
       │
       ▼
2. Gọi Moodle API (Common::...)
       │
       ▼
3. ApiMoodleConfig::on($tenant)->create([...])
       │
       ▼
4. $item->load('moodleConfig')
       │
       ▼
5. return $item (với cache mới)
```

#### Các methods:

| Method | Moodle API Gọi | Mục đích |
|--------|---------------|----------|
| `getCategoryConfig()` | `core_course_get_categories` | Cache info category |
| `invalidateCategoryConfig()` | — | Xóa cache category |
| `getCourseConfig()` | `core_course_get_courses_by_field` | Cache info course |
| `invalidateCourseConfig()` | — | Xóa cache course |
| `getSectionConfig()` | `local_custom_service_get_sections` | Cache 1 section |
| `invalidateSectionConfig()` | — | Xóa cache section |
| **`getSectionConfigBatch()`** ⭐ | `local_custom_service_get_sections` | Cache nhiều sections với **1 lần gọi API** |
| `getActivityConfig()` | `core_course_get_course_module` + `local_custom_service_get_detail_module` | Cache activity |
| `invalidateActivityConfig()` | — | Xóa cache activity |
| `invalidateManyConfigs()` | — | Xóa cache theo nhiều IDs |

#### `getSectionConfigBatch()` — Tối ưu batch sync sections ⭐

> **Vấn đề**: Nếu 1 course có 20 sections, cách sync thông thường sẽ gọi `get_sections` API **20 lần** (1 lần/section) vì mỗi call cần `courseMoodleId`.
>
> **Giải pháp**: `getSectionConfigBatch()` nhận vào **tập hợp nhiều sections cùng course**, gọi API **chỉ 1 lần**, rồi map kết quả cho từng section.

```
getSectionConfigBatch([$sec1, $sec2, ..., $sec20], courseMoodleId=123)
         │
         ▼
1. Lọc ra những sections chưa có cache (needSync)
         │
         ▼
2. Gọi local_custom_service_get_sections(courseMoodleId=123) → 1 lần duy nhất
         │
         ▼
3. Build map: { moodle_section_id: section_data } (O(1) lookup)
         │
         ▼
4. Loop needSync → tạo ApiMoodleConfig cho từng section
         │
         ▼
   Tiết kiệm 19/20 lần gọi API!
```

---

### 5.4 `app/Console/Commands/MoodleSyncConfigsCommand.php` — Artisan Command Mới

**Chạy lệnh:**
```bash
php artisan moodle:sync-configs \
  --tenant=your_tenant_connection \
  --type=all \        # all | category | course | section | activity
  --chunk=50          # số records xử lý mỗi batch (default: 50, max: 200)
```

**Logic xử lý:**

```
handle()
   ├── type=all/category → _bulkSyncCategories()
   ├── type=all/course   → _bulkSyncCourses()
   ├── type=all/section  → _bulkSyncSections()    ← dùng batch
   └── type=all/activity → _bulkSyncActivities()
```

**Chiến lược `whereDoesntHave('moodleConfig')`:**

> Mỗi private method đều filter `whereDoesntHave('moodleConfig')` — **chỉ xử lý các records chưa có cache**. Records đã sync trước đó sẽ bị bỏ qua hoàn toàn. Idempotent — chạy nhiều lần không sao.

**Batch section (đặc biệt):**

```php
// _bulkSyncSections():
ApiMoodle::on($tenant)
    ->where('moodle_type', 'section')
    ->whereDoesntHave('moodleConfig')
    ->with('parent')          // eager load course cha
    ->chunk(50, function($sections) {
        // Group sections theo course (moodle_id của parent)
        $grouped = $sections->groupBy(fn($s) => $s->parent?->moodle_id);

        foreach ($grouped as $courseMoodleId => $sectionGroup) {
            // Gọi API 1 lần cho mỗi course, sync tất cả sections
            $moodleSyncService->getSectionConfigBatch($sectionGroup, $courseMoodleId, $tenant);
        }
    });
```

**Log output:** Ghi vào `storage/logs/moodle_sync_{tenant}_{type}_{timestamp}.log`

---

### 5.5 `app/Http/Controllers/ProductController.php` — 2 API Mới

#### API 1: `POST /api/lms/moodle/sync-configs` — Kích hoạt sync

```
Request ──► validate tenant_connection ──► exec() background command ──► return 200 ngay
```

> ⚠️ **Quan trọng**: API **không chờ** command hoàn thành. Nó dùng `exec("... &")` để chạy background, tránh HTTP timeout khi sync hàng nghìn records.

**Request body:**
| Tham số | Bắt buộc | Mặc định | Mô tả |
|---------|----------|----------|-------|
| `tenant_connection` | ✅ | — | Tên kết nối DB tenant |
| `type` | ❌ | `all` | `all`, `category`, `course`, `section`, `activity` |
| `chunk` | ❌ | `50` | Kích thước batch (1–200) |

**Response:**
```json
{
  "success": true,
  "message": "Started background sync process. Please check the log file for progress.",
  "data": {
    "tenant": "tenant_abc",
    "type": "all",
    "chunk": 50,
    "command": "php /var/www/html/lms_hocmai/artisan moodle:sync-configs ...",
    "log_file": "/var/www/html/lms_hocmai/storage/logs/moodle_sync_tenant_abc_all_20260710_090000.log"
  }
}
```

---

#### API 2: `POST /api/lms/moodle/unsynced-configs` — Kiểm tra chênh lệch

Trả về danh sách ID trong `api_moodle` **chưa có** record tương ứng trong `api_moodle_configs`.

**Request body:**
| Tham số | Bắt buộc | Mặc định | Mô tả |
|---------|----------|----------|-------|
| `tenant_connection` | ✅ | — | Tên kết nối DB tenant |
| `type` | ❌ | `all` | `all`, `category`, `course`, `section`, `activity` |
| `limit` | ❌ | `100` | Số records trả về (tối đa 5000) |

**Response:**
```json
{
  "success": true,
  "tenant": "tenant_abc",
  "type": "all",
  "summary": {
    "total_unsynced": 284,
    "limit_returned": 100
  },
  "unsynced_ids": [12, 45, 67, ...]
}
```

---

### 5.6 `routes/api.php` — Routes Mới

```php
// Trong group Route::prefix('lms')->middleware([...])
Route::post("moodle/sync-configs",    [ProductController::class, 'syncMoodleConfigs']);
Route::post("moodle/unsynced-configs", [ProductController::class, 'getUnsyncedMoodleConfigs']);
```

---

## 6. Quy Trình Sync Đầy Đủ (Step-by-step)

### Bước 1: Kiểm tra trạng thái hiện tại

```bash
curl -X POST https://your-domain/api/lms/moodle/unsynced-configs \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"tenant_connection": "your_tenant", "type": "all"}'
```

→ Xem `summary.total_unsynced` để biết có bao nhiêu records chưa sync.

### Bước 2: Kích hoạt sync

```bash
curl -X POST https://your-domain/api/lms/moodle/sync-configs \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"tenant_connection": "your_tenant", "type": "all", "chunk": 50}'
```

→ API trả về ngay, background job chạy tiếp.

### Bước 3: Theo dõi tiến trình

```bash
# Lấy log_file path từ response bước 2, rồi:
tail -f /var/www/html/lms_hocmai/storage/logs/moodle_sync_your_tenant_all_20260710_090000.log
```

### Bước 4: Xác nhận hoàn thành

Gọi lại API unsynced-configs, `total_unsynced` phải về 0 (hoặc giảm đáng kể).

---

## 7. Sơ Đồ Luồng Sync Chi Tiết

```
POST /api/lms/moodle/sync-configs
           │
           ▼
  Validate tenant_connection
           │
           ▼
  exec("php artisan moodle:sync-configs ..." &)
           │
           ▼ (ngay lập tức, không chờ)
  Return HTTP 200
           
           │ (background)
           ▼
  MoodleSyncConfigsCommand::handle()
           │
    ┌──────┴──────┐──────────────┐──────────────────┐
    ▼             ▼              ▼                   ▼
categories    courses        sections           activities
    │             │              │                   │
    ▼             ▼              ▼                   ▼
 chunk(50)    chunk(50)      chunk(50)           chunk(50)
    │             │              │                   │
    ▼             ▼              ▼                   ▼
whereDoesnt   whereDoesnt    whereDoesnt         whereDoesnt
Have config   Have config    Have config         Have config
    │             │          +with('parent')         │
    ▼             ▼              │                   ▼
getCategoryC  getCourseC     groupBy course     getActivityC
onfig()       onfig()        MoodleId()         onfig()
    │             │              │
    ▼             ▼              ▼
  Cache!        Cache!      getSectionConfig
                            Batch()
                                │
                                ▼ (1 API call / course)
                              Cache all sections!
```

---

## 8. Negative Caching (Khi Moodle Không Có Data)

> **Tình huống**: Activity đã bị xóa bên Moodle nhưng vẫn còn trong `api_moodle` (chưa sync xóa về local).

Khi gọi Moodle API và nhận về data rỗng:
- `getActivityConfig()` vẫn **tạo record trong `api_moodle_configs`** với `moodle_module_data = null`.
- Lần sau khi check `whereDoesntHave('moodleConfig')` → bỏ qua (vì đã có record).
- Không gọi lại Moodle vô ích → **tiết kiệm request**.

---

## 9. Invalidation (Xóa Cache Khi Data Thay Đổi)

Khi user update/delete một entity trên LMS, cache tương ứng phải được xóa để lần sau fetch lại từ Moodle.

```php
// Inject MoodleSyncService
$moodleSyncService->invalidateCategoryConfig($apiMoodleId, $tenantConnection);
$moodleSyncService->invalidateCourseConfig($apiMoodleId, $tenantConnection);
$moodleSyncService->invalidateSectionConfig($apiMoodleId, $tenantConnection);
$moodleSyncService->invalidateActivityConfig($apiMoodleId, $tenantConnection);

// Xóa nhiều cùng lúc (ví dụ khi xóa section và các activities con)
$moodleSyncService->invalidateManyConfigs([101, 102, 103], $tenantConnection);
```

---

## 10. Lưu Ý Quan Trọng

> [!IMPORTANT]
> `tenant_connection` là bắt buộc trong **mọi** API call và Artisan command. Hệ thống multi-tenant, mỗi tenant có DB riêng.

> [!WARNING]
> Không chạy sync nhiều lần song song cùng `tenant + type`. Có thể gây duplicate insert nếu 2 processes cùng detect "chưa có cache" cho cùng 1 record.

> [!TIP]
> Khi sync lần đầu, nên sync theo từng `type` thay vì `all` để dễ theo dõi và tránh quá tải Moodle API:
> ```
> type=category → type=course → type=section → type=activity
> ```

> [!NOTE]
> Background sync dùng `exec()`. Nếu web server không có quyền chạy `php`, hãy kiểm tra permission của user `www-data`/`nginx`. Thay thế lâu dài: migrate sang Laravel Queue.

---

## 11. Tóm Tắt Nhanh (TL;DR)

| Câu hỏi | Trả lời |
|---------|---------|
| Cache lưu ở đâu? | Bảng `api_moodle_configs` |
| Khi nào tạo cache? | Lần đầu tiên fetch (GET request) hoặc qua bulk sync API |
| Khi nào xóa cache? | Khi update/delete entity → gọi `invalidate*Config()` |
| Làm sao sync hàng loạt? | `POST /api/lms/moodle/sync-configs` → chạy background |
| Theo dõi tiến trình? | `tail -f storage/logs/moodle_sync_*.log` |
| Kiểm tra chưa sync? | `POST /api/lms/moodle/unsynced-configs` |
| Section sync nhanh hơn nhờ gì? | `getSectionConfigBatch()` — 1 API call cho nhiều sections cùng course |
