Skip to content

Partners

Written April 2026 — read this first

This page covers the staff-side partner admin. For what a partner can do and see — rate-sheet pricing they cannot override, an enforced credit limit, agency isolation, and the email-matching hole that was closed — read Portals and 11 · Partners. A partner has a credit limit, not a wallet.

External B2B agents / sub-agents. Two surfaces: the staff-side admin page at /agents that manages partner records and credit, and the partner-facing portal at /partner/** where agents run their own business through AlHuda.

Scope

  • Admin partner management — src/pages/partners/Partners.tsx (route /agents). Create partner, set commission/credit, manage login credentials, open ledger, allocate receipts.
  • Partner portal — src/pages/partner/ (route /partner/**). Agent-scoped dashboard, bookings, inventory, invoices, reports, profile, security.
  • Partner auth — src/pages/partner/PartnerAuth.tsx (route /partner/auth). Signup with KYC document upload, login, 2FA.

Two 'partner' directories, one concept

src/pages/partners/ (plural) holds the admin page where staff manage partners. src/pages/partner/ (singular) holds the portal pages that the partner themselves sees. Wire table: the admin page is called Agents internally (src/App.tsx:49) — "partner" is the user-facing label, "agent" is the DB/API term. Treat them as synonyms.

The partner code

Every business partner has a permanent partner code (BP-0012), given by the system (PTY-001). It shows on the partners list (and the list's search finds it), on the partner record, on the partner's own My profile and on the home and More screens of the app, so the partner can quote it. Partners put it in the Partner code column of the booking import (PTY-005). The partner's AGR- / AGP- ledgers keep their codes; their names carry the partner code (PTY-004). See Party codes.

Admin view — /agents

Approving a new partner. Every new partner starts as Pending approval — one who registered from the website or the app, and one the office adds on this page (PTR-001). When it is added, the database e-mails sales@alhudatravels.in and puts a notice in the inbox of everyone who can approve it (PTR-096).

Only a General Manager, a CEO or an Admin (IT_ADMIN or SUPER_ADMIN role) approves or rejects (partners.approve, PTR-095). Open the partner's profile (click the partner, or /partners/:id) → Documents to check what they uploaded and mark each OK or rejected, then Approve or Reject — from the partner's ⋯ menu on the list or the profile's action bar. Both ask for a reason. Approval makes the partner active — they can sign in, see the rate sheet and book on account within their credit limit — and tells them at once (inbox and push). Rejection marks the partner inactive and tells them too. Either way sales@ is e-mailed. A closed partner that was never approved shows Approve registration, not Reactivate, and needs the same permission. The status filter has a "Pending approval" option so nothing waits unseen.

Suspending, deactivating and lifting a suspension of an approved partner stay with partners.edit.

Source: src/pages/partners/Partners.tsx (export default function Agents, Partners.tsx:49). Route: /agents gated by agents.view (src/App.tsx:164).

Surfaces:

  • List + filter (search, status, city, relationship manager, tier) backed by fetchAgentsWithStats from src/services/agentService.ts. The list shows each partner's city, relationship manager and tier from the partner profile, and the search box also finds a city or a manager's name (PTR-090). Each filter has a "none set" choice, so partners without a manager or a tier are easy to find.
  • Cards or table (PTR-092) — a switch above the list; the choice is kept in this browser (cards when the browser keeps nothing). A card shows the initials, the agency, the partner code, the person, city, manager and tier, bookings, outstanding and status. The table shows the same with billed and partner-since.
  • Who is named — the primary contact, else the owner on the profile, else the contact person typed at sign-up. The sign-up placeholder "New Agent" and the internal id are never shown; with nobody known the row says No contact yet. The search box finds that name too.
  • Click a partner to open its profile. Every other action is in the partner's ⋯ menu.
  • KPI strip: total revenue, outstanding, collected (partnersMath.ts::computePartnerStats).
  • Create partner dialog. Fields include company (required), PAN (required, 10 chars), GSTIN (optional, 15-char validated via isValidGstin), contact, commission rate, credit limit. See Partners.tsx:204-214 for the form shape and :293-316 for the validation.
  • Optional "create login" toggle — creates a Supabase auth user in the same call with a generated or typed password; server returns the temp password (Partners.tsx:249-266).
  • The ⋯ menu: open the profile, Ledger, Edit, Access key (reset or create the login), Approve / Reject (pending), Deactivate / Reactivate, Delete — each with the gate and confirmation it had as a button. From the ledger: print statement, allocate an on-account receipt.
  • The list shows a Paused chip beside the status when the partner's login is paused (ACC-070).
  • Ledger dialog uses PartyLedgerDialog + fetchAgentLedger. From the ledger a user can allocate an on-account receipt to open booking items (Partners.tsx:152-178).
  • Deep-links: ?open=<agentId> opens that partner's ledger — used by the Settlement Report and the profile's Ledger; ?accessKey=<agentId> opens that partner's access-key dialog — used by the profile's More → Access key.

The partner record (/partners/:id)

One page per business partner (PTR-083), laid out as a company profile (PTR-091): src/pages/partners/PartnerRecord.tsx with PartnerProfileLayout.tsx, read in one call (GET /partners/:id/record → partner_record, agents.view). Every screen that names a partner links here: the Partners list, the booking page ("Partner record →"), Customer 360 ("Booked through a partner"). The page links back to the booking, group, ledger and audit row it mentions.

Header. A soft band in the brand colour; the agency's initials in a large circle; the name; the verified mark; the partner code (click to copy); the tier; the status; Login paused, No login yet, Over credit limit and N documents missing when they apply; city · district; "Partner since" month and year; the relationship manager, linked to their employee record. Verified means the partner is active and every required document is on file, accepted by the office and in date. Hover the mark to read why a partner is not verified. The mark is worked out from the record and decides nothing.

Action bar.

  • New booking — for an active partner, with bookings.create. The booking wizard opens with "Through business partner" and this partner chosen (/sales/bookings/new?agentId=<id>); the user can still change it.
  • Approve / Reject — a pending registration only (partners.edit, a reason, the partner is told) — PTR-001.
  • Call and WhatsApp — the primary contact, else the first contact with a number, else the agency's phone. A 10-digit number gets +91; the phone's own dialer and WhatsApp open; nothing is sent by the office (PTY-008).
  • Ledger — the partner's ledger on the Partners list (/agents?open=<id>).
  • More — Edit profile, Edit terms (the agency's fields with commission and credit limit; a change to either asks for a reason), Access key (the list's access-key dialog), Pause login / Resume login (ACC-070), Suspend, Lift suspension, Deactivate, Reactivate (each asks for a reason, PATCH /agents/:id, the partner is told), all partners.edit; Delete (agents.delete, type the agency's name, DELETE /agents/:id, then back to the list).

Numbers. Bookings, travellers, revenue this financial year, outstanding (red only when over the credit limit), credit left, average days to pay (PTR-089).

Tabs. The tab is in the address (?tab=); the old names (overview, money, activity, requests) still open the matching tab. At phone width the header stacks, the Intro moves under the numbers and the tabs scroll sideways.

  • Timeline (first) — everything on the agency and by its login as a feed of cards, newest first, at most 100: bookings (with their travellers and group), payments recorded and verified, documents filed and reviewed, notes, status, profile and contact changes, documents removed, requests, sign-ins, pauses, tour-leader appointments, and the audit rows on the agency or by its login (action, changed field names and actor — never the audit values, AUD-004). Each card has an icon, DD/MM/YYYY HH:mm, who, and a link to what it describes. On top, a composer (partners.edit): write a note, pick Call, Meeting, WhatsApp, Email, Visit or Other, an optional follow-up date, Post — the same append-only note as the Notes tab (PTR-088). Beside the feed on a wide screen: Intro (business type, owner, established, website, address, GSTIN, PAN), Contacts (role, Call, WhatsApp), Registrations (IATA, association, state tourism, Haj/Umrah licence with its expiry — expired or expiring within 60 days is marked), Documents (accepted of required, missing, not accepted yet) and Relationship (manager, region, tier, source, onboarded).
  • About — the profile (PTR-084) in three cards — Business (legal and trade name, business type, owner, year established, website, the split address — the old sign-up address shows until the split one is filled —, PAN and GSTIN), Registrations and Office relationship (relationship manager, tier, region, onboarded on, source and internal notes, never shown to the partner) — with Edit profile (partners.edit): a dialog that sends only the changed fields (PATCH /partners/:id/profile) after a Yes/No listing them; the relationship manager is picked from active staff. Then Contacts (PTR-085): name, role, phone, WhatsApp, email, the primary one starred, each with Call, WhatsApp and email links; Add contact, edit and Remove (a reason, the row is kept) need partners.edit. A contact is its own person: its phone is never copied to the partner record or the login. Then Agency and terms with Edit (partners.edit) — changing the phone there also changes the phone on the partner's login, and the dialog says so under the field (ACC-076) —, the status history from the audit trail with the reason typed at each change, and the groups the login leads (PTR-004).
  • Bookings — the performance numbers (PTR-089): bookings and travellers this financial year (from 1 April) against last, revenue (booking totals with GST), paid and due, the average days to pay, cancellations and their rate, and the last booking. Draft, cancelled and rejected bookings are left out of bookings and revenue; average days to pay runs from the booking to its last verified payment, over bookings paid in full. Then a count by status and the last ten bookings with customer, departure, total and balance, each linking to the booking, and the last ten requests from the agency's customers or on its bookings.
  • Payments & ledger — a credit gauge (outstanding against the credit limit; "No credit limit is set" without one), outstanding, credit left or "over the limit", claims awaiting verification, group-invoice counts and what is due, the last ten payments and claims with who recorded and who verified each, and Open the ledger (/agents?open=<id>).
  • Travellers — the travellers on the agency's live bookings: name, booking, group and departure, the booking's status; the latest 50 by departure, with the count of all. Read in the same one call.
  • Documents — every AgentDocument as a card with its type, who filed it and when, open (the file opens inside the ERP through the drive viewer), what is still missing of the required set, and the office's review: OK or Reject with a note (POST /partners/:id/documents/:docId/review → partner_document_review, partners.edit). The partner is told either way; a rejection note is written for them (PTR-080). Staff with partners.edit upload on the partner's behalf (PTR-087): the file goes to the agency's folder on the company drive through upload-partner-doc with the agency's id, with an optional valid until date; the card says it was filed by the office and shows the date (red once expired). Remove takes the record off with a reason; the file stays on the drive. Both are audited.
  • Notes (PTR-088) — the office's interaction log, newest first, with a filter by kind once there are notes of more than one kind: kind (call, meeting, WhatsApp, email, visit, other), what was said, an optional follow-up date, who and when. Add note needs partners.edit. A note is never changed or deleted — a correction is a new note — and the partner never sees notes. The follow-up date is shown on the note; it does not create a task or send a reminder (not built).
  • Access — the login as the employee record shows it: active or paused, authenticator, last sign-in and sign-ins in 30 days, failed sign-ins this week, roles; the sessions (device, browser, IP, open or ended); devices with push alerts; the last sign-ins.

Not built: bank details (account number, IFSC) are not kept on the partner profile — they are sensitive and out of scope. Reminders from a note's follow-up date are not built. A partner logo or cover picture is not built — the header shows the agency's initials, and there is no "logo" document type. The Edit profile dialog and the contacts are desktop and My profile only; the staff app shows them read-only.

Partner access keys and login

The "access key" is the partner's email/password used to log in at /partner/auth. Staff can (re)set it from the admin page. The API endpoint is POST /agents with createUser: true, password: ... for creation and a separate endpoint for reset (Partners.tsx:249-264).

Partner portal — /partner/**

Role-gated: every /partner/** route is wrapped in <ProtectedRoute allowedRoles={['agent']}> (src/App.tsx:189-196). Staff permission matrix does not apply — access inside the portal is enforced by ownership checks in API handlers (see docs/PERMISSIONS.md §4.5).

Page File Purpose
/partner PartnerPortal.tsx Dashboard — KPIs, recent bookings, customers, quotations, purchased seats, service requests. New Request can name a departure (optional) and has the type Package / departure enquiry — the entry that used to be "Request Package" on Flights (PTR-094)
/partner/bookings PartnerBookings.tsx Own bookings list. Opening a booking with a balance shows Pay online (rupee bookings) and I have paid — the same dialogs as the Payments page (PTR-093). A booking whose travellers were all moved to another departure shows Transferred → BK-… as its status and "Transferred" as its balance, and offers no Add Pax (LC-031); the booking they went to is the agency's too, with the money paid for them. A booking finance has not approved yet shows Awaiting finance approval — ₹X will be due in its balance column, not Bal: ₹0 (FIN-033)
/partner/bookings/new PartnerBookingWizard.tsx Partner-scoped booking wizard (always books on-account against the partner's credit)
/partner/invoices PartnerInvoices.tsx Create / edit / print own invoices
/partner/payments PartnerPayments.tsx Payments — see Payments on the web portal
/partner/inventory PartnerInventory.tsx Sell available B2B seats
/partner/flights PartnerFlights.tsx Flights only: the seats on offer to partners (GET /portals/agent/b2b-flights, PTR-060) with Book. Travel groups are not listed here; the page points to New booking for a Hajj/Umrah package and to Seat Inventory for seats already bought (PTR-094)
/partner/reports PartnerReports.tsx Own bookings / revenue / customers reports. Money before finance approval: see Awaiting finance approval on the dashboard and reports
/me (/partner/profile redirects here) src/pages/settings/MyProfile.tsx The agency profile, own documents, name and contact person — see My profile
/partner/auth PartnerAuth.tsx Login / signup / 2FA
Security panel PartnerSecurity.tsx Active sessions, 2FA enrolment

Portal data fetching is centralised in src/services/agentPortalService.ts (e.g. fetchAgentPortalData(userId) at PartnerPortal.tsx:50).

Awaiting finance approval on the dashboard and reports

A booking's balance counts its price only once finance approves the booking voucher; before that it is 0, but the agency still owes what it agreed (FIN-033, owner 01/10/2026). GET /portals/agent returns each booking's agreedDue and awaitingApproval (the computed fields booking_agreed_due / booking_awaiting_approval), and the screens use them:

  • A booking's own line — the bookings list's balance, the dashboard's Recent Bookings Balance column, and the reports' Bookings table — reads Awaiting finance approval — ₹X will be due in amber, in the booking's currency.
  • Totals — the dashboard's Outstanding Balance, the reports' Overview and Revenue Outstanding, and the Bookings table's totals row — stay the sum of balances. Under each, ₹B more once finance approves (N bookings) names what the awaiting bookings will add. The two are never added into one figure.
  • The Bookings report's printout shows the awaiting text in the Balance column; its CSV keeps the numeric Balance and adds a column, Awaiting finance approval (will be due).

Not changed: the Revenue breakdown table and the Customers table add up balances only, so a booking awaiting approval adds nothing to their Balance columns. Revenue and commission count the booked amount, as before.

Payments on the web portal

/partner/payments (sidebar Payments; PTR-093) is the web twin of the app's Money tab. It uses the same server functions; nothing on the page records money.

What the partner sees, in order:

  1. You owe, Credit limit, Credit left and Waiting for the office — You owe is portal_agent_outstanding() (unpaid bookings with GST and open invoices), as in the app.
  2. Bookings with a balance — booking, traveller, departure, total, paid, what is waiting and the balance. Each has Pay online (bookings in rupees only) and I have paid. An online payment started in the last hour and not yet confirmed says so. The balance here is the agreed due — price + GST − receipts − credit — so a booking finance has not approved yet is listed and payable, with Awaiting finance approval — ₹X will be due in amber (FIN-033, owner 01/10/2026). The booking detail and bookings list, the dashboard and reports (above), and the app's booking screen and bookings list say the same.
  3. Group invoices with a balance — issued invoices addressed to the agency, each with I have paid. An invoice is not paid online; Razorpay orders name one booking.
  4. Waiting for the office — the claims recorded pending, with Awaiting verification.
  5. Not accepted — a claim finance rejected in the last 90 days, marked Not accepted, with finance's reason and the day it was decided. Shown only when there is one. The agency is also e-mailed Payment claim not accepted with the reason (COMM-037).
  6. Recent verified payments — the latest 20, office- or Razorpay-recorded. Each verified payment on a booking is also e-mailed to the agency as a receipt (COMM-038).

I have paid asks for the booking or invoice, the amount (filled with what is still due after the claims already waiting), how it was paid (bank transfer, UPI, cheque, cash deposit, card) and the reference (required for everything but cash), then asks Yes/No (PTR-070). partner_submit_payment records it pending; finance verifies it against the bank (PTR-081, FIN-032). When the database refuses (more than is due, a cancelled booking, another agency's invoice), the dialog shows its sentence.

Pay online asks for the amount (up to the agreed due, FIN-033), asks Yes/No, starts an order through razorpay-order and opens Razorpay Checkout in the page. checkout.js is loaded only then, not with the portal. After Checkout reports success the dialog watches for up to a minute for razorpay-webhook's record and says Paid, or that Razorpay has the payment and it will show shortly. Until the owner sets the Razorpay keys the function answers with its "not switched on" sentence and the dialog shows it.

A registration still under review sees its balance but no payment buttons. A suspended partner may still pay what it owes.

Not built here, deliberately:

  • No receipt upload. The app has none either; the office matches the reference to the bank statement.
  • No payment date or note. partner_submit_payment takes neither; the claim is dated when it is reported. Adding them needs a change to the function (a migration).
  • No funds on account. A payment names one booking or one invoice; money on account is recorded by finance (agents.allocate_receipt).
  • No statement (ledger) on the web. GET /portals/agent/ledger exists and the app shows it; no web page reads it yet.

Routes: API → Partner payments.

My profile (/me)

Since 2026-09-25 the partner's own profile page is the shared My profile page (src/pages/settings/MyProfile.tsx, sidebar My Profile, also the Settings menu in the header); the old PartnerProfile.tsx page is gone and /partner/profile redirects to /me. It reads GET /me/profile → profile_me() (ACC-071) and shows:

  • Your login — name, email, username and phone, all read-only for a partner (the office changes them on /agents).
  • Your agency — company, status, GST number, PAN, address, credit limit, commission rate and when the agreement was accepted, read-only. Outstanding against the credit limit is not on this page; it is on the statement under Invoices.
  • Name and contact person — the two fields partner_update_profile allows, saved through PATCH /portals/agent/profile after a Yes/No.
  • Your agency also shows, read-only, what the office keeps on the profile: legal name, business type, IATA number, the Haj/Umrah licence with its expiry, and your relationship manager at Alhuda (name, phone, email). The office-only fields (tier, source, region, onboarded on, internal notes) are never shown (PTR-086).
  • Address and website — address line, city, district, state, PIN, country and website, the partner's to keep current: Save address sends only what changed (PATCH /me/partner-profile → partner_update_my_profile) after a Yes/No.
  • Contacts — the people at the agency, each with Call / WhatsApp / email links; Add contact, edit and remove (a reason is asked) through POST /me/partner-contacts and DELETE /me/partner-contacts/:id (partner_contact_save / partner_contact_remove, own agency only — PTR-085).
  • Documents — the agency's AgentDocument rows from GET /me/documents (the AgentDocument select agency row policy), each with Open — the file in a viewer on the page, through drive-file (ACC-074) — and an upload: a photo or a PDF under 6 MB through upload-partner-doc → partner_add_document (PTR-080).
  • Roles and access — AGENT, active / inactive, and Paused (read-only) when the office has paused the login (ACC-070); a save or an upload is then refused by the database and shown as a toast.
  • Request account deletion — the partner asks the office to delete their login; it is never erased on the partner's own action (AUD-025 … AUD-027). The request carries "Business partner — the office decides", and the partner reads "Your request has been sent. The office will review your account and contact you." Once the office approves, the agency's business record stays — trading name, company, PAN, GSTIN, business address, commission, credit limit, partner code, bookings, invoices, payments, the proof of business, the GST certificate and the signed agreement. The contact person, email and phone on the agency, the agency's contact people, the owner's name, and the identity and bank papers are erased, and the agency becomes inactive. The office (partners.edit) reviews the agency, settles anything open (a trip not finished, a balance either way, an unused advance, an unpaid invoice), and completes or declines the request within 30 days on Account deletions. Whether partners keep this (as "Request account closure") or lose it together with in-app partner sign-up is the owner's open choice.

Partner auth flow

Source: src/pages/partner/PartnerAuth.tsx.

Login (PartnerAuth.tsx:65-96)

  1. Turnstile captcha verification.
  2. signIn(identifier, password, { audience: 'portal', captchaToken }) via useAuth, which calls the auth-login function. An email or a phone number in any format works; the function resolves it server-side and never tells the browser which email a phone belongs to (ACC-062).
  3. The captcha is verified by the server; after a failed attempt the page issues a fresh one.
  4. If the partner has an authenticator app, the TOTP step follows; until it is passed the database grants the session nothing (ACC-064). See Signing in.
  5. On success, redirect to location.state?.from?.pathname || '/partner'.

Signup (PartnerAuth.tsx:36-53)

B2B signup collects:

  • Company name, contact name, email, phone, password.
  • PAN card number.
  • GSTIN (optional, validated with isValidGstin / normalizeGstin).
  • Address.
  • Three document uploads: PAN, Aadhaar, business proof.
  • Agreement checkbox.

The signup creates the Agent row in pending state; an admin must approve it before the partner can transact.

The partner portal in the app

The native app is the partner portal on a phone, through the same functions as the web portal plus three added for it (20260929100000_the_partner_in_the_app.sql):

  • Sign up in the app — the website's partner sign-up, email confirmed first (partner-signup; PTR-082). The account is pending; staff approve it here on /agents as before.
  • Documents — the partner files PAN, identity, proof of business (and the GST certificate, address proof, cancelled cheque) from the phone: upload-partner-doc puts the photo on the company Google Drive under Partners / "<agency> (<id>)" and partner_add_document records the AgentDocument row as the partner (PTR-080); a PDF under 6 MB from the phone's files works too. Each file shows the office's check (accepted, waiting, or rejected with the note), and a kind whose every file was rejected counts as missing again (partner_my_account, 20260930160000). The docType list grew to gst_certificate, address_proof, cancelled_cheque. Documents uploaded at web sign-up are uploaded by the new partner through upload-partner-doc right after the account is made (or from My profile after confirming the email). The desktop lists them on the partner record (/partners/:id → Documents) with Open (the file in a viewer on the page, through drive-file, ACC-074) and the office's review — OK, or rejected with a note the partner reads (PTR-080); the office checks them there, or on the staff app's Partners screen, before approving.
  • Home — partner_my_account: the agency, portal_agent_outstanding against the credit limit, the documents and what is missing, and whether the login leads groups.
  • Bookings — the web wizard's partner_create_booking from the phone (rate-sheet price, credit check, seats, PENDING_OPS), plus the emergency contact and tour-leader consent. The departure list is the web wizard's: public_groups with the status rule of src/lib/groupStatus.ts (planning, open or active), each with the price per category and the seats left; a departure with no published rate sheet, leaving today or full is listed with the reason and cannot be booked (PTR-020). Only an active partner reaches the wizard; a pending one is told the account is waiting for approval and what is still needed (PTR-002). Both wizards send the traveller's gender as "Male" / "Female"; since 20260930160000 the database stores it as male / female, the only spelling BookingPassenger accepts — before that every partner booking with a gender, web or app, was refused by the table's check constraint.
  • Money — the statement from the same rows as GET /portals/agent/ledger, the group invoices (the web portal has the same payments on its Payments page — Payments on the web portal), Pay online (Razorpay on one of the agency's bookings in rupees: razorpay-order, the receipt recorded verified by razorpay-webhook — API → Online payment), and I have paid: partner_submit_payment records a pending payment on one booking or one issued invoice; finance verifies it like a customer's claim (PTR-081). The Waiting tab lists the claims pending and, under Not accepted, the claims finance rejected in the last 90 days with the reason (COMM-037). Funds on account without a target are still recorded by finance (agents.allocate_receipt).
  • A partner who is also a tour leader — staff give the login TOUR_LEADER and assign it to a departure; it stays a partner and marks attendance for that group only (PTR-004).
  • Offers and seat sales (Home and More → Offers and seat sales; apps/mobile/src/lib/partnerOffers.ts) — the web portal's Hotels, Flights and Seat Inventory pages on one screen, through the same functions:
    • Hotels — partner_hotel_offers (open offers for an active partner, the agency's own reservations for any status; no contract cost — PTR-060). Reserve asks for the beds and a note, shows beds × price and the account's outstanding against the credit limit, asks Yes/No (PTR-070) and calls partner_reserve_beds, which takes the beds off the offer's one counter in one statement for this agency only (PTR-040). It posts nothing: the office bills the beds. The function does not run the credit check (portal_assert_credit is called by the booking functions only); the sheet says so.
    • Seats — Seats on offer: public_b2b_offers (active partners; no block cost, PNR or other buyers). Buy takes the seat count, shows the total and the credit preview (PTR-030), then opens the booking wizard with the offer (/partner-booking/new?offerId=): every traveller at the published seat price, infants free, and partner_create_booking with p_b2b_offer_id — the web "Book" button's call — which takes the seats off the offer, checks credit and starts the booking PENDING_OPS in one transaction (PTR-020, PTR-031). My seat blocks: partner_seat_inventory (the blocks the agency bought, sold and remaining from the one counter) with the agency's sales under each (PartnerSeatSale RLS). Sell seats — seats, markup, the buyer's name, phone, email — previews the selling price, total and margin, asks Yes/No and calls partner_sell_seats (PTR-041).
    • My holds — the beds reserved and the seat blocks held, each with a countdown to the check-in or the departure and what it is worth at the partner price. No hold expires and none is released from the app: PTR-042 is open, no partner hold carries an expiry, and no partner function releases beds or seats (the offer tables are staff-only writes; the inventory-hold functions need inventory.holds.manage) — the web portal has no release either. The screen says so and gives the office number.
  • A refused booking shows the database's own words (Credit limit exceeded: …, Not enough seats left …, a passport already registered) with the next step, and the wizard returns to the step that needs changing.

Not in the app: partner invoices to customers, reports, and releasing a hold — the first two are on the web portal; the last is the office's. A group invoice on a departure the agency has no booking on shows as "Group" without its name: the partner may read the invoice but not that departure's row (auth_portal_group_ids() counts bookings and quotations only).

Every read the app's partner screens make is run as an active and as a pending AGENT login by supabase/tests/the_partner_app_works.sql.

Families on a partner's booking

On the partner portal's booking view, and after a booking is made, a partner answers Who is family here? and names a guardian for each child or infant — for their own travellers only (PAX-036, PAX-021). The database refuses anything touching another partner's or the office's travellers and does not show their names. See Bookings — Who is family here and, for the app, Native app.

Partners in finance

Partners appear in finance as the counterparty on agent payables / receivables. Key references:

  • Agent ledger — fetchAgentLedger(agentId) → src/services/ledgerService.ts. Lists every debit/credit with running balance. Opened from /agents row action or auto via ?open=<id>.
  • On-account receipt allocation — POST /finance/vouchers/agent-receipt-allocate (Partners.tsx:163-170). Allocates an unapplied receipt to one or more open booking items. Requires finance.payments.record.
  • Settlement report — Finance module report surfaces per-partner outstanding and deep-links into the partner ledger.
  • Credit limit — per-agent cap stored on the Agent row (0 or blank = no limit set). The database refuses any booking write that would take the partner over it — the partner's own bookings and added travellers, and staff bookings, price changes, added travellers, seat-offer bookings and transfers into the partner's bookings alike (PTR-030). The refusal reads "This would take agency over its credit limit of ₹X (owed ₹Y). Ask a finance manager to override with a reason."
  • Overriding the credit limit — when a save in the booking wizard (new booking or edit) is refused for the credit limit, a user with partners.credit.override (Finance Manager, CEO, GM, Super Admin) sees Override and save: they type a reason (at least 3 characters) and the booking is saved again with it. The database checks the permission and the reason and records them in the audit trail (partner_credit_override, with the limit and what was owed before and after); the reason is not kept on the booking. Anyone else sees the refusal and asks a finance manager. A partner cannot override. The phone app's New booking shows the same refusal and has no override: an override is made on the website. GST stamped on the booking after it is saved counts towards what is owed but does not itself trigger the check.
  • Commission rate — per-agent percentage; used in the group pricing / settlement logic.

Agent receivable GL mapping is configured in FinanceConfig (see Finance module docs).

Permissions

Canonical reference: docs/PERMISSIONS.md §4.1 (agents.*) and §6.9.

Action Permission
View /agents agents.view
Add partner agents.create
Edit partner profile agents.edit
Set / reset access key agents.edit (also agents.access_key.manage in granular catalog — not yet wired)
Deactivate / reactivate agents.edit (also agents.deactivate granular)
Delete partner agents.delete
View agent ledger agents.view (also agents.ledger.view granular)
Allocate on-account receipt finance.payments.record (also agents.allocate_receipt / finance.allocations.agent_receipt granular)
Record an on-account receipt from the partner (Receipt on the partner's ledger; POST /finance/vouchers/agent-on-account-receipt) finance.create
Print Statement / Export Excel (the partner's ledger) no check of its own — anyone who can open the partner's ledger
Set or change the commission rate (Add Business Partner, Edit, Edit terms) agents.commission.set — finance and leadership only (PTR-030). The database refuses a non-zero commission on a new partner, or a changed one, from anyone else (Agent_terms_guard). On the Partners list's Add and Edit dialogs and the partner page's Edit terms dialog the field is disabled with a note
Set or change the credit limit agents.credit_limit.set — the same rule, dialogs and guard
Book over the credit limit, with a reason (PTR-030) partners.credit.override — checked by the database
Portal access (/partner/**) role = agent; no matrix permission
Report a payment / pay online (/partner/payments, booking detail) role = agent; no matrix permission — partner_submit_payment() and online_payment_actor() check the agency owns the booking or invoice

Granular agent permissions are seeded but not yet wired

The granular agents.deactivate, agents.access_key.manage, agents.ledger.view, agents.ledger.export, agents.allocate_receipt permissions exist in the catalog (PERMISSIONS.md §4.4) and are seeded to the right roles, but code still checks the coarse agents.edit / agents.view today. agents.credit_limit.set and agents.commission.set are wired (PTR-030, above). See docs/PERMISSIONS.md §8 drift items 6 and 10.

The legacy partners.create / partners.edit / partners.delete permissions are kept as aliases for RLS backwards compatibility (see PERMISSIONS.md §4.1).

  • Customers — partners create customers via the portal wizard using CustomerFormDialog.
  • Suppliers — distinct from partners despite a similar shape.
  • Finance module — agent receivable ledger, commission settlement, on-account allocation.
  • PERMISSIONS.md §6.9, §6.11 — authoritative action matrix (admin view + partner portal).