# Kế hoạch theo dõi và loại bỏ User Quiz Grade đã Tracking

## Tổng quan

Kế hoạch này mô tả cách theo dõi các user quiz grade đã được sync và update thành công (status = 2 hoặc 6) trong bảng `user_quiz_grade`, sau đó loại bỏ các records trùng lặp sau khi sync data mới từ speakup Moodle.

**Lưu ý**: Command sync user quiz grade đã chạy và có data trong bảng `user_quiz_grade` trước đó. Kế hoạch này sẽ tracking data hiện tại, sau đó khi sync data mới, sẽ xóa các records đã tracking để tránh duplicate.

## Mục tiêu

1. **Theo dõi**: Lưu các user quiz grade đã sync thành công (status = 2 hoặc 6) từ data hiện tại vào tracking table
2. **Lưu trữ**: Lưu danh sách này để tham chiếu sau
3. **Làm sạch**: Sau khi sync data mới, so sánh và xóa các records trùng với tracking (có cùng user_moodle_id, spk_course_id, và spk_quiz_id/spk_cmid)

## Logic xác định User Quiz Grade cần Tracking

Một user quiz grade được **tracking** khi:
- Có trong bảng `user_quiz_grade`
- `status = 6` (completed update user_id và quiz_id) - **BẮT BUỘC**
- `user_moodle_id`, `spk_course_id`, và `spk_cmid` hợp lệ

**Lưu ý**: 
- Chỉ tracking các records đã update thành công (status = 6) - chỉ những records đã được update `user_id` và `quiz_id` thành công
- Không tracking các records có status khác (1, 2, 3, 4, 5, 7)
- Tracking dựa trên composite key: `(user_moodle_id, spk_course_id, spk_cmid)` để xác định một quiz grade cụ thể

## Ví dụ minh họa

### Trước khi sync:
- User ID 1 có 50 quiz grades trong `user_quiz_grade`:
  - 40 quiz grades đã update thành công (status = 6)
  - 10 quiz grades chưa update (status = 1, 2, 3, 4, 5, 7)
- **Tracking**: Lưu 40 quiz grades có status = 6 vào tracking table (với score tại thời điểm tracking)

### Sau khi sync:
- User ID 1 có 60 quiz grades trong `user_quiz_grade`:
  - 45 quiz grades đã update thành công (40 cũ + 5 mới)
  - 15 quiz grades chưa update
- **So sánh**: Tìm các quiz grades trong tracking table có trong data mới (dựa trên `user_moodle_id`, `spk_course_id`, `spk_cmid`)
- **Xóa**: 
  - Xóa các quiz grades trùng với tracking **CHỈ KHI score không thay đổi** (so sánh score trong tracking với score hiện tại)
  - Nếu score có thay đổi thì **KHÔNG xóa**, giữ lại record mới với score mới
- **Kết quả**: 
  - Còn lại các quiz grades có score thay đổi (không bị xóa)
  - Còn lại 5 quiz grades mới
  - Còn lại 15 quiz grades chưa update

## Cấu trúc Database

### Bảng mới: `speakup_user_quiz_grade_tracking`

Bảng này sẽ lưu trữ các user quiz grade đã sync thành công đã được tracking.

```sql
CREATE TABLE `speakup_user_quiz_grade_tracking` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `course_id` INT NOT NULL COMMENT 'Course ID (38, 39, 45, 46, 47, 68) - tương đương spk_course_id',
  `user_moodle_id` BIGINT UNSIGNED NOT NULL COMMENT 'User ID từ speakup Moodle',
  `spk_cmid` BIGINT UNSIGNED NOT NULL COMMENT 'Course module ID từ speakup Moodle',
  `spk_quiz_id` BIGINT UNSIGNED NULL COMMENT 'Quiz ID từ speakup Moodle (lưu để tham khảo)',
  `score` DECIMAL(10, 2) NULL COMMENT 'Điểm số quiz tại thời điểm tracking (dùng để so sánh khi xóa)',
  `quiz_name` VARCHAR(255) NULL COMMENT 'Tên quiz',
  `status` INT NOT NULL DEFAULT 6 COMMENT 'Trạng thái (luôn là 6 - completed update)',
  `tenant_connection` VARCHAR(255) NULL COMMENT 'Tenant connection name',
  `tracked_at` TIMESTAMP NOT NULL COMMENT 'Thời điểm tracking',
  `created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_course_user_cm` (`course_id`, `user_moodle_id`, `spk_cmid`),
  KEY `idx_course_id` (`course_id`),
  KEY `idx_user_moodle_id` (`user_moodle_id`),
  KEY `idx_spk_cmid` (`spk_cmid`),
  KEY `idx_spk_quiz_id` (`spk_quiz_id`),
  KEY `idx_tenant_connection` (`tenant_connection`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

**Lưu ý**: 
- Unique constraint trên `(course_id, user_moodle_id, spk_cmid)` để đảm bảo không duplicate
- Field `score` được lưu để so sánh khi xóa - chỉ xóa nếu score không thay đổi
- Field `spk_quiz_id` được lưu để tham khảo, không dùng trong unique constraint

## Flow Diagram

```
┌─────────────────────────────────────────────────────────────────┐
│ 0. Data hiện tại (đã có)                                         │
│    - user_quiz_grade có data                                    │
│    - Sync command đã chạy thành công                             │
│    - Có các quiz grades với status = 2 hoặc 6                    │
└───────────────────────┬─────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. Tracking user quiz grade đã update thành công                  │
│    API: trackUserQuizGradeFromCurrentData                        │
│    POST /api/speakup/track-user-quiz-grade-from-current-data     │
│    Input: course_id (optional, nếu không có thì track tất cả)    │
│    Logic:                                                        │
│    - Lấy tất cả records từ user_quiz_grade                       │
│    - Filter status = 6 (đã update user_id và quiz_id thành công)  │
│    - Lưu vào tracking table (kèm score tại thời điểm tracking)   │
│    Output: Số lượng records đã tracking                          │
└───────────────────────┬─────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Sync data mới từ speakup Moodle                               │
│    Command: SyncUserQuizGradeCommand                             │
│    hoặc API sync user quiz grade                                 │
│    Input: course_id (optional)                                   │
│    Output: Số lượng records đã sync                              │
└───────────────────────┬─────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. So sánh và xóa records trùng với tracking                     │
│    API: removeTrackedUserQuizGradeFromTable                      │
│    POST /api/speakup/remove-tracked-user-quiz-grade-from-table   │
│    Input: course_id (optional)                                   │
│    Logic:                                                        │
│    - Lấy danh sách từ tracking table                             │
│    - Tìm các records trong user_quiz_grade có                   │
│      (user_moodle_id, spk_course_id, spk_cmid) trùng với tracking│
│    - So sánh score: CHỈ xóa nếu score không thay đổi            │
│      (score trong tracking === score hiện tại trong table)       │
│    - Nếu score thay đổi thì KHÔNG xóa, giữ lại record mới       │
│    Output: Số lượng records đã xóa                              │
└─────────────────────────────────────────────────────────────────┘
```

## APIs cần tạo

### 1. API tracking user quiz grade từ data hiện tại

**Endpoint**: `POST /api/speakup/track-user-quiz-grade-from-current-data`

**Mục đích**: Lưu các user quiz grade đã update thành công (status = 6) vào tracking table

**Logic**:
1. Lấy tất cả records từ `user_quiz_grade` với `status = 6` (bắt buộc)
2. Filter theo `course_id` (spk_course_id) nếu được cung cấp
3. Lưu vào `speakup_user_quiz_grade_tracking` với các thông tin:
   - `course_id` (từ spk_course_id)
   - `user_moodle_id`
   - `spk_cmid` (bắt buộc, dùng làm unique key)
   - `spk_quiz_id` (lưu để tham khảo)
   - `score` (lưu để so sánh khi xóa)
   - `quiz_name`
   - `status` (luôn là 6)
   - `tenant_connection`
   - `tracked_at` = now()

**Request**:
```json
{
  "course_id": 45  // optional, nếu không có thì track tất cả courses
}
```

**Response**:
```json
{
  "success": true,
  "message": "Tracked 5000 user quiz grades from current data",
  "course_id": 45,
  "summary": {
    "total_quiz_grades_in_table": 8000,
    "completed_update_quiz_grades": 5000,
    "tracked_count": 5000,
    "skipped_count": 0
  }
}
```

### 2. API loại bỏ tracked user quiz grade khỏi table

**Endpoint**: `POST /api/speakup/remove-tracked-user-quiz-grade-from-table`

**Mục đích**: Xóa các user quiz grade đã tracking khỏi bảng `user_quiz_grade` (chỉ xóa nếu score không thay đổi)

**Logic**:
1. Lấy danh sách `(course_id, user_moodle_id, spk_cmid, score)` từ `speakup_user_quiz_grade_tracking` theo `course_id` (nếu được cung cấp)
2. Tìm các records trong `user_quiz_grade` có `(spk_course_id, user_moodle_id, spk_cmid)` trùng với tracking
3. **So sánh score**: CHỈ xóa nếu `score` trong tracking === `score` hiện tại trong `user_quiz_grade`
4. Nếu score thay đổi thì **KHÔNG xóa**, giữ lại record mới với score mới

**Request**:
```json
{
  "course_id": 45  // optional, nếu không có thì xóa tất cả tracked records
}
```

**Response**:
```json
{
  "success": true,
  "message": "Removed 4800 tracked user quiz grades from user_quiz_grade table (200 records kept due to score changes)",
  "course_id": 45,
  "removed_count": 4800,
  "kept_due_to_score_change": 200,
  "remaining_count": 3200
}
```

### 3. API lấy danh sách user quiz grade đã tracking

**Endpoint**: `POST /api/speakup/get-user-quiz-grade-tracking`

**Mục đích**: Xem danh sách các user quiz grade đã được tracking

**Request**:
```json
{
  "course_id": 45,  // optional
  "user_moodle_id": 123,  // optional
  "page": 1,  // optional, default: 1
  "per_page": 50  // optional, default: 50
}
```

**Response**:
```json
{
  "success": true,
  "course_id": 45,
  "total_count": 5000,
  "page": 1,
  "per_page": 50,
  "total_pages": 100,
  "quiz_grades": [
    {
      "id": 1,
      "course_id": 45,
      "user_moodle_id": 123,
      "spk_quiz_id": 456,
      "spk_cmid": 789,
      "score": 85.50,
      "quiz_name": "Quiz 1",
      "status": 2,
      "tenant_connection": "tenant1",
      "tracked_at": "2025-01-15 10:30:00",
      "created_at": "2025-01-15 10:30:00"
    }
  ]
}
```

### 4. API xóa tracking data (optional)

**Endpoint**: `POST /api/speakup/clear-user-quiz-grade-tracking`

**Mục đích**: Xóa dữ liệu tracking cho một course hoặc tất cả (khi cần reset)

**Request**:
```json
{
  "course_id": 45  // optional, nếu không có thì xóa tất cả
}
```

**Response**:
```json
{
  "success": true,
  "message": "Cleared tracking data for course 45",
  "course_id": 45,
  "deleted_count": 5000
}
```

## Các bước Implementation

### Bước 1: Tạo Migration

Tạo file migration để tạo bảng `speakup_user_quiz_grade_tracking`:

```bash
php artisan make:migration create_speakup_user_quiz_grade_tracking_table
```

### Bước 2: Tạo Model

Tạo model `SpeakupUserQuizGradeTracking`:

```bash
php artisan make:model SpeakupUserQuizGradeTracking
```

### Bước 3: Tạo Controller Methods

Thêm các methods vào `SpeakupActivitySyncController`:
1. `trackUserQuizGradeFromCurrentData()` - Lưu user quiz grade đã sync thành công vào tracking
2. `removeTrackedUserQuizGradeFromTable()` - Xóa tracked user quiz grade khỏi table
3. `getUserQuizGradeTracking()` - Xem danh sách tracking
4. `clearUserQuizGradeTracking()` - Xóa tracking data (optional)

### Bước 4: Thêm Routes

Thêm các routes vào `routes/api.php` trong prefix `speakup`:

```php
Route::prefix('speakup')->controller(SpeakupActivitySyncController::class)->group(function () {
    // ... existing routes ...
    
    // API mới để tracking user quiz grade
    Route::post('/track-user-quiz-grade-from-current-data', 'trackUserQuizGradeFromCurrentData')
        ->name('speakup.trackUserQuizGradeFromCurrentData');
    
    Route::post('/remove-tracked-user-quiz-grade-from-table', 'removeTrackedUserQuizGradeFromTable')
        ->name('speakup.removeTrackedUserQuizGradeFromTable');
    
    Route::post('/get-user-quiz-grade-tracking', 'getUserQuizGradeTracking')
        ->name('speakup.getUserQuizGradeTracking');
    
    Route::post('/clear-user-quiz-grade-tracking', 'clearUserQuizGradeTracking')
        ->name('speakup.clearUserQuizGradeTracking');
});
```

### Bước 5: Implement logic trong Controller

**Method `trackUserQuizGradeFromCurrentData()`**:

1. Lấy tất cả records từ `user_quiz_grade` với `status = 6` (bắt buộc)
2. Filter theo `spk_course_id` nếu `course_id` được cung cấp
3. Lưu vào `speakup_user_quiz_grade_tracking` với đầy đủ thông tin (bao gồm score để so sánh sau này)
4. Sử dụng batch insert để tối ưu performance
5. Skip các records đã tracking (unique constraint trên `course_id`, `user_moodle_id`, `spk_cmid`)

**Method `removeTrackedUserQuizGradeFromTable()`**:

1. Lấy danh sách `(course_id, user_moodle_id, spk_cmid, score)` từ tracking table (filter theo course_id nếu được cung cấp)
2. Chunk danh sách để xử lý từng batch
3. Với mỗi record trong tracking:
   - Tìm record trong `user_quiz_grade` có `(spk_course_id, user_moodle_id, spk_cmid)` trùng
   - So sánh `score`: 
     - Nếu score bằng nhau (hoặc cả hai đều NULL): XÓA record
     - Nếu score khác nhau: KHÔNG xóa, giữ lại record mới
4. Sử dụng `whereRaw` với multiple conditions để xóa hiệu quả, kèm điều kiện so sánh score

## Workflow sử dụng

### Quy trình chuẩn:

1. **Bước 1**: Tracking user quiz grade đã sync thành công từ data hiện tại
   ```bash
   POST /api/speakup/track-user-quiz-grade-from-current-data
   {
     "course_id": 45  // optional
   }
   ```
   → Lưu các quiz grades có `status = 6` vào tracking table (kèm score để so sánh)

2. **Bước 2**: Sync data mới từ speakup Moodle
   ```bash
   # Chạy command hoặc API sync user quiz grade
   php artisan sync:user-quiz-grade --course-id=45
   ```
   → Sync data mới vào `user_quiz_grade`

3. **Bước 3**: Xóa tracked user quiz grade khỏi table
   ```bash
   POST /api/speakup/remove-tracked-user-quiz-grade-from-table
   {
     "course_id": 45  // optional
   }
   ```
   → Xóa các records trùng với tracking (có cùng user_moodle_id, spk_course_id, và spk_cmid)
   → CHỈ xóa nếu score không thay đổi (so sánh score trong tracking với score hiện tại)
   → Nếu score thay đổi thì giữ lại record mới
   → Chỉ giữ lại các quiz grades mới, khác với tracking, hoặc có score thay đổi

### Quy trình lặp lại:

Sau khi xóa, có thể:
- Tracking lại sau khi có data mới
- Sync lại và lặp lại quy trình

## Lưu ý quan trọng

1. **Unique constraint**: Bảng tracking có unique constraint trên `(course_id, user_moodle_id, spk_cmid)` để tránh duplicate
2. **Chỉ tracking status = 6**: Chỉ lưu các quiz grades đã update thành công (status = 6), không lưu các trạng thái khác (1, 2, 3, 4, 5, 7)
3. **Score comparison**: Khi xóa, phải so sánh score giữa tracking và data hiện tại:
   - Chỉ xóa nếu score bằng nhau (hoặc cả hai đều NULL)
   - Nếu score khác nhau thì KHÔNG xóa, giữ lại record mới với score mới
4. **Performance**: 
   - Khi xóa nhiều records, sử dụng batch delete hoặc chunk để tránh timeout
   - Cần so sánh score cho từng record, có thể ảnh hưởng performance với số lượng lớn
   - Với số lượng lớn (1 triệu+ records), nên sử dụng background process
5. **Transaction**: Sử dụng database transaction cho các operations quan trọng
6. **Logging**: Log đầy đủ các thao tác để debug, bao gồm số lượng records bị giữ lại do score thay đổi
7. **Tenant connection**: Bảng tracking có thể lưu trong master DB (default connection) để dễ quản lý, hoặc có thể lưu trong tenant DB nếu cần tách biệt theo tenant
8. **Data consistency**: Đảm bảo `(user_moodle_id, spk_course_id, spk_cmid)` trong tracking table match với `user_quiz_grade` table
9. **Composite key**: Sử dụng `(user_moodle_id, spk_course_id, spk_cmid)` để xác định một quiz grade cụ thể

## Migration File Template

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('speakup_user_quiz_grade_tracking', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->integer('course_id')->comment('Course ID (38, 39, 45, 46, 47, 68) - tương đương spk_course_id');
            $table->unsignedBigInteger('user_moodle_id')->comment('User ID từ speakup Moodle');
            $table->unsignedBigInteger('spk_cmid')->comment('Course module ID từ speakup Moodle');
            $table->unsignedBigInteger('spk_quiz_id')->nullable()->comment('Quiz ID từ speakup Moodle (lưu để tham khảo)');
            $table->decimal('score', 10, 2)->nullable()->comment('Điểm số quiz tại thời điểm tracking (dùng để so sánh khi xóa)');
            $table->string('quiz_name')->nullable()->comment('Tên quiz');
            $table->integer('status')->default(6)->comment('Trạng thái (luôn là 6 - completed update)');
            $table->string('tenant_connection')->nullable()->comment('Tenant connection name');
            $table->timestamp('tracked_at')->comment('Thời điểm tracking');
            $table->timestamps();
            
            $table->unique(['course_id', 'user_moodle_id', 'spk_cmid'], 'unique_course_user_cm');
            $table->index('course_id', 'idx_course_id');
            $table->index('user_moodle_id', 'idx_user_moodle_id');
            $table->index('spk_quiz_id', 'idx_spk_quiz_id');
            $table->index('spk_cmid', 'idx_spk_cmid');
            $table->index('tenant_connection', 'idx_tenant_connection');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('speakup_user_quiz_grade_tracking');
    }
};
```

## Model Template

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class SpeakupUserQuizGradeTracking extends Model
{
    use HasFactory;

    protected $table = 'speakup_user_quiz_grade_tracking';

    protected $fillable = [
        'course_id',
        'user_moodle_id',
        'spk_quiz_id',
        'spk_cmid',
        'score',
        'quiz_name',
        'status',
        'tenant_connection',
        'tracked_at',
    ];

    protected $casts = [
        'course_id' => 'integer',
        'user_moodle_id' => 'integer',
        'spk_quiz_id' => 'integer',
        'spk_cmid' => 'integer',
        'score' => 'decimal:2',
        'status' => 'integer',
        'tracked_at' => 'datetime',
    ];
}
```

## Logic xóa records trùng (Chi tiết)

Để xóa các records trùng, cần so sánh trên 3 fields: `user_moodle_id`, `spk_course_id` (từ tracking table là `course_id`), và `spk_cmid`. **QUAN TRỌNG**: Chỉ xóa nếu score không thay đổi.

### Cách 1: So sánh từng record và xóa có điều kiện score

```php
// Lấy danh sách (course_id, user_moodle_id, spk_cmid, score) từ tracking
$trackedRecords = SpeakupUserQuizGradeTracking::where('course_id', $courseId)
    ->select('course_id', 'user_moodle_id', 'spk_cmid', 'score')
    ->get();

$removedCount = 0;
$keptDueToScoreChange = 0;

// Chunk để xử lý từng batch
$trackedRecords->chunk(1000)->each(function ($chunk) use ($tenantConnection, &$removedCount, &$keptDueToScoreChange) {
    foreach ($chunk as $tracked) {
        // Tìm record trong user_quiz_grade
        $quizGrade = UserQuizGrade::on($tenantConnection)
            ->where('spk_course_id', $tracked->course_id)
            ->where('user_moodle_id', $tracked->user_moodle_id)
            ->where('spk_cmid', $tracked->spk_cmid)
            ->first();
        
        if ($quizGrade) {
            // So sánh score
            $scoreMatches = false;
            
            // Xử lý NULL values
            if ($tracked->score === null && $quizGrade->score === null) {
                $scoreMatches = true;
            } elseif ($tracked->score !== null && $quizGrade->score !== null) {
                // So sánh với độ chính xác 2 chữ số thập phân
                $scoreMatches = abs($tracked->score - $quizGrade->score) < 0.01;
            }
            
            // Chỉ xóa nếu score bằng nhau
            if ($scoreMatches) {
                $quizGrade->delete();
                $removedCount++;
            } else {
                $keptDueToScoreChange++;
            }
        }
    }
});
```

### Cách 2: Sử dụng Raw Query với điều kiện score (Hiệu quả hơn - KHUYẾN NGHỊ)

```php
// Lấy danh sách từ tracking
$trackedRecords = SpeakupUserQuizGradeTracking::where('course_id', $courseId)
    ->get();

$removedCount = 0;
$keptDueToScoreChange = 0;

// Chunk để xử lý từng batch
$trackedRecords->chunk(1000)->each(function ($chunk) use ($tenantConnection, &$removedCount, &$keptDueToScoreChange) {
    foreach ($chunk as $tracked) {
        // Build query với điều kiện score
        $query = UserQuizGrade::on($tenantConnection)
            ->where('spk_course_id', $tracked->course_id)
            ->where('user_moodle_id', $tracked->user_moodle_id)
            ->where('spk_cmid', $tracked->spk_cmid);
        
        // Xử lý so sánh score (cả hai NULL hoặc bằng nhau)
        if ($tracked->score === null) {
            $query->whereNull('score');
        } else {
            // So sánh với độ chính xác 2 chữ số thập phân (tránh floating point issues)
            $query->whereRaw('ABS(score - ?) < 0.01', [$tracked->score]);
        }
        
        $deleted = $query->delete();
        if ($deleted > 0) {
            $removedCount += $deleted;
        } else {
            // Có thể record tồn tại nhưng score khác, check lại
            $exists = UserQuizGrade::on($tenantConnection)
                ->where('spk_course_id', $tracked->course_id)
                ->where('user_moodle_id', $tracked->user_moodle_id)
                ->where('spk_cmid', $tracked->spk_cmid)
                ->exists();
            
            if ($exists) {
                $keptDueToScoreChange++;
            }
        }
    }
});
```

### Cách 3: Optimized version với batch processing

```php
$trackedRecords = SpeakupUserQuizGradeTracking::where('course_id', $courseId)
    ->select('course_id', 'user_moodle_id', 'spk_cmid', 'score')
    ->get();

$removedCount = 0;
$keptDueToScoreChange = 0;

// Chunk theo course_id và user_moodle_id để giảm số lượng queries
$trackedRecords->chunk(500)->each(function ($chunk) use ($tenantConnection, &$removedCount, &$keptDueToScoreChange) {
    // Lấy tất cả records từ user_quiz_grade cho chunk này
    $courseIds = $chunk->pluck('course_id')->unique()->toArray();
    $userMoodleIds = $chunk->pluck('user_moodle_id')->unique()->toArray();
    
    $existingGrades = UserQuizGrade::on($tenantConnection)
        ->whereIn('spk_course_id', $courseIds)
        ->whereIn('user_moodle_id', $userMoodleIds)
        ->get()
        ->keyBy(function ($item) {
            return $item->spk_course_id . '_' . $item->user_moodle_id . '_' . $item->spk_cmid;
        });
    
    // So sánh và xóa
    foreach ($chunk as $tracked) {
        $key = $tracked->course_id . '_' . $tracked->user_moodle_id . '_' . $tracked->spk_cmid;
        
        if (isset($existingGrades[$key])) {
            $existing = $existingGrades[$key];
            
            // So sánh score
            $scoreMatches = false;
            if ($tracked->score === null && $existing->score === null) {
                $scoreMatches = true;
            } elseif ($tracked->score !== null && $existing->score !== null) {
                $scoreMatches = abs($tracked->score - $existing->score) < 0.01;
            }
            
            if ($scoreMatches) {
                $existing->delete();
                $removedCount++;
            } else {
                $keptDueToScoreChange++;
            }
        }
    }
});
```

**Lưu ý**: 
- Cách 2 và 3 đều xử lý so sánh score chính xác
- Cách 3 optimized hơn vì giảm số lượng queries bằng cách load batch records vào memory
- Cần đảm bảo có indexes trên `(user_moodle_id, spk_course_id, spk_cmid)` trong cả 2 bảng để tối ưu performance
- So sánh score với độ chính xác 0.01 để tránh vấn đề floating point precision
- Xử lý NULL values đúng cách (cả hai NULL được coi là bằng nhau)

## Testing Checklist

- [ ] Test tracking user quiz grade từ data hiện tại
- [ ] Test chỉ tracking status = 6 (bắt buộc)
- [ ] Test unique constraint (không lưu duplicate khi tracking lại)
- [ ] Test xóa tracked user quiz grade khỏi table
- [ ] Test xóa với số lượng lớn records (100000+)
- [ ] Test với nhiều course_id khác nhau
- [ ] Test với tenant connection
- [ ] Test rollback khi có lỗi (transaction)
- [ ] Test xem danh sách tracking
- [ ] Test xóa tracking data (clear)
- [ ] Test với data empty (không có status = 6)
- [ ] Test xóa với điều kiện score (chỉ xóa khi score không thay đổi)
- [ ] Test giữ lại records có score thay đổi
- [ ] Test xử lý NULL score (cả hai NULL được coi là bằng nhau)
- [ ] Test performance với số lượng lớn (1 triệu+ records)
- [ ] Test với course_id optional (track tất cả courses)

## Future Enhancements

1. **Auto-tracking**: Tự động tracking sau khi sync thành công
2. **Batch operations**: Hỗ trợ batch tracking/remove cho nhiều courses cùng lúc
3. **Statistics**: Thêm API để xem thống kê user quiz grade theo course, user, quiz
4. **Retention policy**: Tự động xóa tracking data cũ sau một thời gian nhất định
5. **Export/Import**: Hỗ trợ export/import tracking data
6. **Dry-run mode**: Thêm mode để preview các records sẽ bị xóa trước khi thực sự xóa
7. **Background processing**: Hỗ trợ background process cho các operations lớn
8. **Comparison report**: API để so sánh data tracking với data hiện tại trong user_quiz_grade table

