Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
442 lines
16 KiB
Markdown
442 lines
16 KiB
Markdown
# 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`
|