Coupons / Promo codes
Base paths: /api/coupons (customer) and /api/admin/coupons (admin)
Apply-on-booking is server-side. The app may preview a discount via validate, but the booking create path re-validates, locks the coupon, writes coupon_usages, and sets discount_amount / total_amount. Payment initiate always charges booking.total_amount.
Flow
1. Venue owner creates a coupon for one venue
2. App: GET /coupons → all live venue deals; GET /coupons?venue_id= → that venue only
3. App: POST /coupons/validate { code, amount, venue_id } → preview discount
4. App: POST /bookings { …, coupon_code } → server applies discount + records usage
5. App: POST /payments/initiate → pays the discounted total
6. Cancel unpaid booking → coupon usage released so the code can be reused
GET /api/coupons
| Query | Behavior |
|---|---|
| _(none)_ | All active venue deals |
venue_id | That venue’s active coupons only |
POST /api/coupons/validate [Auth]
{ "code": "SAVE200", "amount": 1500, "venue_id": 1 }Venue-scoped coupons require venue_id. Response includes discount_amount / final_amount.
POST /api/bookings [Auth]
{
"court_id": 1,
"date": "2026-08-27",
"start_time": "10:00",
"end_time": "11:00",
"coupon_code": "SAVE200"
}Server computes pricing, applies coupon (if present), returns booking with discount_amount and reduced total_amount.
Admin CRUD — /api/admin/coupons
| Method | Notes |
|---|---|
GET /?per_page=&venue_id= | Owners: own venues. Super: all / filter |
POST / | venue_id is required. Percent value ≤ 100. Code uppercased |
PUT /{coupon} | value, valid_until, is_active, usage_limit |
DELETE /{coupon} | Soft ownership via policy |
Rules
- One use per user per coupon (unique
coupon_id+user_id) usage_limit/used_countenforced under row lock on apply- Unpaid cancel releases usage and decrements
used_count - Client-supplied discount amounts are ignored