# EsimLao Authentication API Documentation

## Overview
API xác thực người dùng qua email với OTP (One-Time Password).
URL_UAT : http://149.28.132.56:8360/
---

## 1. Request OTP

Gửi mã OTP đến email người dùng để xác thực đăng nhập.

### Endpoint
```
POST /apis/auth/request-otp
```

### Request Headers
| Header | Value | Required |
|--------|-------|----------|
| Content-Type | application/json | Yes |

### Request Body
```json
{
    "email": "user@example.com",
    "lang": "lo"  // Default: "lo" / en
}
```

### Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| email | string | Yes | - | Email address của người dùng |
| lang | string | No | "vi" | Ngôn ngữ email: `vi` (Tiếng Việt), `en` (English), `lo` (ລາວ) |

### Response Success (200)
```json
{
    "errorCode": "0",
    "message": "<Config: OTP_SENT_SUCCESS>",
    "data": {
        "email": "user@example.com",
        "expireInSeconds": 300
    }
}
```

### Response Error (200)
```json
// Email không được cung cấp
{
    "errorCode": "-801",
    "message": "<Config: EMAIL_REQUIRED>",
    "data": {}
}

// Lỗi hệ thống
{
    "errorCode": "-6",
    "message": "<Config: SYSTEM_FAILURE>",
    "data": {}
}
```

### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| errorCode| string | "0" = Success, khác "0" = Error (xem Error Codes) |
| message | string | Thông báo từ CONFIG table (theo ngôn ngữ) |
| data.email | string | Email đã gửi OTP |
| data.expireInSeconds | int | Thời gian OTP hết hạn (giây) |

### Notes
- OTP gồm 6 chữ số
- OTP có hiệu lực trong 5 phút
- Mỗi lần request mới sẽ hủy các OTP cũ chưa sử dụng
- Nếu email chưa tồn tại, hệ thống tự động tạo tài khoản mới

---

## 1.1 Resend OTP

Gửi lại mã OTP mới đến email (hủy OTP cũ).

### Endpoint
```
POST /apis/auth/resend-otp
```

### Request Headers
| Header | Value | Required |
|--------|-------|----------|
| Content-Type | application/json | Yes |

### Request Body
```json
{
    "email": "user@example.com",
    "lang": "lo"  // Default: "lo" / en / vi
}
```

### Response
Tương tự **Request OTP**.

---

## 2. Verify OTP

Xác thực mã OTP và hoàn tất đăng nhập.

### Endpoint
```
POST /apis/auth/verify-otp
```

### Request Headers
| Header | Value | Required |
|--------|-------|----------|
| Content-Type | application/json | Yes |

### Request Body
```json
{
    "email": "user@example.com",
    "otpCode": "123456",
    "lang": "lo"  // Default: "lo" / en
}
```

### Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| email | string | Yes | - | Email đã nhận OTP |
| otpCode | string | Yes | - | Mã OTP 6 số |
| lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) |

### Response Success (200)
```json
{
    "errorCode": "0",
    "message": "<Config: LOGIN_SUCCESS>",
    "data": {
        "userId": 12345,
        "email": "user@example.com",
        "fullName": "Nguyen Van A",
        "avatarUrl": "https://example.com/avatar.jpg",
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
        "expiresAt": "2024-12-30T10:00:00Z"
    }
}
```

### Response Error Cases
```json
// Thiếu email hoặc OTP
{
    "errorCode": "-801",
    "message": "<Config: EMAIL_OTP_REQUIRED>",
    "data": {}
}

// OTP không hợp lệ
{
    "errorCode": "-201",
    "message": "<Config: OTP_INVALID>",
    "data": {}
}

// OTP đã được sử dụng
{
    "errorCode": "-203",
    "message": "<Config: OTP_ALREADY_USED>",
    "data": {}
}

// OTP đã hết hạn
{
    "errorCode": "-202",
    "message": "<Config: OTP_EXPIRED>",
    "data": {}
}

// Không tìm thấy người dùng
{
    "errorCode": "-300",
    "message": "<Config: USER_NOT_FOUND>",
    "data": {}
}

// Lỗi hệ thống
{
    "errorCode": "-6",
    "message": "<Config: SYSTEM_FAILURE>",
    "data": {}
}
```

### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| errorCode| string | "0" = Success, khác "0" = Error (xem Error Codes) |
| message | string | Thông báo từ CONFIG table (theo ngôn ngữ) |
| data.userId | int | ID người dùng |
| data.email | string | Email người dùng |
| data.fullName | string | Họ tên đầy đủ |
| data.avatarUrl | string | URL ảnh đại diện (nullable) |
| data.accessToken | string | JWT access token |
| data.refreshToken | string | Refresh token để làm mới access token |
| data.expiresAt | datetime | Thời điểm access token hết hạn |

### Notes
- Access token có hiệu lực 24 giờ
- Refresh token có hiệu lực 30 ngày
- Mỗi lần đăng nhập thành công, các token cũ sẽ bị thu hồi

---

## 2.1 Google Login - Get Authorization URL

Lấy URL để redirect người dùng đến Google OAuth consent screen.

### Endpoint
```
POST /apis/auth/google-login
```

### Request Headers
| Header | Value | Required |
|--------|-------|----------|
| Content-Type | application/json | Yes |

### Request Body
```json
{
    "lang": "lo"  // Optional: "lo" (default), "en"
}
```

### Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) |

### Response Success (200)
```json
{
    "errorCode": "0",
    "message": "<Config: SUCCESS>",
    "data": {
        "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx&redirect_uri=xxx&response_type=code&scope=email%20profile"
    }
}
```

### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| errorCode | string | "0" = Success |
| message | string | Thông báo từ CONFIG table (theo ngôn ngữ) |
| data.url | string | URL để redirect user đến Google OAuth |

### Flow
1. Frontend gọi API này để lấy Google OAuth URL
2. Frontend redirect user đến URL nhận được
3. User đăng nhập Google và cho phép quyền
4. Google redirect về `redirect_uri` với `code` parameter
5. Frontend gọi API `/apis/auth/google-callback` với `code` này

### Response Error
```json
{
    "errorCode": "-6",
    "message": "<Config: GOOGLE_CONFIG_MISSING>",
    "data": {}
}
```

---

## 2.2 Google Callback - Complete Login

Xác thực authorization code từ Google và hoàn tất đăng nhập.
- Nếu email chưa tồn tại: Tự động tạo tài khoản mới trong `CUSTOMER_INFO`
- Nếu email đã tồn tại: Cập nhật thông tin và đăng nhập

### Endpoint
```
POST /apis/auth/google-callback
```

### Request Headers
| Header | Value | Required |
|--------|-------|----------|
| Content-Type | application/json | Yes |

### Request Body
```json
{
    "code": "4/0AXEWy...",
    "redirectUri": "https://your-app.com/callback",
    "lang": "lo"  // Optional: "lo" (default), "en"
}
```

### Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| code | string | Yes | - | Authorization code từ Google (nhận qua URL callback) |
| redirectUri | string | No | - | Redirect URI đã đăng ký với Google (nếu khác default) |
| lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) |

### Response Success (200) - GIỐNG API verify-otp
```json
{
    "errorCode": "0",
    "message": "<Config: GOOGLE_LOGIN_SUCCESS>",
    "data": {
        "userId": 12345,
        "email": "user@gmail.com",
        "fullName": "Nguyen Van A",
        "avatarUrl": "https://lh3.googleusercontent.com/...",
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
        "expiresAt": "2024-12-30T10:00:00Z"
    }
}
```

### Response Fields (giống verify-otp)
| Field | Type | Description |
|-------|------|-------------|
| errorCode | string | "0" = Success |
| message | string | Thông báo từ CONFIG table (theo ngôn ngữ) |
| data.userId | int | ID người dùng (từ CUSTOMER_INFO.ID) |
| data.email | string | Email người dùng |
| data.fullName | string | Họ tên đầy đủ (từ Google profile) |
| data.avatarUrl | string | URL ảnh đại diện từ Google |
| data.accessToken | string | JWT access token (24 giờ) |
| data.refreshToken | string | Refresh token (30 ngày) |
| data.expiresAt | datetime | Thời điểm access token hết hạn |

### Database Operations
Khi login thành công:
1. **CUSTOMER_INFO**: 
   - User mới: INSERT với `SUR_NAME`, `LAST_NAME`, `EMAIL`, `AVATAR_URL`, `IS_VERIFIED=1`
   - User có sẵn: UPDATE `AVATAR_URL` (nếu trống), `LAST_LOGIN_DATE`, `IS_VERIFIED=1`
2. **USER_TOKEN**: Revoke tokens cũ và tạo token mới

### Response Error Cases
```json
// Code không được cung cấp
{
    "errorCode": "-801",
    "message": "<Config: GOOGLE_CODE_REQUIRED>",
    "data": {}
}

// Lỗi trao đổi token với Google
{
    "errorCode": "-700",
    "message": "<Config: GOOGLE_TOKEN_EXCHANGE_FAILED>",
    "data": { "error": "..." }
}

// Không nhận được email từ Google
{
    "errorCode": "-700",
    "message": "<Config: GOOGLE_NO_EMAIL>",
    "data": {}
}

// Lỗi hệ thống
{
    "errorCode": "-6",
    "message": "<Config: SYSTEM_FAILURE>",
    "data": {}
}
```

### Notes
- Access token có hiệu lực 24 giờ
- Refresh token có hiệu lực 30 ngày
- Mỗi lần đăng nhập thành công, các token cũ sẽ bị thu hồi
- User đăng nhập qua Google tự động được đánh dấu `IS_VERIFIED = 1`
- Nếu user chưa có avatar và Google cung cấp, sẽ tự động lưu

---

## Google OAuth Flow Diagram

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │     │   API       │     │   Google    │     │   Database  │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │                   │
       │ POST /google-login│                   │                   │
       │──────────────────>│                   │                   │
       │                   │                   │                   │
       │   { url: "..." }  │                   │                   │
       │<──────────────────│                   │                   │
       │                   │                   │                   │
       │  Redirect to URL  │                   │                   │
       │──────────────────────────────────────>│                   │
       │                   │                   │                   │
       │                   │                   │ User Login        │
       │                   │                   │ Grant Permission  │
       │                   │                   │                   │
       │ Callback with code│                   │                   │
       │<──────────────────────────────────────│                   │
       │                   │                   │                   │
       │ POST /google-callback                 │                   │
       │    { code: "..." }│                   │                   │
       │──────────────────>│                   │                   │
       │                   │                   │                   │
       │                   │ Exchange code     │                   │
       │                   │──────────────────>│                   │
       │                   │                   │                   │
       │                   │ access_token      │                   │
       │                   │<──────────────────│                   │
       │                   │                   │                   │
       │                   │ Get user info     │                   │
       │                   │──────────────────>│                   │
       │                   │                   │                   │
       │                   │ email, name, pic  │                   │
       │                   │<──────────────────│                   │
       │                   │                   │                   │
       │                   │ Insert/Update CUSTOMER_INFO           │
       │                   │ Create USER_TOKEN                     │
       │                   │──────────────────────────────────────>│
       │                   │                   │                   │
       │  JWT Token Response                   │                   │
       │<──────────────────│                   │                   │
       │                   │                   │                   │
```

---

## Example Usage (cURL) - Google Login

### Step 1: Get Google OAuth URL
```bash
curl -X POST http://149.28.132.56:8360/apis/auth/google-login \
  -H "Content-Type: application/json"
```

### Step 2: Complete Login with Code
```bash
curl -X POST http://149.28.132.56:8360/apis/auth/google-callback \
  -H "Content-Type: application/json" \
  -d '{
    "code": "4/0AXEWy..."
  }'
```

---

## Error Codes

### Success
| errorCode| Constant | Description |
|------|----------|-------------|
| "0" | Success | Thành công (mọi request thành công đều trả về "0") |

### General Errors (-1 to -99)
| errorCode| Constant | Description |
|------|----------|-------------|
| "-1" | Error | Lỗi chung |
| "-6" | SystemError | Lỗi hệ thống |

### OTP Errors (-200 to -299)
| errorCode| Constant | Description |
|------|----------|-------------|
| "-200" | OtpRequired | Yêu cầu OTP |
| "-201" | OtpInvalid | OTP không hợp lệ |
| "-202" | OtpExpired | OTP đã hết hạn |
| "-203" | OtpAlreadyUsed | OTP đã được sử dụng |
| "-204" | OtpMaxAttemptsExceeded | Vượt quá số lần thử |
| "-205" | OtpSendFailed | Gửi OTP thất bại |
| "-206" | OtpTooManyRequests | Request quá nhiều |

### User Errors (-300 to -399)
| errorCode| Constant | Description |
|------|----------|-------------|
| "-300" | UserNotFound | Không tìm thấy người dùng |
| "-304" | InvalidEmail | Email không hợp lệ |

### External Service Errors (-700 to -799)
| errorCode| Constant | Description |
|------|----------|-------------|
| "-700" | ExternalServiceError | Lỗi từ dịch vụ bên ngoài (Google OAuth, etc.) |

### Validation Errors (-800 to -899)
| errorCode| Constant | Description |
|------|----------|-------------|
| "-801" | RequiredFieldMissing | Thiếu trường bắt buộc |

---

## Authentication

Sau khi đăng nhập thành công, sử dụng `accessToken` trong header cho các API yêu cầu xác thực:

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

---

## Flow Diagram

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │     │   API       │     │   Email     │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │
       │ POST /request-otp │                   │
       │──────────────────>│                   │
       │                   │                   │
       │                   │ Generate OTP      │
       │                   │ Save to DB        │
       │                   │ Queue Email       │
       │                   │                   │
       │    Response       │                   │
       │<──────────────────│                   │
       │                   │                   │
       │                   │  Send OTP Email   │
       │                   │──────────────────>│
       │                   │                   │
       │                   │                   │ OTP Email
       │<──────────────────────────────────────│
       │                   │                   │
       │ POST /verify-otp  │                   │
       │──────────────────>│                   │
       │                   │                   │
       │                   │ Verify OTP        │
       │                   │ Generate JWT      │
       │                   │                   │
       │ Token Response    │                   │
       │<──────────────────│                   │
       │                   │                   │
```

---

## Example Usage (cURL)

### Request OTP
```bash
curl -X POST https://api.esimlao.com/apis/auth/request-otp \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "lang": "vi"
  }'
```

### Verify OTP
```bash
curl -X POST https://api.esimlao.com/apis/auth/verify-otp \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "otpCode": "123456"
  }'
```

### Use Token
```bash
curl -X GET https://api.esimlao.com/apis/user/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```

---

## 3. Article Category

Lấy danh sách danh mục bài viết.

### Endpoint
```
POST /apis/article/category
```

### Request
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10,
    "parentId": null
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| lang | string | "lo" | Ngôn ngữ: "lo", "en" (có thể truyền qua header Accept-Language) |
| pageNumber | int | 0 | Trang hiện tại |
| pageSize | int | 10 | Số item mỗi trang |
| parentId | int? | null | ID danh mục cha (null = root) |

### Response Success
```json
{
    "errorCode": "0",
    "message": "Success",
    "data": {
        "categories": [
            {
                "id": 1,
                "categoryName": "Cẩm nang du lịch",
                "categorySlug": "cam-nang-du-lich",
                "description": "Mô tả...",
                "iconUrl": "/icons/travel.png",
                "parentId": null,
                "displayOrder": 1
            }
        ],
        "pagination": {
            "pageNumber": 0,
            "pageSize": 10,
            "totalCount": 5,
            "totalPages": 1
        }
    }
}
```

### Response Fields - categories[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | ARTICLE_CATEGORY.ID | ID danh mục bài viết (Primary Key) |
| categoryName | string | ARTICLE_CATEGORY.CATEGORY_NAME<br/>CATEGORY_NAME_EN<br/>CATEGORY_NAME_LO | Tên danh mục (theo ngôn ngữ được chọn) |
| categorySlug | string | ARTICLE_CATEGORY.CATEGORY_SLUG | URL-friendly slug cho danh mục |
| description | string | ARTICLE_CATEGORY.DESCRIPTION<br/>DESCRIPTION_EN<br/>DESCRIPTION_LO | Mô tả chi tiết danh mục |
| iconUrl | string | ARTICLE_CATEGORY.ICON_URL | Đường dẫn icon của danh mục |
| parentId | int? | ARTICLE_CATEGORY.PARENT_ID | ID danh mục cha (null = danh mục gốc) |
| displayOrder | int | ARTICLE_CATEGORY.DISPLAY_ORDER | Thứ tự hiển thị (số nhỏ hơn hiển thị trước) |

### Response Fields - pagination
| Field | Type | Description |
|-------|------|-------------|
| pageNumber | int | Trang hiện tại (bắt đầu từ 0) |
| pageSize | int | Số items trên mỗi trang |
| totalCount | int | Tổng số items trong database |
| totalPages | int | Tổng số trang (= ceiling(totalCount / pageSize)) |

---

## 4. Article Load (List)

Lấy danh sách bài viết với pagination và filters.

### Endpoint
```
POST /apis/article/load
```

### Request (tất cả bài viết)
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10
}
```

### Request (lọc theo category)
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10,
    "categoryId": 1,
    "isFeatured": false
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) |
| pageNumber | int | 0 | Trang hiện tại (0-indexed) |
| pageSize | int | 10 | Số item mỗi trang |
| categoryId | int? | **null** | **null = lấy tất cả**, có giá trị = lọc theo danh mục |
| isFeatured | bool? | null | true = chỉ bài nổi bật, null = tất cả |

### Response Success
```json
{
    "errorCode": "0",
    "message": "Success",
    "data": {
        "articles": [
            {
                "id": 1,
                "title": "Hướng dẫn cài đặt eSIM",
                "slug": "huong-dan-cai-dat-esim",
                "summary": "Tóm tắt...",
                "thumbnailUrl": "/images/article1.jpg",
                "categoryId": 1,
                "viewCount": 150,
                "isFeatured": true,
                "isPinned": false,
                "publishedDate": "2024-12-25"
            }
        ],
        "pagination": {
            "pageNumber": 0,
            "pageSize": 10,
            "totalCount": 25,
            "totalPages": 3
        }
    }
}
```

### Response Fields - articles[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | ARTICLE.ID | **ID bài viết** - dùng để lấy chi tiết qua `/apis/article/detail` |
| title | string | ARTICLE.TITLE<br/>TITLE_EN<br/>TITLE_LO | Tiêu đề bài viết (theo ngôn ngữ) |
| slug | string | ARTICLE.SLUG | URL-friendly slug (unique) |
| summary | string | ARTICLE.SUMMARY<br/>SUMMARY_EN<br/>SUMMARY_LO | Tóm tắt ngắn gọn |
| thumbnailUrl | string | ARTICLE.THUMBNAIL_URL | Ảnh thumbnail (dùng cho list) |
| categoryId | int | ARTICLE.CATEGORY_ID | ID danh mục (FK → ARTICLE_CATEGORY) |
| viewCount | int | ARTICLE.VIEW_COUNT | Số lượt xem |
| isFeatured | bool | ARTICLE.IS_FEATURED | Bài viết nổi bật (true/false) |
| isPinned | bool | ARTICLE.IS_PINNED | Bài viết được ghim (true/false) |
| publishedDate | datetime | ARTICLE.PUBLISHED_DATE | Ngày xuất bản |

### Important Notes
- **Ordering**: Danh sách sắp xếp theo:
  1. `IS_PINNED` DESC (bài ghim lên đầu)
  2. `PUBLISHED_DATE` DESC (mới nhất trước)
  3. `CREATED_DATE` DESC
- **Filter logic**:
  - `categoryId = null` → Lấy **tất cả** bài viết
  - `categoryId = 1` → Chỉ lấy bài viết thuộc category 1

### cURL Examples

#### Lấy tất cả bài viết
```bash
curl -X POST http://localhost:8360/apis/article/load \
  -H "Content-Type: application/json" \
  -d '{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10
  }'
```

#### Lấy bài viết theo category
```bash
curl -X POST http://localhost:8360/apis/article/load \
  -H "Content-Type: application/json" \
  -d '{
    "categoryId": 1,
    "lang": "en",
    "pageSize": 5
  }'
```

#### Lấy bài viết nổi bật
```bash
curl -X POST http://localhost:8360/apis/article/load \
  -H "Content-Type: application/json" \
  -d '{
    "isFeatured": true,
    "lang": "lo",
    "pageSize": 6
  }'
```

---

## 4.1 Article Detail

Lấy chi tiết 1 bài viết theo ID hoặc slug.

### Endpoint
```
POST /apis/article/detail
```

### Request (theo ID - khuyến nghị)
```json
{
    "id": 1,
    "lang": "lo"
}
```

### Request (theo slug - SEO friendly)
```json
{
    "slug": "huong-dan-cai-dat-esim",
    "lang": "lo"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | int? | Conditional | ID bài viết (lấy từ list API) |
| slug | string? | Conditional | Slug của bài viết |
| lang | string | No | Ngôn ngữ (default: "lo") |

**Note**: Phải có **ít nhất một** trong `id` hoặc `slug`

### Response Success
```json
{
    "errorCode": "0",
    "message": "Success",
    "data": {
        "article": {
            "id": 1,
            "title": "Hướng dẫn cài đặt eSIM",
            "slug": "huong-dan-cai-dat-esim",
            "summary": "Tóm tắt...",
            "content": "<p>Nội dung HTML đầy đủ...</p>",
            "thumbnailUrl": "/images/article1.jpg",
            "coverImageUrl": "/images/cover1.jpg",
            "metaDescription": "SEO description",
            "metaKeywords": "esim, laos",
            "categoryId": 1,
            "viewCount": 151,
            "isFeatured": true,
            "publishedDate": "2024-12-25",
            "createdDate": "2024-12-20"
        }
    }
}
```

### Response Fields - article
| Field | Type | DB Column | Description |
|-------|------|----------|-------------|
| id | int | ARTICLE.ID | ID bài viết (Primary Key) |
| title | string | ARTICLE.TITLE<br/>TITLE_EN<br/>TITLE_LO | Tiêu đề bài viết (theo ngôn ngữ) |
| slug | string | ARTICLE.SLUG | URL-friendly slug (unique) |
| summary | string | ARTICLE.SUMMARY<br/>SUMMARY_EN<br/>SUMMARY_LO | Tóm tắt ngắn gọn |
| content | string | ARTICLE.CONTENT<br/>CONTENT_EN<br/>CONTENT_LO | **Nội dung HTML đầy đủ** |
| thumbnailUrl | string | ARTICLE.THUMBNAIL_URL | Ảnh thumbnail |
| coverImageUrl | string | ARTICLE.COVER_IMAGE_URL | Ảnh bìa (dùng cho detail page) |
| metaDescription | string | ARTICLE.META_DESCRIPTION<br/>META_DESCRIPTION_EN<br/>META_DESCRIPTION_LO | SEO meta description |
| metaKeywords | string | ARTICLE.META_KEYWORDS | SEO keywords (comma-separated) |
| categoryId | int | ARTICLE.CATEGORY_ID | ID danh mục |
| viewCount | int | ARTICLE.VIEW_COUNT | Số lượt xem (**tự động +1 khi gọi API này**) |
| isFeatured | bool | ARTICLE.IS_FEATURED | Bài viết nổi bật |
| publishedDate | datetime | ARTICLE.PUBLISHED_DATE | Ngày xuất bản |
| createdDate | datetime | ARTICLE.CREATED_DATE | Ngày tạo bài viết |

### Important Notes
- **Auto view count**: Mỗi lần gọi API này, `viewCount` tự động +1
- **Recommended flow**:
  1. Gọi `/apis/article/load` → Lấy `id` từ danh sách
  2. Gọi `/apis/article/detail` với `id` đó

### cURL Examples

#### Lấy chi tiết theo ID
```bash
curl -X POST http://localhost:8360/apis/article/detail \
  -H "Content-Type: application/json" \
  -d '{
    "id": 1,
    "lang": "lo"
  }'
```

#### Lấy chi tiết theo slug  
```bash
curl -X POST http://localhost:8360/apis/article/detail \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "huong-dan-cai-dat-esim",
    "lang": "en"
  }'
```

---

## 5. Banner Load

Lấy danh sách banner.

### Endpoint
```
POST /apis/content/banner
```

### Request
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10,
    "position": "home"
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) |
| pageNumber | int | 0 | Trang hiện tại |
| pageSize | int | 10 | Số item mỗi trang |
| position | string? | null | Vị trí: "home", "sidebar"... |

### Response
```json
{
    "errorCode": "0",
    "data": {
        "banners": [
            {
                "id": 1,
                "title": "Banner Title",
                "subtitle": "Subtitle",
                "imageUrl": "/images/banner1.jpg",
                "imageMobileUrl": "/images/banner1_m.jpg",
                "linkUrl": "/promo",
                "linkTarget": "_blank",
                "position": "home",
                "displayOrder": 1
            }
        ],
        "pagination": {...}
    }
}
```

### Response Fields - banners[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | BANNER.ID | ID banner (Primary Key) |
| title | string | BANNER.TITLE<br/>TITLE_EN<br/>TITLE_LO | Tiêu đề banner (theo ngôn ngữ) |
| subtitle | string | BANNER.SUBTITLE<br/>SUBTITLE_EN<br/>SUBTITLE_LO | Phụ đề banner |
| imageUrl | string | BANNER.IMAGE_URL | Ảnh banner (desktop) |
| imageMobileUrl | string | BANNER.IMAGE_MOBILE_URL | Ảnh banner (mobile) |
| linkUrl | string | BANNER.LINK_URL | URL đích khi click banner |
| linkTarget | string | BANNER.LINK_TARGET | Target (_self, _blank,...) |
| position | string | BANNER.POSITION | Vị trí hiển thị (home, category,...) |
| displayOrder | int | BANNER.DISPLAY_ORDER | Thứ tự hiển thị |

**Lọc tự động**: Chỉ trả về banners với `STATUS = 1` và trong khoảng `START_DATE ≤ now ≤ END_DATE`

---

## 6. Customer Review Load

Lấy đánh giá của khách hàng.

### Endpoint
```
POST /apis/content/review
```

### Request
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10,
    "isFeatured": true
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| lang | string | "lo" | Ngôn ngữ |
| pageNumber | int | 0 | Trang |
| pageSize | int | 10 | Số item |
| isFeatured | bool? | null | Lọc review nổi bật |

### Response
```json
{
    "errorCode": "0",
    "data": {
        "reviews": [
            {
                "id": 1,
                "customerName": "Nguyen Van A",
                "avatarUrl": "/avatars/user1.jpg",
                "rating": 1,
                "reviewContent": "Dich vu rat tot...",
                "destination": "Vientiane, Laos",
                "isFeatured": true,
                "createdDate": "2024-12-25"
            }
        ],
        "pagination": {...}
    }
}
```

### Response Fields - reviews[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | CUSTOMER_REVIEW.ID | ID đánh giá (Primary Key) |
| customerName | string | CUSTOMER_REVIEW.CUSTOMER_NAME | Tên khách hàng |
| avatarUrl | string | CUSTOMER_REVIEW.AVATAR_URL | Ảnh đại diện (nullable) |
| rating | int | CUSTOMER_REVIEW.RATING | Số sao (1-5) |
| reviewContent | string | CUSTOMER_REVIEW.REVIEW_CONTENT<br/>REVIEW_CONTENT_EN<br/>REVIEW_CONTENT_LO | Nội dung đánh giá (theo ngôn ngữ) |
| destination | string | CUSTOMER_REVIEW.DESTINATION<br/>DESTINATION_EN<br/>DESTINATION_LO | Địa điểm du lịch |
| isFeatured | bool | CUSTOMER_REVIEW.IS_FEATURED | Review nổi bật |
| createdDate | datetime | CUSTOMER_REVIEW.CREATED_DATE | Ngày tạo review |

**Lọc tự động**: Chỉ trả về reviews với `STATUS = 1` (đã duyệt)

---

## 6.1 Customer Review Create

Khách hàng gửi đánh giá (chờ duyệt).

### Endpoint
```
POST /apis/content/review/create
```

### Request
```json
{
    "lang": "lo",
    "customerName": "Nguyen Van A",
    "reviewContent": "Dich vu rat tot, toi rat hai long!",
    "destination": "Vientiane, Laos",
    "rating": 5
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| lang | string | No | Ngôn ngữ |
| customerName | string | Yes | Tên khách hàng |
| reviewContent | string | Yes | Nội dung đánh giá |
| destination | string | No | Địa điểm |
| rating | int | No | Đánh giá (1-5) |

### Response Success
```json
{
    "errorCode": "0",
    "message": "Review submitted successfully",
    "data": {
        "reviewId": 123
    }
}
```

### Note
- Review mới sẽ có `Status = false` (chờ admin duyệt)

---

## 7. FAQ Category Load

Lấy danh mục FAQ (hỗ trợ cấu trúc phân cấp).

### Endpoint
```
POST /apis/content/faq-category
```

### Request
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10,
    "parentId": null
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) |
| pageNumber | int | 0 | Trang hiện tại |
| pageSize | int | 10 | Số item mỗi trang |
| parentId | int? | null | ID danh mục cha (null = lấy root categories) |

### Response
```json
{
    "errorCode": "0",
    "data": {
        "categories": [
            {
                "id": 1,
                "categoryName": "Cài đặt eSIM",
                "categorySlug": "cai-dat-esim",
                "description": "Hướng dẫn cài đặt",
                "iconUrl": "/icons/setup.png",
                "parentId": null,
                "displayOrder": 1
            }
        ],
        "pagination": {...}
    }
}
```

### Response Fields - categories[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | FAQ_CATEGORY.ID | ID danh mục FAQ (Primary Key) |
| categoryName | string | FAQ_CATEGORY.CATEGORY_NAME<br/>CATEGORY_NAME_EN<br/>CATEGORY_NAME_LO | Tên danh mục FAQ |
| categorySlug | string | FAQ_CATEGORY.CATEGORY_SLUG | URL-friendly slug |
| description | string | FAQ_CATEGORY.DESCRIPTION<br/>DESCRIPTION_EN<br/>DESCRIPTION_LO | Mô tả danh mục |
| iconUrl | string | FAQ_CATEGORY.ICON_URL | Icon của danh mục |
| parentId | int? | FAQ_CATEGORY.PARENT_ID | **ID danh mục cha (null = danh mục gốc)** |
| displayOrder | int | FAQ_CATEGORY.DISPLAY_ORDER | Thứ tự hiển thị |

### Use Cases
- **Homepage FAQ**: `parentId = null` → Lấy danh mục gốc
- **Support Center**: `parentId = <categoryId>` → Lấy danh mục con
- Hỗ trợ cấu trúc phân cấp không giới hạn cấp độ

---

## 8. FAQ Load

Lấy danh sách câu hỏi thường gặp.

### Endpoint
```
POST /apis/content/faq
```

### Request
```json
{
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 10,
    "categoryId": 1,
    "isFeatured": false
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| lang | string | "lo" | Ngôn ngữ |
| pageNumber | int | 0 | Trang |
| pageSize | int | 10 | Số item |
| categoryId | int? | null | Lọc theo danh mục |
| isFeatured | bool? | null | Lọc FAQ nổi bật |

### Response
```json
{
    "errorCode": "0",
    "data": {
        "faqs": [
            {
                "id": 1,
                "question": "Làm sao để cài đặt eSIM?",
                "answer": "<p>Hướng dẫn chi tiết...</p>",
                "categoryId": 1,
                "viewCount": 100,
                "isFeatured": true
            }
        ],
        "pagination": {...}
    }
}
```

### Response Fields - faqs[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | FAQ.ID | ID câu hỏi (Primary Key) |
| question | string | FAQ.QUESTION<br/>QUESTION_EN<br/>QUESTION_LO | Câu hỏi (theo ngôn ngữ) |
| answer | string | FAQ.ANSWER<br/>ANSWER_EN<br/>ANSWER_LO | Câu trả lời (HTML format) |
| categoryId | int | FAQ.CATEGORY_ID | ID danh mục (FK → FAQ_CATEGORY) |
| viewCount | int | FAQ.VIEW_COUNT | Số lượt xem |
| isFeatured | bool | FAQ.IS_FEATURED | FAQ nổi bật |

**Lọc tự động**: Chỉ trả về FAQs với `STATUS = 1`

---

## 9. Device eSIM Compatibility - Get Metadata

Lấy danh sách brands và categories để hiển thị tabs/filters cho tính năng kiểm tra thiết bị.

### Endpoint
```
GET /apis/content/device-metadata
```

### Request
Không cần body (GET request)

### Response
```json
{
    "errorCode": "0",
    "message": "Success",
    "data": {
        "brands": [
            {
                "brand": "Apple",
                "deviceCount": 24,
                "popularCount": 24,
                "devices": [
                    {
                        "id": 1,
                        "modelName": "iPhone XS",
                        "category": "Phone",
                        "isPopular": true,
                        "displayOrder": 1,
                        "modelNameEn": "iPhone XS",
                        "modelNameLo": "iPhone XS",
                        "notes": "Tất cả phiên bản trừ China Mainland, Hong Kong, Macao",
                        "notesEn": "All versions except China Mainland, Hong Kong, Macao",
                        "notesLo": "ທຸກເວີຊັນຍົກເວັ້ນ China Mainland, Hong Kong, Macao"
                    }
                ]
            },
            {
                "brand": "Samsung",
                "deviceCount": 19,
                "popularCount": 19,
                "devices": [...]
            }
        ],
        "categories": [
            {
                "category": "Phone",
                "deviceCount": 67
            },
            {
                "category": "Tablet",
                "deviceCount": 4
            }
        ]
    }
}
```

### Response Fields - brands[]
| Field | Type | Description |
|-------|------|-------------|
| brand | string | Tên hãng (Apple, Samsung, Google,...) |
| deviceCount | int | Tổng số thiết bị của hãng này |
| popularCount | int | Số thiết bị phổ biến (IS_POPULAR = 1) |
| **devices[]** | array | **Danh sách tất cả thiết bị của brand** |

### Response Fields - brands[].devices[]
| Field | Type | Description |
|-------|------|-------------|
| id | int | ID thiết bị |
| modelName | string | Tên model (mặc định Vietnamese) |
| category | string | Loại thiết bị (Phone, Tablet, Laptop, Watch) |
| isPopular | bool | Thiết bị phổ biến |
| displayOrder | int | Thứ tự sắp xếp |
| modelNameEn | string | Tên model tiếng Anh |
| modelNameLo | string | Tên model tiếng Lào |
| notes | string | Ghi chú (Vietnamese) |
| notesEn | string | Ghi chú (English) |
| notesLo | string | Ghi chú (Lao) |

### Response Fields - categories[]
| Field | Type | Description |
|-------|------|-------------|
| category | string | Loại thiết bị (Phone, Tablet, Laptop, Watch) |
| deviceCount | int | Tổng số thiết bị thuộc category này |

### Use Case
API này trả về **TẤT CẢ** devices trong một lần gọi để:
- Frontend có thể filter/search ở client-side
- Hiển thị tabs cho từng brand với danh sách devices
- Không cần pagination vì dùng cho client-side filtering
- Giảm số lượng API calls

### Important Notes
- **Trả về toàn bộ devices** trong response (không phân trang)
- Frontend tự filter theo brand/category/search
- Chỉ devices có `STATUS = 1` và `SUPPORTS_ESIM = 1`
- Sắp xếp theo `DISPLAY_ORDER`, `BRAND`, `MODEL_NAME`

---

## 10. Device eSIM Compatibility - Search & Filter

Tìm kiếm và lọc danh sách thiết bị hỗ trợ eSIM.

### Endpoint
```
POST /apis/content/device-compatibility
```

### Request Examples

#### Example 1: Lấy tất cả devices phổ biến (Quick Lookup)
```json
{
    "isPopular": true,
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 50
}
```

#### Example 2: Filter theo brand (Tab Apple)
```json
{
    "brand": "Apple",
    "isPopular": true,
    "lang": "en",
    "pageNumber": 0,
    "pageSize": 50
}
```

#### Example 3: Search keyword
```json
{
    "searchKeyword": "iPhone 13",
    "lang": "lo",
    "pageNumber": 0,
    "pageSize": 20
}
```

#### Example 4: Filter brand + category
```json
{
    "brand": "Apple",
    "category": "Tablet",
    "lang": "en",
    "pageNumber": 0,
    "pageSize": 10
}
```

### Request Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| brand | string | No | null | Filter theo hãng (Apple, Samsung, Google,...) |
| category | string | No | null | Filter theo loại (Phone, Tablet, Laptop, Watch) |
| searchKeyword | string | No | null | Tìm kiếm trong tên model (all languages) |
| isPopular | bool | No | null | `true` = chỉ lấy thiết bị phổ biến (quick lookup) |
| lang | string | No | "lo" | Ngôn ngữ response: "lo", "en" |
| pageNumber | int | No | 0 | Trang hiện tại (0-indexed) |
| pageSize | int | No | 50 | Số items/page (mặc định 50 cho device list) |

### Response Success
```json
{
    "errorCode": "0",
    "message": "Success",
    "data": {
        "devices": [
            {
                "id": 1,
                "brand": "Apple",
                "modelName": "iPhone XS",
                "category": "Phone",
                "notes": "ທຸກເວີຊັນຍົກເວັ້ນ iPhone ຈາກ ຈີນແຜ່ນດິນໃຫຍ່, ຮ່ອງກົງ ແລະ ມາເກົາ",
                "supportsEsim": true,
                "isPopular": true,
                "displayOrder": 1
            },
            {
                "id": 2,
                "brand": "Apple",
                "modelName": "iPhone 13",
                "category": "Phone",
                "notes": null,
                "supportsEsim": true,
                "isPopular": true,
                "displayOrder": 12
            }
        ],
        "pagination": {
            "pageNumber": 0,
            "pageSize": 50,
            "totalCount": 24,
            "totalPages": 1
        }
    }
}
```

### Response Fields - devices[]
| Field | Type | DB Column | Description |
|-------|------|-----------|-------------|
| id | int | DEVICE_ESIM_COMPATIBILITY.ID | ID thiết bị (Primary Key) |
| brand | string | DEVICE_ESIM_COMPATIBILITY.BRAND | Hãng sản xuất (Apple, Samsung,...) |
| modelName | string | DEVICE_ESIM_COMPATIBILITY.MODEL_NAME<br/>MODEL_NAME_EN<br/>MODEL_NAME_LO | Tên model (theo ngôn ngữ được chọn) |
| category | string | DEVICE_ESIM_COMPATIBILITY.CATEGORY | Loại thiết bị (Phone, Tablet, Laptop, Watch) |
| notes | string | DEVICE_ESIM_COMPATIBILITY.NOTES<br/>NOTES_EN<br/>NOTES_LO | Lưu ý đặc biệt (nullable, theo ngôn ngữ) |
| supportsEsim | bool | DEVICE_ESIM_COMPATIBILITY.SUPPORTS_ESIM | Hỗ trợ eSIM (luôn = true cho kết quả được trả về) |
| isPopular | bool | DEVICE_ESIM_COMPATIBILITY.IS_POPULAR | Thiết bị phổ biến (hiển thị trong quick lookup) |
| displayOrder | int | DEVICE_ESIM_COMPATIBILITY.DISPLAY_ORDER | Thứ tự sắp xếp |

### Filter Logic
- **Auto-filter**: Chỉ trả về devices với `STATUS = 1` và `SUPPORTS_ESIM = 1`
- **Search**: Tìm kiếm keyword trong `MODEL_NAME`, `MODEL_NAME_EN`, `MODEL_NAME_LO` (case-insensitive)
- **Ordering**: `DISPLAY_ORDER` ASC → `BRAND` ASC → `MODEL_NAME` ASC

### cURL Examples

#### Get metadata (brands & categories)
```bash
curl -X GET http://localhost:8360/apis/content/device-metadata \
  -H "Content-Type: application/json"
```

#### Search for "iPhone 13"
```bash
curl -X POST http://localhost:8360/apis/content/device-compatibility \
  -H "Content-Type: application/json" \
  -d '{
    "searchKeyword": "iPhone 13",
    "lang": "en",
    "pageSize": 20
  }'
```

#### Get popular Apple devices
```bash
curl -X POST http://localhost:8360/apis/content/device-compatibility \
  -H "Content-Type: application/json" \
  -d '{
    "brand": "Apple",
    "isPopular": true,
    "lang": "lo",
    "pageSize": 50
  }'
```

### Implementation Notes
- **Sample Data**: 70+ devices đã có sẵn trong `Database/DeviceCompatibility_Schema.sql`
- **Quick Lookup**: Set `isPopular=true` để chỉ lấy thiết bị phổ biến (Apple 24 devices, Samsung 19 devices, Google Pixel 10 devices)
- **Full Search**: Không set `isPopular` và dùng `searchKeyword` để tìm kiếm toàn bộ database
- **Multi-language**: Notes field hỗ trợ 3 ngôn ngữ (vi, en, lo)

---
