Bỏ qua

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ặc http://<server-ip>:3000 khi test nội bộ) Ngày verify: 22/03/2026


Mục lục

  1. Tổng quan kiến trúc
  2. Tài khoản test
  3. Bước 1 — Đăng nhập (Login)
  4. Bước 2 — Xem thông tin người dùng hiện tại
  5. Bước 3 — Tạo sân bóng (Field Owner)
  6. Bước 4 — Admin duyệt sân
  7. Bước 5 — Tạo đặt sân (Booking)
  8. Bước 6 — Tạo thanh toán (Payment Intent)
  9. Bước 7 — Xác nhận đặt sân (Confirm Booking)
  10. Bước 8 — Luồng thông báo Kafka (Notification)
  11. Sơ đồ luồng tổng thể
  12. Bảng tóm tắt tất cả API
  13. 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ùng refreshToken để lấy token mới

2. Tài khoản test

Role Email 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 accessTokenrefreshToken. 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 ý

  • accessToken dùng để xác thực mọi request tiếp theo
  • expiresIn: 300 — token hết hạn sau 5 phút
  • JWT payload chứa realm_access.roles vớ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ới keycloakId)
  • keycloakId = sub trong JWT — đây là giá trị được lưu trong booking.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_APPROVAL chờ 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_11
  • surfaceType: GRASS | ARTIFICIAL | HYBRID | INDOOR
  • address: 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 id củ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: startTimeendTime phải ở định dạng ISO 8601 (YYYY-MM-DDTHH:mm:ss.000Z), không phải HH:mm hay YYYY-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 Owner
  • expiresAt — booking sẽ tự động hết hạn nếu không được xác nhận
  • userId trong booking = Keycloak sub (không phải DB user id)
  • 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.userId trong response login (Keycloak sub)
  • idempotencyKey: Chuỗi unique để tránh tạo payment trùng lặp — nên dùng bookingId + timestamp
  • paymentMethod: 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út
  • status: "PENDING" → sau khi VNPAY callback thành công → "COMPLETED"
  • idempotencyKey phả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 confirmed và hệ thống phát sự kiện BOOKING_CONFIRMED qua 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.