# Tài Liệu Tham Khảo Mã Lỗi API

Tất cả các lỗi của GoodGo Platform API đều tuân theo định dạng JSON có cấu trúc thống nhất. Tài liệu này liệt kê mọi giá trị `errorCode`, mã HTTP tương ứng, mô tả và ví dụ sử dụng.

## Định Dạng Phản Hồi Lỗi

Mỗi phản hồi lỗi từ API có dạng như sau:

```json
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Human-readable error description",
  "details": { },
  "correlationId": "optional-uuid",
  "timestamp": "2026-04-10T12:00:00.000Z"
}
```

| Field           | Type     | Description |
|-----------------|----------|-------------|
| `statusCode`    | `number` | Mã HTTP status (ví dụ: `400`, `401`, `404`). |
| `errorCode`     | `string` | Mã lỗi dạng máy đọc được (xem các bảng bên dưới). |
| `message`       | `string` | Giải thích dạng người đọc được (tiếng Việt cho các quy tắc nghiệp vụ, tiếng Anh cho lỗi hệ thống). |
| `details`       | `object` | Metadata có cấu trúc tùy chọn (ví dụ: tên trường validation, trạng thái hiện tại/mục tiêu). |
| `correlationId` | `string` | Phản chiếu từ header `X-Correlation-Id` của request khi có. |
| `timestamp`     | `string` | Dấu thời gian ISO 8601 tại thời điểm xảy ra lỗi. |

---

## Mã Lỗi Chung

Các mã này áp dụng cho tất cả các module. Chúng cũng được dùng làm mã dự phòng khi một `HttpException` thông thường (không có mã lỗi domain) được ném ra.

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `INTERNAL_ERROR` | `500` | Lỗi máy chủ không mong đợi. | Ngoại lệ chưa được xử lý, lỗi hạ tầng. |
| `VALIDATION_FAILED` | `400` | Dữ liệu request không qua xác thực. | Trường dữ liệu không hợp lệ, vi phạm quy tắc nghiệp vụ (ví dụ: định dạng số điện thoại sai, chuyển trạng thái không hợp lệ). |
| `NOT_FOUND` | `404` | Tài nguyên được yêu cầu không tồn tại. | Tra cứu thực thể theo ID không có kết quả. |
| `CONFLICT` | `409` | Xung đột trạng thái tài nguyên. | Tạo trùng lặp, chuyển đổi trạng thái xung đột. |
| `UNAUTHORIZED` | `401` | Yêu cầu xác thực hoặc xác thực thất bại. | Token xác thực thiếu/không hợp lệ/hết hạn, thông tin đăng nhập sai. |
| `FORBIDDEN` | `403` | Không đủ quyền truy cập. | Từ chối quyền theo vai trò, lỗi CSRF token, không khớp quyền sở hữu tài nguyên. |
| `BAD_REQUEST` | `400` | Request không đúng định dạng. | Tải lên tệp không hợp lệ, loại nội dung không được hỗ trợ, tham số truy vấn sai. |
| `TOO_MANY_REQUESTS` | `429` | Vượt quá giới hạn tốc độ. | Gửi quá nhiều request API trong thời gian ngắn. |

---

## Mã Lỗi Xác Thực

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `AUTH_INVALID_CREDENTIALS` | `401` | Thông tin đăng nhập không chính xác. | Số điện thoại hoặc mật khẩu không khớp khi đăng nhập nội bộ. |
| `AUTH_TOKEN_EXPIRED` | `401` | JWT token đã hết hạn. | Access token hoặc refresh token đã quá thời gian hết hạn. |
| `AUTH_TOKEN_INVALID` | `401` | JWT token bị sai định dạng hoặc bị giả mạo. | Xác minh chữ ký token thất bại. |
| `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Người dùng đã xác thực không có vai trò/quyền cần thiết. | Người dùng không phải admin truy cập endpoint admin, không khớp vai trò. |

### Ví Dụ Sử Dụng Module Xác Thực

```json
// POST /auth/login — wrong password
{
  "statusCode": 401,
  "errorCode": "UNAUTHORIZED",
  "message": "Số điện thoại hoặc mật khẩu không đúng"
}

// POST /auth/refresh — expired refresh token
{
  "statusCode": 401,
  "errorCode": "UNAUTHORIZED",
  "message": "Refresh token không hợp lệ hoặc đã hết hạn"
}

// POST /auth/register — phone already registered
{
  "statusCode": 409,
  "errorCode": "CONFLICT",
  "message": "Số điện thoại đã được đăng ký"
}

// POST /auth/register — invalid phone format
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Số điện thoại không hợp lệ. Yêu cầu 10 chữ số bắt đầu bằng 0"
}
```

---

## Mã Lỗi Người Dùng

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `USER_NOT_FOUND` | `404` | Người dùng không tồn tại. | Tra cứu hồ sơ, quản lý người dùng qua admin. |
| `USER_ALREADY_EXISTS` | `409` | Người dùng với định danh đã cho tồn tại. | Đăng ký trùng lặp (số điện thoại hoặc email). |
| `USER_INVALID_PHONE` | `400` | Định dạng số điện thoại không hợp lệ. | Đăng ký hoặc cập nhật hồ sơ với số điện thoại sai định dạng. |

### Ví Dụ Sử Dụng Module Người Dùng

```json
// GET /auth/profile — deleted or nonexistent user
{
  "statusCode": 404,
  "errorCode": "NOT_FOUND",
  "message": "Người dùng with id '...' not found"
}

// DELETE /auth/account — account already deleted
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Tài khoản đã bị xóa"
}
```

---

## Mã Lỗi Tin Đăng

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `LISTING_NOT_FOUND` | `404` | Tin đăng không tồn tại. | Tra cứu theo ID để xem, kiểm duyệt, hoặc thay đổi trạng thái. |
| `LISTING_INVALID_STATUS_TRANSITION` | `400` | Chuyển đổi trạng thái không được phép. | Thực hiện bước quy trình không hợp lệ (ví dụ: `EXPIRED` → `ACTIVE`). |
| `LISTING_ALREADY_ACTIVE` | `409` | Tin đăng đã ở trạng thái hoạt động. | Kích hoạt lại tin đăng đã được xuất bản. |
| `LISTING_EXPIRED` | `400` | Tin đăng đã hết hạn. | Thực hiện các thao tác trên tin đăng đã hết hạn. |

### Ví Dụ Sử Dụng Module Tin Đăng

```json
// PATCH /listings/:id/status — invalid transition
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Không thể chuyển trạng thái từ EXPIRED sang ACTIVE",
  "details": {
    "currentStatus": "EXPIRED",
    "targetStatus": "ACTIVE"
  }
}

// POST /admin/listings/:id/approve — already approved
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Listing này không ở trạng thái chờ duyệt"
}

// POST /listings — invalid address
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Địa chỉ không hợp lệ"
}
```

---

## Mã Lỗi Bất Động Sản

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `PROPERTY_NOT_FOUND` | `404` | Bất động sản không tồn tại. | Tra cứu bất động sản để tải media hoặc tạo tin đăng. |

### Ví Dụ Sử Dụng Module Bất Động Sản

```json
// POST /properties/:id/media — property not found
{
  "statusCode": 404,
  "errorCode": "NOT_FOUND",
  "message": "Property with id '...' not found"
}

// POST /properties/:id/media — too many files
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Tối đa 30 ảnh/video cho mỗi bất động sản"
}
```

---

## Mã Lỗi Media

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `MEDIA_UPLOAD_FAILED` | `500` | Tải tệp lên nhà cung cấp lưu trữ thất bại. | Lỗi ghi vào S3/cloud storage. |
| `MEDIA_LIMIT_EXCEEDED` | `400` | Đã đạt số lượng tệp media tối đa. | Vượt quá giới hạn media cho mỗi bất động sản. |

---

## Mã Lỗi Thanh Toán

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `PAYMENT_FAILED` | `500` | Xử lý thanh toán thất bại. | Lỗi cổng thanh toán, hết thời gian chờ nhà cung cấp, lỗi tạo liên kết thanh toán. |
| `PAYMENT_ALREADY_PROCESSED` | `409` | Thanh toán đã được hoàn tất, thất bại, hoặc hoàn tiền. | Callback trùng lặp, thử lại trên thanh toán không ở trạng thái chờ, hoàn tiền thanh toán chưa hoàn tất. |
| `PAYMENT_INVALID_AMOUNT` | `400` | Số tiền thanh toán không hợp lệ. | Số tiền bằng 0/âm, số tiền dưới ngưỡng tối thiểu. |

### Ví Dụ Sử Dụng Module Thanh Toán

```json
// POST /payments — duplicate idempotency key
{
  "statusCode": 409,
  "errorCode": "CONFLICT",
  "message": "Thanh toán với idempotency key này đã tồn tại"
}

// POST /payments/callback — invalid signature
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Chữ ký callback không hợp lệ"
}

// POST /payments/:id/refund — payment not completed
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Chỉ có thể hoàn tiền cho thanh toán đã hoàn tất"
}

// Entity-level: marking already-processed payment as completed
{
  "statusCode": 409,
  "errorCode": "PAYMENT_ALREADY_PROCESSED",
  "message": "Cannot complete payment in status COMPLETED"
}
```

---

## Mã Lỗi Gói Dịch Vụ

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `SUBSCRIPTION_NOT_FOUND` | `404` | Gói dịch vụ không tồn tại. | Tra cứu để hủy, nâng cấp, hoặc đo lường sử dụng. |
| `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | Người dùng đã có gói dịch vụ đang hoạt động. | Cố tạo gói dịch vụ hoạt động thứ hai. |
| `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Gói dịch vụ đã bị hủy trước đó. | Hủy lại gói dịch vụ đã hủy. |
| `SUBSCRIPTION_INACTIVE` | `409` | Gói dịch vụ không ở trạng thái hoạt động. | Nâng cấp, hết hạn, hoặc đánh dấu quá hạn trên gói không hoạt động. |
| `QUOTA_EXCEEDED` | `403` | Đã vượt quá hạn mức sử dụng của gói hiện tại. | Vượt quá giới hạn tin đăng, khách hàng tiềm năng, hoặc tìm kiếm. |

### Ví Dụ Sử Dụng Module Gói Dịch Vụ

```json
// POST /subscriptions — already has active subscription
{
  "statusCode": 409,
  "errorCode": "CONFLICT",
  "message": "Người dùng đã có subscription đang hoạt động"
}

// POST /subscriptions/cancel — already cancelled
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Subscription đã bị hủy trước đó"
}

// POST /subscriptions/upgrade — subscription not active
{
  "statusCode": 400,
  "errorCode": "VALIDATION_FAILED",
  "message": "Subscription không ở trạng thái hoạt động"
}

// Entity-level: upgrade on inactive subscription
{
  "statusCode": 409,
  "errorCode": "SUBSCRIPTION_INACTIVE",
  "message": "Không thể nâng cấp subscription ở trạng thái CANCELLED"
}

// Quota guard — plan limit exceeded
{
  "statusCode": 403,
  "errorCode": "FORBIDDEN",
  "message": "Bạn đã đạt giới hạn listings. Vui lòng nâng cấp gói để tiếp tục."
}
```

---

## Mã Lỗi Khóa Học

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `COURSE_NOT_FOUND` | `404` | Khóa học không tồn tại. | Tra cứu khóa học theo ID. |
| `COURSE_ALREADY_PUBLISHED` | `409` | Khóa học đã ở trạng thái xuất bản. | Xuất bản lại khóa học đang hoạt động. |
| `COURSE_ENROLLMENT_CLOSED` | `400` | Thời hạn đăng ký khóa học đã kết thúc. | Cố đăng ký sau ngày chốt danh sách. |

---

## Tra Cứu Nhanh Mã Lỗi (Sắp Xếp Theo Bảng Chữ Cái)

| Error Code | HTTP Status | Module |
|---|---|---|
| `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Auth |
| `AUTH_INVALID_CREDENTIALS` | `401` | Auth |
| `AUTH_TOKEN_EXPIRED` | `401` | Auth |
| `AUTH_TOKEN_INVALID` | `401` | Auth |
| `BAD_REQUEST` | `400` | General |
| `CONFLICT` | `409` | General |
| `COURSE_ALREADY_PUBLISHED` | `409` | Course |
| `COURSE_ENROLLMENT_CLOSED` | `400` | Course |
| `COURSE_NOT_FOUND` | `404` | Course |
| `FORBIDDEN` | `403` | General |
| `INTERNAL_ERROR` | `500` | General |
| `LISTING_ALREADY_ACTIVE` | `409` | Listing |
| `LISTING_EXPIRED` | `400` | Listing |
| `LISTING_INVALID_STATUS_TRANSITION` | `400` | Listing |
| `LISTING_NOT_FOUND` | `404` | Listing |
| `MEDIA_LIMIT_EXCEEDED` | `400` | Media |
| `MEDIA_UPLOAD_FAILED` | `500` | Media |
| `NOT_FOUND` | `404` | General |
| `PAYMENT_ALREADY_PROCESSED` | `409` | Payment |
| `PAYMENT_FAILED` | `500` | Payment |
| `PAYMENT_INVALID_AMOUNT` | `400` | Payment |
| `PROPERTY_NOT_FOUND` | `404` | Property |
| `QUOTA_EXCEEDED` | `403` | Subscription |
| `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | Subscription |
| `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Subscription |
| `SUBSCRIPTION_INACTIVE` | `409` | Subscription |
| `SUBSCRIPTION_NOT_FOUND` | `404` | Subscription |
| `TOO_MANY_REQUESTS` | `429` | General |
| `UNAUTHORIZED` | `401` | General |
| `USER_ALREADY_EXISTS` | `409` | User |
| `USER_INVALID_PHONE` | `400` | User |
| `USER_NOT_FOUND` | `404` | User |

---

## Hướng Dẫn Tích Hợp Frontend

### Kiểu Lỗi TypeScript

```typescript
interface ApiError {
  statusCode: number;
  errorCode: string;
  message: string;
  details?: Record<string, unknown>;
  correlationId?: string;
  timestamp: string;
}
```

### Xử Lý Lỗi Theo Mã

```typescript
import axios, { AxiosError } from 'axios';

async function handleApiCall() {
  try {
    const res = await axios.post('/api/listings', payload);
    return res.data;
  } catch (err) {
    if (axios.isAxiosError(err)) {
      const apiError = err.response?.data as ApiError;

      switch (apiError.errorCode) {
        case 'VALIDATION_FAILED':
          // Show field-level errors from apiError.details
          showValidationErrors(apiError.details);
          break;
        case 'UNAUTHORIZED':
        case 'AUTH_TOKEN_EXPIRED':
          // Redirect to login or refresh token
          await refreshSession();
          break;
        case 'QUOTA_EXCEEDED':
          // Show upgrade prompt
          showUpgradeDialog(apiError.message);
          break;
        case 'CONFLICT':
          // Inform user of duplicate action
          showToast(apiError.message, 'warning');
          break;
        case 'NOT_FOUND':
          // Navigate to 404 page or show missing resource message
          router.push('/404');
          break;
        default:
          showToast('Đã xảy ra lỗi, vui lòng thử lại', 'error');
      }
    }
  }
}
```

### Hằng Số Mã Lỗi (Tùy Chọn)

Để đảm bảo an toàn kiểu, hãy duy trì các mã lỗi dưới dạng union type ở phía frontend:

```typescript
type ErrorCode =
  | 'INTERNAL_ERROR'
  | 'VALIDATION_FAILED'
  | 'NOT_FOUND'
  | 'CONFLICT'
  | 'UNAUTHORIZED'
  | 'FORBIDDEN'
  | 'BAD_REQUEST'
  | 'TOO_MANY_REQUESTS'
  | 'AUTH_INVALID_CREDENTIALS'
  | 'AUTH_TOKEN_EXPIRED'
  | 'AUTH_TOKEN_INVALID'
  | 'AUTH_INSUFFICIENT_PERMISSIONS'
  | 'USER_NOT_FOUND'
  | 'USER_ALREADY_EXISTS'
  | 'USER_INVALID_PHONE'
  | 'COURSE_NOT_FOUND'
  | 'COURSE_ALREADY_PUBLISHED'
  | 'COURSE_ENROLLMENT_CLOSED'
  | 'LISTING_NOT_FOUND'
  | 'LISTING_INVALID_STATUS_TRANSITION'
  | 'LISTING_ALREADY_ACTIVE'
  | 'LISTING_EXPIRED'
  | 'PROPERTY_NOT_FOUND'
  | 'MEDIA_UPLOAD_FAILED'
  | 'MEDIA_LIMIT_EXCEEDED'
  | 'PAYMENT_FAILED'
  | 'PAYMENT_ALREADY_PROCESSED'
  | 'PAYMENT_INVALID_AMOUNT'
  | 'SUBSCRIPTION_NOT_FOUND'
  | 'SUBSCRIPTION_ALREADY_ACTIVE'
  | 'SUBSCRIPTION_ALREADY_CANCELLED'
  | 'SUBSCRIPTION_INACTIVE'
  | 'QUOTA_EXCEEDED';
```

---

## Tệp Nguồn

- **Enum mã lỗi**: `apps/api/src/modules/shared/domain/error-codes.ts`
- **Domain exceptions**: `apps/api/src/modules/shared/domain/domain-exception.ts`
- **Global exception filter**: `apps/api/src/modules/shared/infrastructure/filters/global-exception.filter.ts`
