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
fetchAgentsWithStatsfromsrc/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. SeePartners.tsx:204-214for the form shape and:293-316for 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), allpartners.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) needpartners.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
AgentDocumentas 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 withpartners.editupload on the partner's behalf (PTR-087): the file goes to the agency's folder on the company drive throughupload-partner-docwith 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:
- You owe, Credit limit, Credit left and Waiting for the office —
You oweisportal_agent_outstanding()(unpaid bookings with GST and open invoices), as in the app. - 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.
- 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.
- Waiting for the office — the claims recorded pending, with Awaiting verification.
- 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).
- 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_paymenttakes 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/ledgerexists 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_profileallows, saved throughPATCH /portals/agent/profileafter 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-contactsandDELETE /me/partner-contacts/:id(partner_contact_save/partner_contact_remove, own agency only — PTR-085). - Documents — the agency's
AgentDocumentrows fromGET /me/documents(theAgentDocument select agencyrow policy), each with Open — the file in a viewer on the page, throughdrive-file(ACC-074) — and an upload: a photo or a PDF under 6 MB throughupload-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)
- Turnstile captcha verification.
signIn(identifier, password, { audience: 'portal', captchaToken })viauseAuth, which calls theauth-loginfunction. 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).- The captcha is verified by the server; after a failed attempt the page issues a fresh one.
- 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.
- 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 ispending; staff approve it here on/agentsas before. - Documents — the partner files PAN, identity, proof of business (and the GST certificate,
address proof, cancelled cheque) from the phone:
upload-partner-docputs the photo on the company Google Drive underPartners / "<agency> (<id>)"andpartner_add_documentrecords theAgentDocumentrow 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). ThedocTypelist grew togst_certificate,address_proof,cancelled_cheque. Documents uploaded at web sign-up are uploaded by the new partner throughupload-partner-docright 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, throughdrive-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_outstandingagainst the credit limit, the documents and what is missing, and whether the login leads groups. - Bookings — the web wizard's
partner_create_bookingfrom 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_groupswith the status rule ofsrc/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"; since20260930160000the database stores it asmale/female, the only spellingBookingPassengeraccepts — 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 byrazorpay-webhook— API → Online payment), and I have paid:partner_submit_paymentrecords 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_LEADERand 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 callspartner_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_creditis 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, andpartner_create_bookingwithp_b2b_offer_id— the web "Book" button's call — which takes the seats off the offer, checks credit and starts the bookingPENDING_OPSin 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 (PartnerSeatSaleRLS). Sell seats — seats, markup, the buyer's name, phone, email — previews the selling price, total and margin, asks Yes/No and callspartner_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.
- Hotels —
- 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/agentsrow 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. Requiresfinance.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
Agentrow (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).
Related
- 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).