Payments
Base path: /api/payments
Supports eSewa, Khalti, FonePay, cash, and wallet. Online app payments create a pending payment row on initiate, then complete it after gateway verification (idempotent).
Happy path (customer app + eSewa)
1. POST /api/bookings → booking pending, paid_amount = 0
2. POST /api/payments/initiate { booking_id, method: "esewa" } → pending payments row + signed eSewa v2 form fields
3. App auto-POSTs the form to gateway_url (RC: https://rc-epay.esewa.com.np/api/epay/main/v2/form)
4. User pays on eSewa (sandbox ID 9806800001 / MPIN 1234)
5. eSewa redirects to success_url with ?data=<base64>
6. App calls POST /api/payments/esewa/verify { data } or browser hits GET /api/payments/esewa/callback
7. Backend verifies with eSewa status API → payment completed, booking confirmed, paid_amount updated
8. Play counter shows the session as App / Paid · esewa, Due 0 (extras can still be added)
Env (eSewa RC / test)
ESEWA_MERCHANT_ID=EPAYTEST
ESEWA_SECRET="8gBm/:&EnhH.1/q"
ESEWA_URL=https://rc-epay.esewa.com.npPOST /api/payments/initiate [Auth]
{ "booking_id": 42, "method": "esewa", "return_url": null, "failure_url": null }Response (eSewa):
{
"gateway": "esewa",
"payment_id": 10,
"payment_number": "PAY-XXXXXXXX",
"booking_id": 42,
"booking_number": "BK-…",
"amount": "1500.00",
"status": "pending",
"gateway_url": "https://rc-epay.esewa.com.np/api/epay/main/v2/form",
"amount": "1500.00",
"tax_amount": "0.00",
"total_amount": "1500.00",
"transaction_uuid": "PAY-XXXXXXXX-1712345678",
"product_code": "EPAYTEST",
"product_service_charge": "0.00",
"product_delivery_charge": "0.00",
"success_url": "https://api…/api/payments/esewa/callback",
"failure_url": "https://api…/api/payments/esewa/failure",
"signed_field_names": "total_amount,transaction_uuid,product_code",
"signature": "…"
}Gateway callbacks (no auth)
| Method | Path |
|---|---|
| GET/POST | /api/payments/esewa/callback |
| GET/POST | /api/payments/esewa/failure |
| POST | /api/payments/khalti/callback |
| POST | /api/payments/fonepay/callback |
App verify (auth)
POST /api/payments/esewa/verify { "data": "<base64 from eSewa redirect>" }
Venue payments table (staff)
GET /api/venues/{venueId}/payments?date=YYYY-MM-DD
Lists completed + pending gateway/cash rows for the venue (used by admin Payments page).
Cash (staff)
POST /api/payments/cash { booking_id, amount } or { indoor_game_booking_id, amount }
Play counter “Collect & close” uses session-bill complete, which records cash via this service.
Idempotency / conflict rules
- Initiate creates
payments.status = pendingwith uniquegateway_uuid - Re-initiate marks previous pending rows for that booking+method as
failed - Callback/verify looks up by
gateway_uuid(not eSewatransaction_code) - Completed
transaction_idis unique — replay returns the same payment, does not double-incrementpaid_amount - Applied amount is capped at remaining balance due
Security controls
- eSewa redirect payload must pass HMAC signature check; status must be COMPLETE from eSewa status API (client status alone is ignored)
- Paid amount must match the initiated pending amount (± Rs 0.05)
- Client
return_url/failure_urlare ignored (server-only callback URLs — blocks open redirects) - Initiate / verify / cash / refund are authorized (owner or venue ops); refunds limited to owner/manager/super_admin
- Free wallet top-up is disabled (no unverified credit)
- Sandbox eSewa credentials are refused in production
- Rate limits:
throttle:payments(auth) andthrottle:payment-callbacks(public) - Payment resource does not expose
gateway_uuidin list responses