Skip to content

Partner payments API

The routes behind the partner portal's Payments page and the Pay online / I have paid buttons on a partner's booking. Rules: PTR-081 (a claim), PTR-093 (the web portal), FIN-032 (a claim stays pending). Screen: Partners → Payments on the web portal.

Handler: handlePartnerPayments in src/lib/partnerPayments.ts, loaded on demand by apiFetch (like /screens/*), so the API chunk every screen downloads does not carry it. Client: src/services/partnerPaymentsService.ts.

Who may call them

A partner's login (role = agent). There is no matrix permission (PERMISSIONS.md §4.5): the database decides by ownership.

  • partner_submit_payment() takes the agency from the session (auth_agent_id()) and refuses a booking or invoice that is not the agency's.
  • razorpay-order asks online_payment_actor(), with the caller's own session, whether the caller is the booking's agency.
  • The reads run as the partner, under the agency row security the app reads through (PTR-010); the handler also keeps only rows whose Booking.agentId or GroupInvoice.payerId is the agency, so a login that is also a traveller does not see its own trips here.

GET /portals/agent/payments

Everything the page shows, from six reads sent side by side — one round trip (PRF-010): partner_my_account(), the agency's bookings (latest 500), its group invoices (latest 200), payments pending or verified (latest 200), payments rejected in the last 90 days (latest 50, with rejectionReason) and online orders still created in the last hour.

{
  "account": { "agentId": "…", "company": "…", "partnerCode": "BP-0001", "status": "active",
               "currency": "INR", "creditLimit": 500000, "outstanding": 180000, "creditLeft": 320000 },
  "bookingsDue": [{ "id": "…", "bookingNo": "BK-…", "customerName": "…", "groupName": "…",
                    "departureDate": "2026-10-20", "currency": "INR", "total": 105000, "paid": 20000,
                    "balance": 85000, "pending": 10000, "claimable": 75000, "payableOnline": true, "awaitingApproval": false,
                    "onlineStarted": null }],
  "invoicesDue": [{ "id": "…", "invoiceNumber": "INV-…", "groupName": "…", "issuedAt": "…", "dueDate": "…",
                    "currency": "INR", "total": 90000, "balanceDue": 90000, "pending": 0, "claimable": 90000 }],
  "pending": [{ "id": "…", "amount": 10000, "method": "upi", "reference": "…", "status": "pending",
                "online": false, "createdAt": "…", "verifiedAt": null,
                "target": { "kind": "booking", "id": "…", "label": "Booking BK-…" } }],
  "recent": [ … the same shape, status "verified", the latest 20 … ],
  "rejected": [ … the same shape, status "rejected", decided in the last 90 days, the latest 20,
                with "rejectionReason": "No credit for UTR … on our statement" … ]
}
  • Every row has rejectionReason; it is null except on a rejected claim (COMM-037). A rejected claim is not counted in pending or claimable.

  • outstanding is portal_agent_outstanding(), as the app shows it.

  • creditLeft is null when no credit limit is set.
  • balance is the booking's agreed due — price + GST − receipts − cancellation credit (computed field booking_agreed_due), approved voucher or not — what razorpay-order caps an online payment at (FIN-033). awaitingApproval is true while finance has not approved the booking voucher; the page then shows "Awaiting finance approval — ₹X will be due".
  • claimable is the agreed due less the claims waiting — what partner_submit_payment will still accept. For an invoice, balanceDue less the claims waiting.
  • payableOnline is false for a booking not priced in rupees.
  • Cancelled and refused bookings, draft, paid and cancelled invoices and credit notes are left out.

404 when the login is not linked to a partner.

POST /portals/agent/payments — I have paid

Input — { amount, method, reference?, bookingId? , groupInvoiceId? }: exactly one of bookingId and groupInvoiceId; method one of bank_transfer, upi, cheque, cash, card. The route refuses a missing or double target, an unknown method or an amount of zero with 400 before the database; everything else is partner_submit_payment()'s decision:

Refusal Status Sentence (the database's)
more than is still due 409 "Amount is more than the outstanding balance of … (including payments awaiting verification)"
no reference on a non-cash payment 400 "A upi payment needs its reference (UTR, UPI id, cheque number)"
a cancelled or refused booking; an invoice not issued 409 "Payments cannot be submitted for a cancelled booking" / "This invoice is paid and cannot be paid"
another agency's booking or invoice 403 "You do not have access to this booking"

Output — { id, status: "pending", amount, currency }, or { id, status: "pending", amount, duplicate: true } for the same claim sent again within two minutes (recorded once). The claim's note reads "Submitted by partner through the app" for web and app alike.

Not accepted: a payment date, a note, a receipt file. The function takes none of them.

POST /portals/agent/payments/online-order — Pay online

Input — { bookingId, amount } (rupees). Forwards to razorpay-order with purpose: "Partner portal (web)" and answers with its order (orderId, amount in paise, keyId, prefill, …). Its refusals come through with their status and sentence — including 503 "Online payment is not switched on yet — …" until the Razorpay keys are set.

The page then opens Razorpay Checkout (src/lib/razorpayCheckout.ts). The payment is recorded by razorpay-webhook, never by the page.

GET /portals/agent/payments/online-order/:id

{ status: "paid" | "created", paymentId }: paid once OnlinePaymentOrder.status is paid or a verified Payment carries that razorpayOrderId. The page asks every three seconds for up to a minute after Checkout reports success.

Where to look

Concern Path
Routes src/lib/partnerPayments.ts (+ api.partnerPayments.test.ts)
Page src/pages/partner/PartnerPayments.tsx (+ .test.tsx)
Dialogs src/components/partner/PartnerPaymentDialogs.tsx
Checkout in the browser src/lib/razorpayCheckout.ts (+ .test.ts); the CSP hosts in public/_headers (Security headers)
The claim function partner_submit_payment() — supabase/migrations/20260929140000_profiles_and_a_paused_account.sql; test supabase/tests/the_partner_in_the_app.sql