# API Error Codes Reference

All GoodGo Platform API errors follow a structured JSON format. This document lists every `errorCode` value, its HTTP status, description, and usage examples.

## Error Response Format

Every error response from the API has this shape:

```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` | HTTP status code (e.g. `400`, `401`, `404`). |
| `errorCode`     | `string` | Machine-readable error identifier (see tables below). |
| `message`       | `string` | Human-readable explanation (Vietnamese for business rules, English for system errors). |
| `details`       | `object` | Optional structured metadata (e.g. validation field names, current/target status). |
| `correlationId` | `string` | Echoed from the `X-Correlation-Id` request header when present. |
| `timestamp`     | `string` | ISO 8601 timestamp of the error occurrence. |

---

## General Error Codes

These codes apply across all modules. They are also used as fallback codes when a plain `HttpException` (without a domain error code) is thrown.

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `INTERNAL_ERROR` | `500` | Unexpected server error. | Unhandled exceptions, infrastructure failures. |
| `VALIDATION_FAILED` | `400` | Request data failed validation. | Invalid input fields, business rule violations (e.g. invalid phone format, bad status transitions). |
| `NOT_FOUND` | `404` | Requested resource does not exist. | Entity lookup by ID returns no result. |
| `CONFLICT` | `409` | Resource state conflict. | Duplicate creation, conflicting state transition. |
| `UNAUTHORIZED` | `401` | Authentication required or failed. | Missing/invalid/expired auth token, bad credentials. |
| `FORBIDDEN` | `403` | Insufficient permissions. | Role-based access denial, CSRF token failure, resource ownership mismatch. |
| `BAD_REQUEST` | `400` | Malformed request. | Invalid file upload, unsupported content type, bad query parameters. |
| `TOO_MANY_REQUESTS` | `429` | Rate limit exceeded. | Too many API requests in a short period. |

---

## Auth Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `AUTH_INVALID_CREDENTIALS` | `401` | Login credentials are incorrect. | Phone number or password mismatch during local login. |
| `AUTH_TOKEN_EXPIRED` | `401` | JWT token has expired. | Access token or refresh token past expiration. |
| `AUTH_TOKEN_INVALID` | `401` | JWT token is malformed or tampered with. | Token signature verification failure. |
| `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Authenticated user lacks required role/permission. | Non-admin accessing admin endpoints, role mismatch. |

### Auth Module Usage Examples

```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"
}
```

---

## User Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `USER_NOT_FOUND` | `404` | User does not exist. | Profile lookup, admin user management. |
| `USER_ALREADY_EXISTS` | `409` | User with given identifier already exists. | Duplicate registration (phone or email). |
| `USER_INVALID_PHONE` | `400` | Phone number format is invalid. | Registration or profile update with bad phone format. |

### User Module Usage Examples

```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"
}
```

---

## Listing Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `LISTING_NOT_FOUND` | `404` | Listing does not exist. | Lookup by ID for viewing, moderation, or status change. |
| `LISTING_INVALID_STATUS_TRANSITION` | `400` | Status transition is not allowed. | Attempting an invalid workflow step (e.g. `EXPIRED` → `ACTIVE`). |
| `LISTING_ALREADY_ACTIVE` | `409` | Listing is already in active state. | Re-activating a listing that is already published. |
| `LISTING_EXPIRED` | `400` | Listing has expired. | Attempting operations on an expired listing. |

### Listing Module Usage Examples

```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ệ"
}
```

---

## Property Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `PROPERTY_NOT_FOUND` | `404` | Property does not exist. | Property lookup for media upload or listing creation. |

### Property Module Usage Examples

```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"
}
```

---

## Media Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `MEDIA_UPLOAD_FAILED` | `500` | File upload to storage provider failed. | S3/cloud storage write failure. |
| `MEDIA_LIMIT_EXCEEDED` | `400` | Maximum number of media files reached. | Exceeding per-property media limit. |

---

## Payment Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `PAYMENT_FAILED` | `500` | Payment processing failed. | Gateway error, provider timeout, payment link creation failure. |
| `PAYMENT_ALREADY_PROCESSED` | `409` | Payment has already been completed, failed, or refunded. | Duplicate callback, retry on non-pending payment, refunding a non-completed payment. |
| `PAYMENT_INVALID_AMOUNT` | `400` | Payment amount is invalid. | Zero/negative amount, amount below minimum threshold. |

### Payment Module Usage Examples

```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"
}
```

---

## Subscription Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `SUBSCRIPTION_NOT_FOUND` | `404` | Subscription does not exist. | Lookup for cancellation, upgrade, or usage metering. |
| `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | User already has an active subscription. | Attempting to create a second active subscription. |
| `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Subscription has already been cancelled. | Re-cancelling a subscription. |
| `SUBSCRIPTION_INACTIVE` | `409` | Subscription is not in an active state. | Upgrading, expiring, or marking past-due on a non-active subscription. |
| `QUOTA_EXCEEDED` | `403` | Usage quota for the current plan has been exceeded. | Exceeding listing, lead, or search quota limits. |

### Subscription Module Usage Examples

```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."
}
```

---

## Course Error Codes

| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `COURSE_NOT_FOUND` | `404` | Course does not exist. | Course lookup by ID. |
| `COURSE_ALREADY_PUBLISHED` | `409` | Course is already in published state. | Re-publishing an active course. |
| `COURSE_ENROLLMENT_CLOSED` | `400` | Course enrollment period has ended. | Attempting to enroll after the cutoff date. |

---

## Error Code Quick-Reference (Sorted Alphabetically)

| 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 |

---

## Frontend Integration Guide

### TypeScript Error Type

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

### Handling Errors by Code

```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');
      }
    }
  }
}
```

### Error Code Constants (Optional)

For type safety, maintain error codes as a union type on the 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';
```

---

## Source Files

- **Error code enum**: `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`
