api / be

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 → 409 dengan error email 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_KEY kosong 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 berdasarkan display_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) serta bundled_space (lihat 2.1).

2.3 Get Court Availability (Availability Hourly)

  • URL: GET /courts/:id/availability?date=2024-10-01 (branch_id opsional)
  • Occupancy: Hours are venue wall-clock. A non-Canceled booking (including Completed / checked-out) marks every hourly slot in [start_time, end_time) as is_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_id opsional)
  • 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 id amenity_id pada Checkout array amenities.
  • 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!
  • 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 bookings lama 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_spacebest-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) dengan booking_session_id yang sama ikut berstatus Cancelled.

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 produk product_type=FnB dan ada bookings. Window: dari (min(booking.start) − before_minutes) sampai max(booking.end), kelipatan interval_minutes (dari GET /settings/fnb-pickup). Tanpa booking court atau tanpa FnB → abaikan / jangan kirim. Disimpan di transactions.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/404 Reservation not found, 403 Forbidden bila bukan pemilik), lalu menyimpan di transactions.space_reservation_id. Order ini lalu muncul di orders[] reservasi tsb pada GET /spaces/reservations/me (lihat §14.4). Tidak mengubah fnb_pickup_time (tetap opsional, hanya wajib bila ada booking court + FnB).
  • Response Data: Objek Transaction (termasuk detil item, tax_amount 11%, discount_amount, dan fnb_pickup_time bila ada).
  • Catatan Diskon Otomatis (TierBenefit): Backend akan secara otomatis mengecek paket MembershipTier pelanggan dan mengevaluasi batasan benefit yang bisa dipakai (contoh: Diskon 40% max 3x per bulan). Jika valid, kuota pengguna (BenefitUsage) dipotong dan discount_amount akan bertambah secara otomatis pada Response Data.
  • Catatan Pembayaran: Transaksi juga memuat info pembayaran seperti payment_url, va_number, dan qris_string untuk 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 membuat space_reservation untuk Room tersebut (source CourtBundle, status Confirmed, 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 opsional bundled_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: customerId di URL harus cocok dengan id di 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 Transaction baru (status Pending, transaction_type = Tip, satu item item_type: "Fee" bernama "Tip" dengan reference_id = id order asli). Client membayar invoice ini lewat flow pembayaran existing (POST /transactions/:id/complete) — sama seperti reschedule fee, bukan POST /orders baru.
  • 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 amountamount < 5000.
  • Catatan: tip disimpan sebagai Transaction biasa (transaction_type: "Tip"), bukan kolom baru — sehingga tidak ada perubahan kontrak data pada bookings/transactions selain field response baru di bawah.
  • Field baru pada Transaction response (§5.2, §5.3, dan checkout POST /orders): tip_amount (number, opsional, omitempty) — muncul pada order yang sudah di-tip (invoice Tip-nya sudah Completed), 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 as product_type, category_id, group_id, display_order, group_name, category_name, variants seperti "Ice", "Hot", dan addons seperti "Extra Shot"). Data diurutkan ascending berdasarkan display_order. Member apps may use image_url as the primary photo; gallery is 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 (Ditambahkan image_url, description, target_tiers, gallery, bracket, results [NEW - v2026-07-21], dan endpoint POST /events/:id/register).
  • Coaches: /coaches (Ditambahkan image_url, description, achievements).
  • Promos: /promosmarketing / home banners only (CMS active_promos). Not applied at checkout.
  • Vouchers: /vouchersredeemable 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 on Notify.
  • 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_type supports Court | Coach | Class | Player.
  • Classes: GET /classes — catalog managed via admin CRUD.

10.2 Notifications & push

MethodPathAuthNotes
GET/notifications/customer/:customerIdYes (own id)Inbox list
GET/notifications/meYesInbox for JWT customer
PUT/notifications/:id/readYesMark read
POST/device-tokensBearer customer or X-Admin-KeyBody: { token, platform, app?, audience?, audience_id? } — customer JWT forces audience=customer
DELETE/device-tokensBearer / Admin keyBody: { token }
POST/admin/notifications/broadcastAdmin keyPromo to all customers (inbox+push+email)
POST/admin/notifications/pushAdmin keyPush-only to audience tokens
POST/admin/notifications/notify-customerAdmin keyFull 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.

MethodPathAuthNotes
GET/matches?status=OpenNoList; default status Open; includes participant_count
POST/matchesYesCreate listing; sets host; auto-adds host as participant
GET/matches/:idNoDetail + participants + games
PUT/matches/:idYes (host)Update title/date/max_players/level/status/court_id
DELETE/matches/:idYes (host)Soft delete
POST/matches/:id/joinYesJoin roster; 409 if full/already joined
DELETE/matches/:id/leaveYesLeave roster (host cannot leave)
POST/matches/:id/participantsYes (host)Body { "customer_id"? } or { "display_name"? }
DELETE/matches/:id/participants/:pidYes (host)Kick / remove guest
POST/matches/:id/generateYes (host)Body { "is_double": bool } → create games, status→Ongoing
PUT/matches/:id/games/:gidYes (participant)Body { score_a?, score_b?, status? } → recalc points
GET/matches/:id/leaderboardNoParticipants 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): tambah image_url (string, nullable) dan display_order (int, default 0). Diurutkan display_order ASC, created_at ASC. Dipakai Android/iOS untuk render carousel gambar di Home; promo tanpa image_url tetap 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 (./static di working dir mpc-be). Promo images di-hotlink lewat image_url seperti https://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), fallback 06:0023:00 bila 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_minutes sebelum start booking court hingga end booking; pilihan berjarak interval_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_timeresto_close_time (default 10:0022:00).

Aturan availability (reservasi "aktif" = status Confirmed/Seated, belum soft-delete):

  • Room: eksklusif — slot tersedia hanya jika tidak ada reservasi aktif yang overlap. Syarat pax <= capacity_pax.
  • TableArea: pool pax — slot tersedia jika SUM(pax reservasi aktif overlap) + pax <= COALESCE(online_capacity_pax, capacity_pax). pax juga dibatasi max_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, urut display_order. max_party_pax sudah 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 (pax default 2)
  • Slot per 30 menit dari resto_open_time sampai (resto_close_time − durasi), venue wall-clock. Slot yang sudah lewat (untuk hari ini) = available: false.
  • pax melebihi batas (lihat aturan di atas) → 400 dengan 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, source Online, customer_id dari token, code auto (RSV-xxx), minimum_spend_snapshot di-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_time descending, termasuk space_name, space_type, space_image_url, dan status. Reservasi hasil bundling court muncul dengan source: "CourtBundle" + booking_session_id.
  • orders (array, NEW v2026-08-13, hanya muncul bila ada — omitempty): pre-order F&B yang ditempel ke reservasi ini lewat POST /orders dengan space_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 sebelum start_time → status jadi Cancelled.
  • Reservasi source: "CourtBundle" tidak bisa dibatalkan lewat endpoint ini (400) — batalkan lewat cancel booking court/session (cascade otomatis).