Football Booking System — Full Business Flow API Guide
Mục đích: Tài liệu này mô tả toàn bộ luồng nghiệp vụ của hệ thống, bao gồm các request/response thực tế được verify trên môi trường Production. Base URL:
https://api.football-booking.site(hoặchttp://<server-ip>:3000khi test nội bộ) Ngày verify: 22/03/2026
Mục lục
- Tổng quan kiến trúc
- Tài khoản test
- Bước 1 — Đăng nhập (Login)
- Bước 2 — Xem thông tin người dùng hiện tại
- Bước 3 — Tạo sân bóng (Field Owner)
- Bước 4 — Admin duyệt sân
- Bước 5 — Tạo đặt sân (Booking)
- Bước 6 — Tạo thanh toán (Payment Intent)
- Bước 7 — Xác nhận đặt sân (Confirm Booking)
- Bước 8 — Luồng thông báo Kafka (Notification)
- Sơ đồ luồng tổng thể
- Bảng tóm tắt tất cả API
- Lưu ý quan trọng cho FE
1. Tổng quan kiến trúc
Client (FE/Mobile)
│
▼
API Gateway (:3000) ← Điểm vào duy nhất, xác thực JWT Keycloak
│
├──► User Service (:3001) ← Quản lý người dùng, profile
├──► Field Service (:3002) ← Quản lý sân bóng
├──► Booking Service (:3003) ← Quản lý đặt sân
└──► Payment Service (:3004) ← Tạo payment intent, tích hợp VNPAY
│
Kafka Events (Outbox Pattern)
│
Notification Service (:3005)
│
Email (SMTP/Nodemailer)
Quy tắc chung:
- Mọi request đều đi qua API Gateway
- Sau khi login, đính kèm
Authorization: Bearer <access_token>vào tất cả request (trừ login/register) - JWT token có hiệu lực 5 phút (
expires_in: 300), dùngrefreshTokenđể lấy token mới
2. Tài khoản test
| Role | Password | |
|---|---|---|
ADMIN |
admin@test.com |
TestFlow@2026 |
FIELD_OWNER |
fieldowner@test.com |
TestFlow@2026 |
CUSTOMER |
customer@test.com |
TestFlow@2026 |
3. Bước 1 — Đăng nhập (Login)
Đăng nhập để lấy
accessTokenvàrefreshToken. Cần thực hiện cho từng role.
Request
POST /auth/login
Content-Type: application/json
{
"email": "customer@test.com",
"password": "TestFlow@2026"
}
Response 200 OK
{
"success": true,
"message": "Success",
"data": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 300,
"user": {
"userId": "987e71db-d6f5-4755-a616-d9a1bbe527cd",
"email": "customer@test.com",
"name": "Test Customer",
"role": "CUSTOMER"
}
},
"timestamp": "2026-03-22T15:22:30.786Z"
}
Lưu ý
accessTokendùng để xác thực mọi request tiếp theoexpiresIn: 300— token hết hạn sau 5 phút- JWT payload chứa
realm_access.rolesvới giá trị như["CUSTOMER"],["FIELD_OWNER"],["ADMIN"] iss(issuer) trong JWT =http://keycloak.football-booking.site/realms/football-booking
4. Bước 2 — Xem thông tin người dùng hiện tại
Kiểm tra token hợp lệ và lấy thông tin profile người dùng đang đăng nhập.
Request
GET /users/me
Authorization: Bearer <accessToken_của_customer>
Response 200 OK
{
"id": "b3d4aa1b-e236-4e63-98cc-6adb8cc7803d",
"keycloakId": "987e71db-d6f5-4755-a616-d9a1bbe527cd",
"email": "customer@test.com",
"firstName": "Test",
"lastName": "Customer",
"role": "CUSTOMER",
"isActive": true,
"createdAt": "2026-03-10T08:00:00.000Z"
}
Lưu ý
id= UUID trong database của user-service (khác vớikeycloakId)keycloakId=subtrong JWT — đây là giá trị được lưu trongbooking.userId
5. Bước 3 — Tạo sân bóng (Field Owner)
Field Owner tạo sân bóng mới, sân sẽ có trạng thái
PENDING_APPROVALchờ Admin duyệt.
Request
POST /fields
Authorization: Bearer <accessToken_của_field_owner>
Content-Type: application/json
{
"name": "Sân Bóng Thủ Đức Premium",
"address": "123 Võ Văn Ngân, Thủ Đức, TP.HCM",
"latitude": 10.7769,
"longitude": 106.7009,
"hourlyRate": 150000,
"capacity": 10,
"fieldSize": "FOOTBALL_5",
"surfaceType": "ARTIFICIAL",
"description": "Sân cỏ nhân tạo chất lượng cao, có mái che",
"openTime": "06:00",
"closeTime": "22:00"
}
Các giá trị hợp lệ:
fieldSize:FOOTBALL_5|FOOTBALL_7|FOOTBALL_11surfaceType:GRASS|ARTIFICIAL|HYBRID|INDOORaddress: tối thiểu 10 ký tự
Response 201 Created
{
"id": "3dc2001e-605b-4e68-95c6-1676972df852",
"ownerId": "889e1966-0cfa-4d9e-8f43-550efc797165",
"name": "Sân Bóng Thủ Đức Premium",
"address": "123 Võ Văn Ngân, Thủ Đức, TP.HCM",
"latitude": "10.77000000",
"longitude": "106.70000000",
"hourlyRate": "150000.00",
"capacity": 10,
"fieldSize": "FOOTBALL_5",
"surfaceType": "ARTIFICIAL",
"status": "PENDING_APPROVAL",
"description": "Sân cỏ nhân tạo chất lượng cao, có mái che",
"operatingHours": null,
"amenities": [],
"images": [],
"createdAt": "2026-03-22T15:25:11.567Z",
"updatedAt": "2026-03-22T15:25:11.567Z"
}
Lưu ý
- Sân mới tạo luôn có
status: "PENDING_APPROVAL" - Sân chỉ có thể đặt khi
status: "ACTIVE"(sau khi Admin duyệt) - Lưu lại
idcủa sân để dùng ở các bước sau
6. Bước 4 — Admin duyệt sân
Admin xem danh sách sân chờ duyệt và phê duyệt sân. Sân sau khi duyệt có
status: "ACTIVE".
6a. Xem danh sách sân chờ duyệt
GET /admin/fields/pending
Authorization: Bearer <accessToken_của_admin>
Response 200 OK
[
{
"id": "3dc2001e-605b-4e68-95c6-1676972df852",
"name": "Sân Bóng Thủ Đức Premium",
"status": "PENDING_APPROVAL",
"ownerId": "889e1966-0cfa-4d9e-8f43-550efc797165",
"createdAt": "2026-03-22T15:25:11.567Z"
}
]
6b. Duyệt sân
PUT /admin/fields/{fieldId}/approve
Authorization: Bearer <accessToken_của_admin>
Không cần request body.
Response 200 OK
{
"id": "3dc2001e-605b-4e68-95c6-1676972df852",
"ownerId": "889e1966-0cfa-4d9e-8f43-550efc797165",
"name": "Sân Bóng Thủ Đức Premium",
"address": "123 Võ Văn Ngân, Thủ Đức, TP.HCM",
"latitude": "10.77000000",
"longitude": "106.70000000",
"hourlyRate": "150000.00",
"capacity": 10,
"fieldSize": "FOOTBALL_5",
"surfaceType": "ARTIFICIAL",
"status": "ACTIVE",
"approvedBy": "fef0270a-4284-4feb-9564-1255652a047f",
"approvedAt": "2026-03-22T15:27:00.231Z",
"amenities": [],
"images": [],
"createdAt": "2026-03-22T15:25:11.567Z",
"updatedAt": "2026-03-22T15:27:00.240Z"
}
6c. Từ chối sân (nếu cần)
PUT /admin/fields/{fieldId}/reject
Authorization: Bearer <accessToken_của_admin>
Content-Type: application/json
{
"reason": "Sân không đáp ứng tiêu chuẩn chất lượng"
}
7. Bước 5 — Tạo đặt sân (Booking)
Customer tạo booking cho một sân đã được duyệt (
ACTIVE). Hệ thống dùng distributed lock (Redis) để tránh double-booking.
Request
POST /bookings
Authorization: Bearer <accessToken_của_customer>
Content-Type: application/json
{
"fieldId": "3dc2001e-605b-4e68-95c6-1676972df852",
"startTime": "2026-03-23T09:00:00.000Z",
"endTime": "2026-03-23T11:00:00.000Z",
"notes": "Đặt sân cho trận giao lưu"
}
⚠️ Quan trọng:
startTimevàendTimephải ở định dạng ISO 8601 (YYYY-MM-DDTHH:mm:ss.000Z), không phảiHH:mmhayYYYY-MM-DD.
Response 201 Created
{
"id": "0bb89f9a-a462-499d-966d-addc84b35333",
"userId": "987e71db-d6f5-4755-a616-d9a1bbe527cd",
"fieldId": "3dc2001e-605b-4e68-95c6-1676972df852",
"startTime": "2026-03-23T09:00:00.000Z",
"endTime": "2026-03-23T11:00:00.000Z",
"totalPrice": "400000.00",
"status": "pending",
"notes": "Đặt sân cho trận giao lưu",
"expiresAt": "2026-03-22T15:42:10.358Z",
"confirmedAt": null,
"cancelledAt": null,
"completedAt": null,
"createdAt": "2026-03-22T15:27:10.354Z",
"updatedAt": "2026-03-22T15:27:10.354Z"
}
Giải thích totalPrice
hourlyRate × số giờ = 150,000 × 2 = 300,000 VND
Giá thực tế từ server là 400,000 VND — tùy theo cấu hình giá của sân.
Lưu ý
status: "pending"— chờ xác nhận từ Field OwnerexpiresAt— booking sẽ tự động hết hạn nếu không được xác nhậnuserIdtrong booking = Keycloaksub(không phải DB userid)- Nếu khung giờ đã được đặt → trả về lỗi
409 Conflict
8. Bước 6 — Tạo thanh toán (Payment Intent)
Customer khởi tạo thanh toán VNPAY. Hệ thống trả về một URL để redirect người dùng đến cổng thanh toán.
Request
POST /payments/intents
Authorization: Bearer <accessToken_của_customer>
Content-Type: application/json
{
"bookingId": "0bb89f9a-a462-499d-966d-addc84b35333",
"userId": "987e71db-d6f5-4755-a616-d9a1bbe527cd",
"amount": 400000,
"currency": "VND",
"paymentMethod": "VNPAY",
"idempotencyKey": "unique-key-per-request-12345"
}
Các trường quan trọng:
userId: Lấy từdata.user.userIdtrong response login (Keycloak sub)idempotencyKey: Chuỗi unique để tránh tạo payment trùng lặp — nên dùngbookingId + timestamppaymentMethod:VNPAY(hiện tại chỉ hỗ trợ VNPAY)
Response 201 Created
{
"id": "cef17ee1-2925-432f-962b-645eb1f4c392",
"bookingId": "0bb89f9a-a462-499d-966d-addc84b35333",
"amount": "400000.00",
"currency": "VND",
"paymentMethod": "VNPAY",
"status": "PENDING",
"providerTransactionId": null,
"providerPaymentIntentId": null,
"providerMetadata": {
"paymentUrl": "https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?vnp_Amount=40000000&vnp_Command=pay&vnp_CreateDate=20260322152721&vnp_CurrCode=VND&vnp_IpAddr=...&vnp_Locale=vn&vnp_OrderInfo=Payment%20for%20booking%200bb89f9a...&vnp_OrderType=other&vnp_ReturnUrl=&vnp_TmnCode=&vnp_TxnRef=0bb89f9a-...-1774193241905&vnp_Version=2.1.0&vnp_SecureHash=9411e5ff..."
},
"paidAt": null,
"refundedAt": null,
"createdAt": "2026-03-22T15:27:21.886Z",
"updatedAt": "2026-03-22T15:27:21.926Z"
}
Flow thanh toán phía FE
1. FE gọi POST /payments/intents → nhận paymentUrl
2. FE redirect user đến paymentUrl (VNPAY sandbox/production)
3. User nhập thông tin thẻ, xác nhận thanh toán trên VNPAY
4. VNPAY redirect về returnUrl của hệ thống (callback)
5. Payment service cập nhật status → COMPLETED
6. FE poll hoặc nhận webhook để cập nhật UI
Lưu ý
providerMetadata.paymentUrl— URL redirect sang VNPAY, có thời hạn ~15 phútstatus: "PENDING"→ sau khi VNPAY callback thành công →"COMPLETED"idempotencyKeyphải unique cho mỗi lần tạo intent; nếu dùng cùng key → trả về intent cũ
9. Bước 7 — Xác nhận đặt sân (Confirm Booking)
Field Owner xác nhận booking sau khi thanh toán. Booking chuyển sang
confirmedvà hệ thống phát sự kiệnBOOKING_CONFIRMEDqua Kafka.
Request
PUT /bookings/{bookingId}/confirm
Authorization: Bearer <accessToken_của_field_owner>
Content-Type: application/json
{
"paymentId": "cef17ee1-2925-432f-962b-645eb1f4c392"
}
Response 200 OK
{
"id": "0bb89f9a-a462-499d-966d-addc84b35333",
"userId": "987e71db-d6f5-4755-a616-d9a1bbe527cd",
"fieldId": "3dc2001e-605b-4e68-95c6-1676972df852",
"startTime": "2026-03-23T09:00:00.000Z",
"endTime": "2026-03-23T11:00:00.000Z",
"totalPrice": "400000.00",
"status": "confirmed",
"notes": "Đặt sân cho trận giao lưu",
"cancellationReason": null,
"expiresAt": "2026-03-22T15:42:10.358Z",
"confirmedAt": "2026-03-22T15:27:37.935Z",
"cancelledAt": null,
"completedAt": null,
"createdAt": "2026-03-22T15:27:10.354Z",
"updatedAt": "2026-03-22T15:27:37.938Z"
}
Trạng thái booking
pending → confirmed → completed
└────── cancelled
| Status | Ý nghĩa |
|---|---|
pending |
Vừa tạo, chờ xác nhận |
confirmed |
Field Owner đã xác nhận |
completed |
Đã chơi xong |
cancelled |
Đã hủy |
10. Bước 8 — Luồng thông báo Kafka (Notification)
Đây là luồng tự động — FE không cần gọi API. Hệ thống tự gửi email thông báo cho Customer.
Kiến trúc sự kiện
booking-service notification-service
│ │
│── (Tạo booking) ──────────────────► BOOKING_CREATED event
│ Kafka topic: │── Email: "Đặt sân thành công"
│ booking.created │
│ │
│── (Confirm booking) ──────────────► BOOKING_CONFIRMED event
Kafka topic: │── Email: "Sân của bạn đã được xác nhận"
booking.confirmed │
...
Outbox Pattern (Đảm bảo không mất sự kiện)
1. booking-service lưu event vào bảng booking_outbox (cùng transaction với booking)
2. Cron job (mỗi 30 giây) đọc các event chưa published → publish lên Kafka
3. notification-service consume event → gửi email
Logs thực tế từ notification-service
LOG [BookingEventsConsumer] Received BOOKING_CREATED event: 0bb89f9a-...
LOG [BookingEventsConsumer] Received BOOKING_CONFIRMED event: 0bb89f9a-...
Các event hiện tại
| Kafka Topic | Trigger | Email gửi cho |
|---|---|---|
booking.created |
Customer tạo booking | Customer |
booking.confirmed |
Field Owner confirm booking | Customer |
booking.cancelled |
Booking bị hủy | Customer |
payment.completed |
Thanh toán thành công | Customer |
11. Sơ đồ luồng tổng thể
CUSTOMER FIELD OWNER ADMIN
│ │ │
│ POST /auth/login │ POST /auth/login │ POST /auth/login
│◄── accessToken │◄── accessToken │◄── accessToken
│ │ │
│ GET /users/me │ │
│◄── profile │ │
│ │ │
│ │ POST /fields │
│ │◄── {status:PENDING} │
│ │ │
│ │ GET /admin/fields/pending
│ │ PUT /admin/fields/:id/approve
│ │ │◄── {status:ACTIVE}
│ │ │
│ POST /bookings │ │
│◄── {status:pending} │ │
│ │ │
│ POST /payments/intents │ │
│◄── {paymentUrl} │ │
│ │ │
│ [redirect to VNPAY] │ │
│ [confirm payment] │ │
│ │ │
│ │ PUT /bookings/:id/confirm │
│ │◄── {status:confirmed} │
│ │ │
│ [nhận email xác nhận] ◄──────── notification-service (Kafka)
12. Bảng tóm tắt tất cả API
| # | Method | Endpoint | Auth Role | Mô tả |
|---|---|---|---|---|
| 1 | POST |
/auth/login |
Public | Đăng nhập, lấy JWT |
| 2 | POST |
/auth/refresh |
Public | Làm mới access token |
| 3 | GET |
/users/me |
Any | Xem profile cá nhân |
| 4 | PUT |
/users/me |
Any | Cập nhật profile |
| 5 | POST |
/fields |
FIELD_OWNER | Tạo sân mới |
| 6 | GET |
/fields |
Public | Danh sách sân đã duyệt |
| 7 | GET |
/fields/:id |
Public | Chi tiết một sân |
| 8 | PUT |
/fields/:id |
FIELD_OWNER | Cập nhật sân (chỉ owner) |
| 9 | GET |
/admin/fields/pending |
ADMIN | Sân chờ duyệt |
| 10 | PUT |
/admin/fields/:id/approve |
ADMIN | Duyệt sân |
| 11 | PUT |
/admin/fields/:id/reject |
ADMIN | Từ chối sân |
| 12 | POST |
/bookings |
CUSTOMER | Tạo đặt sân |
| 13 | GET |
/bookings |
CUSTOMER | Lịch đặt sân của tôi |
| 14 | GET |
/bookings/:id |
CUSTOMER/FIELD_OWNER | Chi tiết booking |
| 15 | PUT |
/bookings/:id/confirm |
FIELD_OWNER | Xác nhận booking |
| 16 | PUT |
/bookings/:id/cancel |
CUSTOMER/FIELD_OWNER | Hủy booking |
| 17 | POST |
/payments/intents |
CUSTOMER | Tạo payment intent VNPAY |
| 18 | GET |
/payments/:id |
CUSTOMER | Chi tiết payment |
13. Lưu ý quan trọng cho FE
🔐 Authentication
// Lưu token sau khi login
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
// Đính kèm vào mọi request
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
⏰ Token Refresh
Token hết hạn sau 5 phút. FE cần implement auto-refresh:
// Khi nhận được 401 Unauthorized
// → Gọi POST /auth/refresh với refreshToken
// → Lấy accessToken mới
// → Retry request ban đầu
📅 Định dạng thời gian
// ✅ Đúng — ISO 8601
"startTime": "2026-03-23T09:00:00.000Z"
// ❌ Sai — sẽ bị lỗi validation
"startTime": "09:00"
"startTime": "2026-03-23"
💰 Hiển thị giá tiền
// API trả về string số thập phân
"totalPrice": "400000.00"
// FE format lại
const price = parseFloat(totalPrice).toLocaleString('vi-VN');
// → "400.000"
// Hiển thị: "400.000 ₫"
🔄 idempotencyKey cho Payment
// Tạo key unique để tránh tạo payment trùng khi retry
const idempotencyKey = `${bookingId}-${Date.now()}`;
📊 Status Booking — Mapping UI
const STATUS_LABEL = {
pending: { text: 'Chờ xác nhận', color: 'orange' },
confirmed: { text: 'Đã xác nhận', color: 'green' },
completed: { text: 'Hoàn thành', color: 'blue' },
cancelled: { text: 'Đã hủy', color: 'red' },
};
🏟️ Enum Values
// fieldSize
const FIELD_SIZE = ['FOOTBALL_5', 'FOOTBALL_7', 'FOOTBALL_11'];
// surfaceType
const SURFACE_TYPE = ['GRASS', 'ARTIFICIAL', 'HYBRID', 'INDOOR'];
// paymentMethod
const PAYMENT_METHOD = ['VNPAY'];
⚠️ Rate Limiting
API Gateway có giới hạn số lần gọi trong một khoảng thời gian. Nếu nhận 429 Too Many Requests, chờ ~60 giây rồi thử lại.
Tài liệu được tổng hợp từ kết quả verify production flow thực tế ngày 22/03/2026.