Base: https://api.sajilosport.com/api

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.np

POST /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)

MethodPath
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 = pending with unique gateway_uuid
  • Re-initiate marks previous pending rows for that booking+method as failed
  • Callback/verify looks up by gateway_uuid (not eSewa transaction_code)
  • Completed transaction_id is unique — replay returns the same payment, does not double-increment paid_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_url are 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) and throttle:payment-callbacks (public)
  • Payment resource does not expose gateway_uuid in list responses