Skip to content

Portals — partners and customers

Two outside audiences reach the system through their own screens: B2B partners (agents) and end customers. Neither ever holds a staff permission (ACC-010).

Rules: 11 · Partners (PTR), C360-002, ACC-002, ACC-010, ACC-051.


1. The one rule that matters

A portal user never writes a table directly. Every partner and customer action is a SECURITY DEFINER database function that takes the actor from the session. The browser sends what it wants; the function decides what happens.

That is not a style preference. Before it was true:

  • partner sign-up wrote the agency row with staff-only table rights, and stored identity documents behind public links;
  • a partner login was matched to its agency by email where no link existed, so anyone who signed up with a partner's address could take over that agency;
  • the partner booking wizard sent a price the partner typed and the database stored it;
  • the credit limit was displayed and never enforced;
  • partners could read every booking, customer (with passport, Aadhaar and PAN), airline block (with PNR and cost) and other partners' purchases;
  • partners could set their own invoices to PAID and edit issued invoices.

2. Identity

A partner login belongs to one agency through Agent.userId only — never by matching an email address (PTR-003). The same holds for customers: Customer.userId. Staff link an existing agency or customer to a login explicitly.

Public sign-up creates customer accounts — with an email address (TRV-007) or with a code on WhatsApp (TRV-016), or by writing "sign up" to the business number on WhatsApp (TRV-017). Every way, sign-up makes a login only; the customer record follows with the first booking request (LC-006, LC-OPEN-5 decided 2026-10-01). A partner registers and lands pending; staff approve them by setting the status to active. A login that already holds a staff role cannot register as a partner (PTR-001, ACC-051).

A partner whose status is anything but active can sign in and read their own records but cannot create bookings, add passengers, reserve beds, sell seats, issue invoices or raise requests (PTR-002).

3. What a partner can see

Their own agency; their agency's customers and bookings, with those bookings' passengers, payments, invoices, tickets and visa cases; their seat purchases and resales; their invoices and documents; hotel beds they reserved; their statement entries. Passport numbers in lists are masked to the last four characters (PTR-010, AUD-020).

Offer lists shown to partners never include airline block cost, PNR, internal notes, hotel contract cost or who else bought (PTR-060).

4. Partner screens

Route Screen
/partner/auth sign in and register (public)
/partner home
/partner/bookings, /partner/bookings/new bookings and the booking wizard
/partner/invoices partner-branded invoices
/partner/payments what the agency owes, Pay online and I have paid, and claims not accepted with the reason (Partners → Payments on the web portal, PTR-093, COMM-037)
/partner/inventory, /partner/flights, /partner/hotels seat resale, flight seats on offer (flights only — PTR-094), bed reservations
/partner/reports reports
/me (/partner/profile redirects here) the agency profile, own documents, name and contact person (Partners → My profile)

All gated to the agent role tier.

Pricing, credit and capacity

  • Price comes from the rate sheet. A partner booking on a group is priced per passenger from the group's rate sheet for that passenger's category; a B2B flight offer uses the published seat price and infants are zero. Any price, rate or category the partner sends is ignored. A group without a published rate sheet cannot be booked by partners (PTR-020).
  • Category comes from the date of birth on the group departure date — under 2 infant, 2–11 child, 12 and over adult. Date of birth and passport number are required for every passenger (PTR-021).
  • The credit limit is enforced, under a row lock on the agency: outstanding booking totals plus open issued group invoices plus the new booking may not exceed it. It binds the office's bookings for the partner too; only a finance manager, the CEO or the GM can go over it, with a recorded reason. A partner cannot. A limit of 0 or blank currently means no limit set — whether it should instead mean "prepay only" is an open question (PTR-030).
  • Capacity is reserved in the same transaction that creates the booking; if anything fails, nothing is kept (PTR-031).
  • A partner booking starts in PENDING_OPS, on account, with the partner login recorded as creator. Passengers can be added only while it is DRAFT, PENDING_OPS or NEEDS_CORRECTION. A second identical submission within two minutes returns the first booking (PTR-022).
  • A booking that is not ready is refused when it is made, with the words a staff booking gets when it is submitted — "Booking is not ready to submit: …": every traveller needs a name, date of birth and passport, a child or infant needs an adult on the booking, and there is at most one infant per adult (PTR-022). Nothing is kept.
  • Sent back for correction. When Alhuda sends a booking back, the bookings list says Sent back for correction and the row has Send for approval. The partner writes what was corrected and the booking goes back to the team that sent it back, after the same checks (PTR-023). A booking finance rejected is not sent back from here — raise a request. The partner app has the same button on the booking (Native app).
  • Bed reservations and seat resale are atomic, against one counter under a row lock, so a partner cannot reserve or resell more than remain (PTR-040, PTR-041).
  • Partner invoices are numbered atomically, totalled from their line items, and are DRAFT or ISSUED only — PAID comes from recorded payments, and an issued invoice cannot be edited (PTR-050).

5. Customer screens

Route Screen
/customer/auth sign in and sign up (public)
/customer the portal — one page with tabs
/customer/set-password?t=… the first password of an account made in the WhatsApp chat (a one-time link from the chat, 30 minutes)
/pay/<token> pay a booking's balance with Razorpay from a link the WhatsApp menu sent (once, 30 minutes; no login)
/visa/apply the public visa intake form (no login)

Sign up on /customer/auth has two ways, picked at the top of the Sign Up tab:

  • Mobile (WhatsApp), the default (TRV-016): name, mobile number and an optional email, the captcha, Send code on WhatsApp; then the 6-digit code; then a password typed twice (each box has the eye). The account is made and the person lands on /customer. The page always says the same thing after "Send code" — a number that already has an account gets no code — and always shows Already have an account? Sign in · Forgot password. A wrong or old code takes the person back to the code step. If WhatsApp codes are not switched on, the page says so and offers Sign up with email instead.
  • Email (TRV-007): name, email and password, and the captcha. The customer-signup function makes the login and mails a confirmation link; the page shows its one message (the same for an address that already has an account) and moves to Sign In. The link brings the person back to /customer/auth with "Your email is confirmed. Sign in with your email and password."

Sign up in the WhatsApp chat (TRV-017): a customer writes "sign up" to +91 95419 10494 (or taps https://wa.me/919541910494?text=Sign%20up, or scans its QR code), fills the form WhatsApp shows (name, city, email, privacy consent) and gets a link back in the chat. The link opens /customer/set-password: a password typed twice, Set password and sign in, and they land on /customer. The page takes the link's token out of the address bar as soon as it opens. A link that is wrong, expired or used shows one message and Go to sign in; they use Forgot password → WhatsApp code. A number that already has an account is told so in the chat and gets no form. The app has no screen of its own for this — it opens the same web page. Sign-up only: no orders or bookings over WhatsApp.

A new login has no customer record (either way of signing up). The portal reads empty — no trips, quotations or requests — and My Profile says "Your details will be added with your first booking." The first Apply on an available group (a group_interest request) makes the record from the login's name, number and email; the office then asks for the passport and other details. A question or any other request sent before that is refused with "Your details will be added with your first booking. Send a booking request first, or call the office." (409).

A customer can submit a payment, accept or decline a quotation, raise and respond to requests, and update their own profile — each through its own ownership-checked function.

Request account deletion (AUD-025 … AUD-027): a card at the foot of My Profile (src/components/account/DeleteMyAccountCard.tsx, loaded only when the tab opens) and, for a partner, at the foot of /me. It says what is erased and what the law keeps, asks for DELETE and the password, and calls the account-delete function. Nobody erases their own account: this always sends a request, and the card then reads "Your request has been sent. The office will review your account and contact you." The person stays signed in; the office approves or declines within 30 days. What the office settles first is listed in plain words — a trip not finished, a balance, money the company holds for the person ("We hold ₹5000.00 for you on booking BK-4 — the office will settle it with you first."), a refund waiting, an unused advance, a payment claim, an open request, an unpaid invoice, a paused account. A declined request shows the office's note and may be sent again. The app has the same screen (native app).

The customer portal in the app

The same customer login opens the traveller's stack of the native app (TRV-001 … TRV-009): their trip (booking, departure, tour leader, passengers, emergency contact, balance), the departure's programme and notices live on the phone, a step-by-step guide for their trip type, a passport scan that waits for staff review, and a location-sharing switch that is theirs alone (FLD-006).

The app is also the customer portal on a phone, through the same functions as this page:

  • Sign up in the app has the same two ways: Mobile (WhatsApp) — the code, a login only (TRV-016); the sign-up in the WhatsApp chat (TRV-017) needs no app at all — and Email, through the customer-signup function, which makes a CUSTOMER login — no customer record — and mails the confirmation link (TRV-007).
  • Tours are the website's posters (public_departures, public_groups), and can be browsed before signing in. Book this trip sends a group_interest request through customer_create_request — the same request this portal's "I'm interested" sends. The office confirms it, sends a quotation, and creates the booking; a traveller never writes a booking (TRV-008, LC-006).
  • Requests, quotations and payment claims — customer_create_request, customer_respond_quotation, customer_submit_payment — are in the app under Requests and quotes. A payment reported from the app is pending until finance verifies it (FIN-032); the app does not take money, and sends no proof file. When finance rejects a claim the customer reported, the customer is e-mailed Payment claim not accepted with the reason; when a payment is verified, the receipt e-mail goes to the customer (or the payer) (COMM-037, COMM-038). The customer's screens do not list a rejected claim — only the partner's do. My details is customer_update_profile (TRV-009).

A partner has their own stack in the app — see Partners → The partner portal in the app.

6. What is not built

The portals are due a rebuild

Both portals work and are safe, but they are the September 2026 versions of screens designed earlier. Wave 3 rebuilds them.

  • Customer 360 parity. C360-002 says the customer portal should show the same Now / Itinerary / Travellers / Documents / Money / Visa sections as staff see, minus internal notes, costs and restricted medical detail. The server functions already allow a customer to call them for their own bookings, and that is tested — the portal UI has not been built on them yet.
  • Confirmation dialogs in the portals. UX-001 requires every state-changing action to ask Yes/No with a summary. Staff screens are covered (coverage list); partner pages use the same useConfirm for their main actions; the customer portal is Wave 3.
  • Online payment (Razorpay) from the web customer portal. It is built in the native app (Requests and quotes → Payments → Pay online, and the partner's Money → Pay online): razorpay-order creates the order, Checkout runs on the phone, razorpay-webhook records the verified receipt — API → Online payment. The web portal still has no button. Until the owner sets RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET the app says online payment is not switched on and points to I have paid.
  • Partner wallet and statement of account, agency sub-users, holds with deadlines in the partner booking flow — all Wave 3. A partner has a credit limit, not a wallet.
  • A cancellation request with a policy preview from the customer portal.
  • Per-permission write tightening on a set of operational tables that are still gated by a blanket staff check rather than a specific permission. AGENT is excluded from that check, so it does not widen partner access, but it is not least privilege (ACC-002 is still PROPOSED).
  • Partner and customer dashboard profiles. The twelve role dashboards are staff jobs; the portals have their own hand-built home pages.

7. Where to look

Concern Path
Isolation, portal functions supabase/migrations/20260918100000_data_isolation_portals.sql
Partner screens src/pages/partner/
Customer portal src/pages/customer/CustomerPortal.tsx
Public visa intake supabase/functions/visa-intake/, src/pages/VisaIntakeForm.tsx
Staff-side partner admin /agents, src/pages/agents/
Tests supabase/tests/data_isolation.sql

The customer's money summary

Billed, Paid and Balance on the customer portal (and its printed account statement) are the bookings' own figures — the ones staff see: Paid is verified receipts net of refunds; Balance is the booking's balance — price + GST once finance approves the booking voucher (0 until then), − paid − cancellation credit (FIN-033); Billed is Paid + Balance, so the three always add up. A booking still waiting for finance therefore adds nothing to Balance. It is not hidden: under Balance Due the summary says ₹B more once finance approves — the agreed due of those bookings, never added into Balance — and the Payment Due banner says the same. The booking's own card reads "Awaiting finance approval — ₹X will be due" in amber, in the booking's currency, instead of Balance: ₹0. Pay Now (on the card, the banner and in the dialog) offers the agreed due, approved or not — what customer_submit_payment accepts. The printed account statement's summary carries the same ₹B more once finance approves line; its rows and closing balance are unchanged (FIN-033, owner 01/10/2026). Until 30/09/2026 Paid summed verified payments without netting refunds and Billed left out GST (FIN-033).

The partner's reports count revenue and commission on live bookings only: a cancelled, transferred or rejected booking earns no commission (its commission voucher is reversed).