Document all 33 structured errorCode values from DomainException/ErrorCode enum across all modules (auth, user, listing, property, media, payment, subscription, course). Includes HTTP status mapping, Vietnamese error messages, usage examples per module, alphabetical quick-reference table, and TypeScript integration guide for frontend error handling. Co-Authored-By: Paperclip <noreply@paperclip.ing>
442 lines
14 KiB
Markdown
442 lines
14 KiB
Markdown
# 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`
|