MPC Backend - API Documentation (Updated)
Dokumen ini berisi spesifikasi API yang up-to-date sesuai dengan implementasi Backend (Golang) terbaru untuk aplikasi Android.
Base URL: http://localhost:8080/api/v1 (Gunakan http://10.0.2.2:8080/api/v1 jika dari Android Emulator).
Semua response sukses menggunakan format envelope standar berikut (Tolong perbarui parsing di Android untuk mendeteksi success, message, dan data / error):
{
"success": true,
"message": "Sukses",
"data": { ... }
}
Jika gagal:
{
"success": false,
"message": "Error Message",
"error": "Detailed error context"
}
1. Authentication & Users
Harap dicatat: Semua endpoint yang dilindungi (Protected) memerlukan header Authorization: Bearer <jwt-token>.
1.1 Register
- URL:
POST /auth/register - Request Body:
{
"name": "John Doe",
"email": "john@example.com",
"phone": "081234567890",
"password": "securepassword123" // <-- Wajib
}
- Email dan nomor HP harus unik. HP dinormalisasi ke digit (contoh
+62 812-…→0812…); format yang sama dianggap duplikat →409dengan erroremail already registered/phone already registered.
1.2 Login (Password)
- URL:
POST /auth/login - Request Body:
{
"email": "john@example.com",
"password": "securepassword123" // <-- Validasi ke DB hash
}
- Response Data:
{
"token": "jwt-token-string",
"customer": {
"id": "uuid",
"name": "John Doe",
"email": "john@example.com",
"phone": "081234567890"
}
}
1.3 Google Sign-In
- URL:
POST /auth/google - Request Body:
{
"id_token": "google-id-token"
}
- Response Data: Sama seperti Login (token & customer).
1.4 Send OTP
- URL:
POST /auth/otp/send - Request Body:
{
"email": "john@example.com"
}
- Mengirim kode 4 digit acak ke email via Resend. Berlaku 10 menit. Cooldown kirim ulang 60 detik (
429). - Jika email belum terdaftar: response tetap sukses (
OTP sent) tanpa mengirim — anti-enumerasi. - Jika
RESEND_API_KEYkosong atau pengiriman gagal:500. - Response:
{ "success": true, "message": "OTP sent", "data": null }
1.5 OTP Verification
- URL:
POST /auth/otp/verify - Request Body:
{
"email": "john@example.com",
"otp": "4821"
}
- Kode harus sama dengan yang dikirim ke email (bukan nilai tetap). Maksimal 5 percobaan; kadaluarsa 10 menit. Setelah sukses, OTP dihapus.
- Response Data:
{ "token": "jwt-token-string", "customer": {...} }
1.6 Forgot Password (Reset)
- URL:
POST /auth/password/reset - Request Body:
{
"email": "john@example.com",
"otp": "4821",
"new_password": "newsecurepassword123"
}
- Memakai OTP yang sama dari
POST /auth/otp/send. OTP dikonsumsi setelah reset sukses.
1.7 Get Profile (Me)
- URL:
GET /users/me - Header:
Authorization: Bearer <token> - Response Data: Objek Customer.
1.8 Update Profile (Me)
- URL:
PUT /users/me - Header:
Authorization: Bearer <token> - Request Body:
{
"name": "John Updated",
"email": "john2@example.com",
"phone": "0811111111",
"avatar_url": "https://link-to-avatar.png"
}
1.9 Refresh Token
- URL:
POST /auth/refresh - Header:
Authorization: Bearer <old-token> - Response Data:
{ "token": "new-jwt-token" }
1.10 Logout
- URL:
POST /auth/logout - Header:
Authorization: Bearer <token> - Response Data:
{ "success": true, "message": "Logged out successfully" }
2. Courts (Lapangan)
Optional query on list/availability: branch_id (UUID). Omit = default/Main branch. Court responses may include branch_id.
2.1 Get All Courts
- URL:
GET /courts|GET /courts?branch_id=<uuid> - Response Data: Array dari objek Court (berisi
image_url,category_id,display_order,group_name,category_name,branch_id, dll). Data diurutkan ascending berdasarkandisplay_order. Scoped ke resolved branch. - Konten showcase (untuk halaman Court Detail):
description(string|null),gallery(array of image URL),specs(array of{ "label": string, "value": string }),perks(array of string). - NEW (v2026-08-13) —
bundled_space(object|null): Room VIP resto yang otomatis ikut ter-reserve saat court ini dibooking bila Room tersedia (best-effort; lihat 5.1).
"bundled_space": { "id": "uuid-space", "name": "Room VIP 1", "minimum_spend": 2000000.0 }
2.2 Get Court By ID
- URL:
GET /courts/:id|GET /courts/:id?branch_id=<uuid> - Court di luar resolved branch → 404.
- Response menyertakan field showcase yang sama (
description,gallery,specs,perks) sertabundled_space(lihat 2.1).
2.3 Get Court Availability (Availability Hourly)
- URL:
GET /courts/:id/availability?date=2024-10-01(branch_idopsional) - Occupancy: Hours are venue wall-clock. A non-
Canceledbooking (includingCompleted/ checked-out) marks every hourly slot in[start_time, end_time)asis_available: false. Soft-deleted rows are ignored. - NEW (v2026-08-13) — VIP bundling best-effort: reservasi resto pada Room bundling tidak mempengaruhi availability court — slot court tetap available meski Room-nya sedang ter-reserve (bundling Room hanya bonus best-effort saat checkout, lihat 5.1).
- Response Data:
[
{
"start_time": "06:00",
"end_time": "07:00",
"is_available": true,
"price": 200000.0 // <-- NEW (v2026-07-30): Dynamic Pricing (Peak Hour / Special Day / Weekday)
},
{
"start_time": "07:00",
"end_time": "08:00",
"is_available": false,
"price": 250000.0
}
]
2.4 Bulk Availability
- URL:
GET /courts/availability?date=2024-10-01(branch_idopsional) - Map court_id → slots, hanya courts di resolved branch. Same occupancy rules as 2.3 (reservasi resto Room bundling tidak mempengaruhi slot).
3. Amenities (Rent)
Menyediakan fitur penyewaan handuk, raket, bola, dll. (label UI member: Rent).
(Data dari table products dengan product_type = Rent. Path /amenities tetap untuk kompatibilitas client.)
3.1 Get All Amenities
- URL:
GET /amenities - Response Data: Array dari objek Amenity (mem-passing ID Product dari inventory; filter
product_type = Rent). - Catatan Android: Tidak ada perubahan struktur JSON. Gunakan
idamenity_id pada Checkout arrayamenities. - Filter: hanya produk dengan
product_type = Rent.
3.2 Get Amenity By ID
- URL:
GET /amenities/:id - Hanya produk dengan
product_type = Rent.
4. Bookings
Catatan: Secara normal, flow pemesanan harus melalui endpoint /orders untuk multi-item checkout, tetapi endpoint bookings ini digunakan internal / langsung.
4.1 Get User Bookings (Me)
- URL:
GET /bookings - Header:
Authorization: Bearer <token> - Response Data: Array dari objek Booking.
4.2 Get Booking By ID
- URL:
GET /bookings/:id - Header:
Authorization: Bearer <token>
4.3 Reschedule Booking (NEW - v2026-07-30)
- URL:
POST /bookings/:id/reschedule - Header:
Authorization: Bearer <token> - Request Body:
{
"new_date": "2026-08-01",
"new_start_time": "10:00",
"new_end_time": "12:00"
}
- Response Data:
- Jika Gratis (Reschedule Charge = 0): Akan mengembalikan data kosong (
null) dengan message sukses. Booking lama langsung diupdate jadwalnya. - Jika Berbayar (Reschedule Charge > 0): Akan mengembalikan objek Transaction yang memuat tagihan Reschedule Fee. Booking lama akan berstatus Confirmed sampai pembayaran ini dibayar. User harus diarahkan ke halaman pembayaran!
- Jika Gratis (Reschedule Charge = 0): Akan mengembalikan data kosong (
- NEW (v2026-08-13): pada jalur gratis, reservasi Room VIP bundling (source
CourtBundle) di session yang sama ikut dipindah ke window baru — best-effort: bila Room tidak tersedia di window baru, reservasi Room dilepas (Cancelled) dan reschedule court tetap berhasil.
4.4 Reschedule Session (Split Sessions) (NEW - v2026-07-30)
- URL:
POST /booking-sessions/:sessionId/reschedule - Header:
Authorization: Bearer <token> - Request Body:
{
"bookings": [
{
"court_id": "uuid-court",
"date": "2026-08-01",
"start_time": "10:00",
"end_time": "11:00"
}
]
}
- Response Data: Sama seperti 4.3. Jika berbayar akan menerbitkan Invoice Transaction. (Total Fee = Reschedule Charge + Max(0, Harga Baru - Harga Lama)). Seluruh
bookingslama dalam session tersebut dibatalkan dan diganti blok baru. - NEW (v2026-08-13): reservasi Room VIP bundling session lama dibatalkan dan dibuat ulang untuk window baru bila court baru punya
bundled_space— best-effort: bila Room tidak tersedia di window baru, reschedule court tetap berhasil tanpa reservasi Room.
4.5 Cancel Session (NEW - v2026-07-30)
- URL:
POST /booking-sessions/:sessionId/cancel - Header:
Authorization: Bearer <token> - NEW (v2026-08-13): cascade — reservasi Room VIP bundling (source
CourtBundle) denganbooking_session_idyang sama ikut berstatusCancelled.
5. Orders & Transactions (Checkout Multi-item)
5.1 Checkout (Multi-Court, Amenities, Voucher)
- URL:
POST /orders - Header:
Authorization: Bearer <token> - Request Body:
{
"bookings": [
{
"session_ref": "group_1", // <-- NEW: BE akan membuatkan UUID session_id berdasarkan referensi ini
"court_id": "uuid-court",
"date": "2024-10-01",
"start_time": "10:00",
"end_time": "11:00"
}
],
"amenities": [
{
"amenity_id": "uuid-amenity",
"quantity": 2
}
],
"products": [
{
"product_id": "uuid-product",
"quantity": 1,
"notes": "Less sugar, extra hot", // <-- NEW
"variant_id": "uuid-variant", // <-- NEW
"addon_ids": [ // <-- NEW
"uuid-addon-1",
"uuid-addon-2"
]
}
],
"coaches": [
{
"coach_id": "uuid-coach",
"quantity": 2 // in hours
}
],
"classes": [
{
"class_id": "uuid-class",
"quantity": 1
}
],
"voucher_code": "DISC50",
"fnb_pickup_time": "2024-10-01T09:30:00Z",
"space_reservation_id": "uuid-space-reservation"
}
fnb_pickup_time(opsional): ISO datetime. Wajib jika request berisi produkproduct_type=FnBdan adabookings. Window: dari (min(booking.start) − before_minutes) sampaimax(booking.end), kelipataninterval_minutes(dariGET /settings/fnb-pickup). Tanpa booking court atau tanpa FnB → abaikan / jangan kirim. Disimpan ditransactions.fnb_pickup_time.space_reservation_id(opsional, NEW v2026-08-13): dipakai untuk alur pre-order Resto — order F&B tanpa booking court, ditempel ke reservasi Resto yang sudah confirmed. Backend memvalidasi reservasi tersebut milik customer (401/404Reservation not found, 403Forbiddenbila bukan pemilik), lalu menyimpan ditransactions.space_reservation_id. Order ini lalu muncul diorders[]reservasi tsb padaGET /spaces/reservations/me(lihat §14.4). Tidak mengubahfnb_pickup_time(tetap opsional, hanya wajib bila ada booking court + FnB).- Response Data: Objek Transaction (termasuk detil item,
tax_amount11%,discount_amount, danfnb_pickup_timebila ada). - Catatan Diskon Otomatis (TierBenefit): Backend akan secara otomatis mengecek paket
MembershipTierpelanggan dan mengevaluasi batasan benefit yang bisa dipakai (contoh: Diskon 40% max 3x per bulan). Jika valid, kuota pengguna (BenefitUsage) dipotong dandiscount_amountakan bertambah secara otomatis pada Response Data. - Catatan Pembayaran: Transaksi juga memuat info pembayaran seperti
payment_url,va_number, danqris_stringuntuk gateway pihak ketiga. Backend membungkusnya dalam Atomic Transaction. - NEW (v2026-08-13) — Auto-reserve Room VIP bundling (best-effort): bila order memesan court yang punya
bundled_space, backend otomatis membuatspace_reservationuntuk Room tersebut (sourceCourtBundle, statusConfirmed,pax= kapasitas Room, window = gabungan slot contiguous per court/session,booking_session_id= session booking). Availability Room dicek di dalam transaksi yang sama (row lock). Bila Room sudah terisi, order tetap berhasil tanpa reservasi Room (best-effort, bukan hard block). Response Transaction menyertakan field opsionalbundled_room_included(boolean, hanya muncul saat order memesan court ber-bundled_space):true= reservasi Room ikut dibuat,false= Room penuh sehingga order jalan tanpa Room. Backward compatible — client lama bisa mengabaikan field ini.
5.2 Get User Transactions (Payment History)
- URL:
GET /transactions/customer/:customerId - Header:
Authorization: Bearer <token> - (Keamanan:
customerIddi URL harus cocok denganiddi JWT token).
5.3 Get Transaction By ID
- URL:
GET /transactions/:id - Header:
Authorization: Bearer <token>
5.4 Give a Tip (NEW v2026-08-13)
- URL:
POST /orders/:id/tip—:id= id order/Transaction booking court yang sudah Completed. - Header:
Authorization: Bearer <token> - Request Body:
{ "amount": 20000 }
amount: nominal tip dalam Rupiah, minimum Rp 5.000, tidak ada batas atas.- Response Data: Objek
Transactionbaru (statusPending,transaction_type=Tip, satu itemitem_type: "Fee"bernama"Tip"denganreference_id= id order asli). Client membayar invoice ini lewat flow pembayaran existing (POST /transactions/:id/complete) — sama seperti reschedule fee, bukanPOST /ordersbaru. - Validasi & error:
- 404
Order not found— id order tidak ditemukan. - 403
Forbidden— order bukan milik customer yang login. - 409
Cannot tip this order— order masih ada booking yang belum selesai (Upcoming), semua booking Canceled, atau order sudah pernah di-tip sebelumnya (1x per order). - 400
Invalid tip amount—amount < 5000.
- 404
- Catatan: tip disimpan sebagai
Transactionbiasa (transaction_type: "Tip"), bukan kolom baru — sehingga tidak ada perubahan kontrak data padabookings/transactionsselain field response baru di bawah. - Field baru pada
Transactionresponse (§5.2, §5.3, dan checkoutPOST /orders):tip_amount(number, opsional,omitempty) — muncul pada order yang sudah di-tip (invoice Tip-nya sudahCompleted), berisi nominal tip.null/tidak ada = order belum pernah di-tip. Backward compatible.
6. Food, Drinks, & Pro Shop (Inventory)
products.product_type enum: FnB | ProShop | Rent. Endpoint ini mengembalikan FnB dan Pro Shop saja (Rent di-exclude; sewa lewat /amenities). Response: product_type dan category (alias nilai yang sama — dipakai member apps untuk split Court Café vs Pro Shop).
6.1 Get Inventory
- URL:
GET /inventory - Response Data: Array dari objek Product (termasuk
description,image_url,gallery(JSON array of URL strings, default[]),product_type=FnB|ProShop,category= same asproduct_type,category_id,group_id,display_order,group_name,category_name,variantsseperti "Ice", "Hot", danaddonsseperti "Extra Shot"). Data diurutkan ascending berdasarkandisplay_order. Member apps may useimage_urlas the primary photo;galleryis available for future multi-photo UI.
7. Memberships
7.1 Get Active Subscription (Me)
- URL:
GET /memberships/me - Header:
Authorization: Bearer <token>
7.2 Get Membership Tiers (NEW - v2026-07-21)
- URL:
GET /memberships/tiers - Response Data: Array dari objek
MembershipTier.
[
{
"id": "uuid-tier",
"name": "Pro Member",
"price": 1000000.0,
"billing_cycle": "Yearly",
"benefits": "Free stringing, 10% off F&B",
"status": "Active"
}
]
7.3 Subscribe to Membership
- URL:
POST /memberships/subscribe - Header:
Authorization: Bearer <token> - Request Body:
{
"membership_tier_id": "uuid-tier"
}
7.4 Cancel Subscription
- URL:
POST /memberships/cancel - Header:
Authorization: Bearer <token> - Response Data:
{ "success": true, "message": "Subscription cancelled successfully" }
8. Classes
8.1 Get All Active Classes
- URL:
GET /classes - Response Data: Array dari objek Class (termasuk
title,description,image_url,start_time,end_time,price, dll).
8.2 Get Class By ID
- URL:
GET /classes/:id
9. Favorites
9.1 Get User Favorites
- URL:
GET /favorites - Header:
Authorization: Bearer <token>
9.2 Add to Favorites
- URL:
POST /favorites - Header:
Authorization: Bearer <token> - Request Body:
{
"target_type": "Court", // Atau "Coach", "Class"
"target_id": "uuid-target"
}
9.3 Remove from Favorites
- URL:
DELETE /favorites/:targetType/:targetID - Header:
Authorization: Bearer <token>
10. Modul Lainnya
- Events & Tournaments:
/events(Ditambahkanimage_url,description,target_tiers,gallery,bracket,results[NEW - v2026-07-21], dan endpointPOST /events/:id/register). - Coaches:
/coaches(Ditambahkanimage_url,description,achievements). - Promos:
/promos— marketing / home banners only (CMSactive_promos). Not applied at checkout. - Vouchers:
/vouchers— redeemable discount codes at checkout (voucher_code). - Matches (matchplay): see §10.1 below.
- Notifications:
/notifications— inbox in-app; written on payment/booking/etc. Member apps list + mark read. Push (FCM) + email (Resend) fan-out onNotify. - Device tokens:
POST/DELETE /device-tokens— register FCM/Web Push tokens. - Admin notify:
POST /admin/notifications/broadcast,/push,/notify-customer(X-Admin-Key). - Favorites:
target_typesupportsCourt|Coach|Class|Player. - Classes:
GET /classes— catalog managed via admin CRUD.
10.2 Notifications & push
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /notifications/customer/:customerId | Yes (own id) | Inbox list |
| GET | /notifications/me | Yes | Inbox for JWT customer |
| PUT | /notifications/:id/read | Yes | Mark read |
| POST | /device-tokens | Bearer customer or X-Admin-Key | Body: { token, platform, app?, audience?, audience_id? } — customer JWT forces audience=customer |
| DELETE | /device-tokens | Bearer / Admin key | Body: { token } |
| POST | /admin/notifications/broadcast | Admin key | Promo to all customers (inbox+push+email) |
| POST | /admin/notifications/push | Admin key | Push-only to audience tokens |
| POST | /admin/notifications/notify-customer | Admin key | Full NotifyWithOpts for one customer |
Env: FCM_PROJECT_ID, FCM_CREDENTIALS_JSON or GOOGLE_APPLICATION_CREDENTIALS, RESEND_API_KEY, EMAIL_FROM, MEMBER_WEB_BASE_URL. Without FCM/Resend credentials, inbox still works; push/email skipped.
10.1 Matches / Matchplay
Tables: matches (+ host_customer_id, is_double, court_id), match_participants, match_games.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /matches?status=Open | No | List; default status Open; includes participant_count |
| POST | /matches | Yes | Create listing; sets host; auto-adds host as participant |
| GET | /matches/:id | No | Detail + participants + games |
| PUT | /matches/:id | Yes (host) | Update title/date/max_players/level/status/court_id |
| DELETE | /matches/:id | Yes (host) | Soft delete |
| POST | /matches/:id/join | Yes | Join roster; 409 if full/already joined |
| DELETE | /matches/:id/leave | Yes | Leave roster (host cannot leave) |
| POST | /matches/:id/participants | Yes (host) | Body { "customer_id"? } or { "display_name"? } |
| DELETE | /matches/:id/participants/:pid | Yes (host) | Kick / remove guest |
| POST | /matches/:id/generate | Yes (host) | Body { "is_double": bool } → create games, status→Ongoing |
| PUT | /matches/:id/games/:gid | Yes (participant) | Body { score_a?, score_b?, status? } → recalc points |
| GET | /matches/:id/leaderboard | No | Participants sorted by points desc |
Create body:
{ "title": "Open Mixed", "date": "2026-08-12T10:00:00Z", "max_players": 8, "level": "Intermediate" }
Detail response (core):
{
"id": "uuid",
"title": "Open Mixed",
"status": "Ongoing",
"is_double": true,
"host_customer_id": "uuid",
"max_players": 8,
"participants": [
{ "id": "pid", "customer_id": "uuid", "display_name": "Rafa", "role": "host", "points": 12 }
],
"games": [
{
"id": "gid",
"sequence": 1,
"team_a": ["pid1", "pid2"],
"team_b": ["pid3", "pid4"],
"score_a": 6,
"score_b": 4,
"status": "Completed"
}
]
}
Status match: Open | Ongoing | Completed. Game status: Pending | Ongoing | Completed.
Leaderboard points: for each Completed game, each player on a team adds that team's score.
11. Reviews (NEW - v2026-07-21)
11.1 Get Active Review Questions
- URL:
GET /reviews/questions - Response Data: Array dari objek pertanyaan yang disetup oleh admin.
[
{
"id": "uuid-q1",
"question": "Bagaimana kebersihan lapangan?",
"type": "TEXT",
"is_active": true,
"display_order": 1
}
]
11.2 Submit Review
- URL:
POST /reviews - Header:
Authorization: Bearer <token> - Request Body:
{
"booking_id": "uuid-booking",
"rating": 5,
"comments": "Lapangan sangat bagus!",
"answers": [
{
"question_id": "uuid-q1",
"answer": "Sangat bersih"
}
]
}
- Response Data: Objek review yang baru saja disimpan.
12. CMS / Home (NEW - v2026-07-21)
12.1 Get Home Content
- URL:
GET /content/home - Response Data: Data agregat untuk Home Screen (Court, Promo, Matches, Top Players, Testimonials, Club Stats).
{
"courts": [...],
"active_promos": [...],
"top_players": [...],
"open_matches": [...],
"testimonials": [...],
"club_stats": {...}
}
active_promos[](NEW - v2026-08-15): tambahimage_url(string, nullable) dandisplay_order(int, default 0). Diurutkandisplay_order ASC, created_at ASC. Dipakai Android/iOS untuk render carousel gambar di Home; promo tanpaimage_urltetap ada di response tapi di-skip client dari carousel.- Static assets (NEW - v2026-08-15):
GET /static/*— file publik tanpa auth langsung dari disk server (./staticdi working dir mpc-be). Promo images di-hotlink lewatimage_urlsepertihttps://mpc-be.glenrh.com/static/promos/<file>.png. Bukan endpoint upload — file ditaruh manual/rsync ke server; tidak ada API untuk upload dari admin saat ini.
13. Settings (NEW - v2026-07-21)
13.1 Get Operating Hours
- URL:
GET /settings/operating-hours - Response Data: dari default branch (
open_time/close_time), fallback06:00–23:00bila belum ada branch.
{
"open_time": "06:00",
"close_time": "23:00"
}
13.2 Get FnB Pickup Settings
- URL:
GET /settings/fnb-pickup - Response Data: dari
store_settings(default 30 / 15).
{
"before_minutes": 30,
"interval_minutes": 15
}
- Arti: window pickup = mulai
before_minutessebelum start booking court hingga end booking; pilihan berjarakinterval_minutes.
14. Resto Spaces & Reservations (NEW - v2026-08-13)
Reservasi tempat di resto (Room VIP privat & area meja/TableArea). Gratis (Rp 0) — tanpa pembayaran & tanpa pajak. minimum_spend bersifat display-only (komitmen yang dipenuhi lewat pembelian FnB di venue). Durasi reservasi fixed dari store_settings.space_reservation_duration_minutes (default 90 menit); member hanya memilih jam mulai. Jam operasional resto: store_settings.resto_open_time–resto_close_time (default 10:00–22:00).
Aturan availability (reservasi "aktif" = status Confirmed/Seated, belum soft-delete):
Room: eksklusif — slot tersedia hanya jika tidak ada reservasi aktif yang overlap. Syaratpax <= capacity_pax.TableArea: pool pax — slot tersedia jikaSUM(pax reservasi aktif overlap) + pax <= COALESCE(online_capacity_pax, capacity_pax).paxjuga dibatasimax_party_pax(di atas itu → error 400, arahkan member "hubungi kami untuk rombongan besar").
14.1 Get Spaces
- URL:
GET /spaces|GET /spaces?branch_id=<uuid>(tanpa auth, sama seperti/courts) - Response Data: array space
Active+is_online_reservable, urutdisplay_order.max_party_paxsudah efektif: Room =capacity_pax; TableArea = min(max_party_pax, kuota online).
[
{
"id": "uuid-space",
"name": "Room VIP 1",
"code": "SPC-VIP-1",
"space_type": "Room", // "Room" | "TableArea"
"capacity_pax": 12,
"max_party_pax": 12,
"minimum_spend": 2000000.0, // null bila tidak ada
"image_url": null,
"description": "Ruang privat untuk 12 tamu dengan layanan khusus.",
"display_order": 3
}
]
14.2 Get Space Availability
- URL:
GET /spaces/:id/availability?date=2026-08-14&pax=4(paxdefault 2) - Slot per 30 menit dari
resto_open_timesampai (resto_close_time− durasi), venue wall-clock. Slot yang sudah lewat (untuk hari ini) =available: false. paxmelebihi batas (lihat aturan di atas) →400dengan pesan jelas.- Response Data:
{
"date": "2026-08-14",
"duration_minutes": 90,
"slots": [
{ "start_time": "10:00", "end_time": "11:30", "available": true },
{ "start_time": "10:30", "end_time": "12:00", "available": false }
]
}
14.3 Create Reservation
- URL:
POST /spaces/reservations - Header:
Authorization: Bearer <token> - Request Body:
{
"space_id": "uuid-space",
"date": "2026-08-14",
"start_time": "14:00",
"pax": 6,
"notes": "Ulang tahun" // opsional
}
- Durasi otomatis (fixed). Status
Confirmed, sourceOnline,customer_iddari token,codeauto (RSV-xxx),minimum_spend_snapshotdi-snapshot dari space saat pembuatan. Availability dicek ulang di dalam transaksi DB dengan row lock per space (race-safe). - Error:
400(slot tidak valid / masa lalu / pax melebihi batas),409(slot sudah penuh/terisi). - Response Data: objek reservation lengkap termasuk
space_name,space_type,minimum_spend_snapshot:
{
"id": "uuid-reservation",
"space_id": "uuid-space",
"customer_id": "uuid-customer",
"reservation_date": "2026-08-14T00:00:00Z",
"start_time": "2026-08-14T14:00:00Z",
"end_time": "2026-08-14T15:30:00Z",
"pax": 6,
"status": "Confirmed",
"source": "Online",
"notes": "Ulang tahun",
"minimum_spend_snapshot": 2000000.0,
"booking_session_id": null,
"code": "RSV-001",
"space_name": "Room VIP 1",
"space_type": "Room",
"space_image_url": null
}
14.4 Get My Reservations
- URL:
GET /spaces/reservations/me - Header:
Authorization: Bearer <token> - Response Data: array reservation milik member (exclude soft-deleted), urut
start_timedescending, termasukspace_name,space_type,space_image_url, danstatus. Reservasi hasil bundling court muncul dengansource: "CourtBundle"+booking_session_id. orders(array, NEW v2026-08-13, hanya muncul bila ada —omitempty): pre-order F&B yang ditempel ke reservasi ini lewatPOST /ordersdenganspace_reservation_id(lihat §5.1). Satu reservasi bisa punya lebih dari satu order.
{
"id": "uuid-reservation",
"code": "RSV-001",
"space_name": "Indoor Dining",
"status": "Confirmed",
"orders": [
{
"transaction_code": "TRX-0042",
"status": "Pending",
"total_amount": 165000.0,
"created_at": "2026-08-14T13:05:00Z",
"items": [
{ "name": "Iced Latte", "quantity": 2, "unit_price": 45000.0, "subtotal": 90000.0 },
{ "name": "Club Sandwich", "quantity": 1, "unit_price": 75000.0, "subtotal": 75000.0 }
]
}
]
}
14.5 Cancel Reservation
- URL:
POST /spaces/reservations/:id/cancel - Header:
Authorization: Bearer <token> - Hanya reservasi milik sendiri, status
Confirmed, dan sebelumstart_time→ status jadiCancelled. - Reservasi
source: "CourtBundle"tidak bisa dibatalkan lewat endpoint ini (400) — batalkan lewat cancel booking court/session (cascade otomatis).