11 · Partners (B2B agents)
How business partners join, what they can see, how they are priced and how much they can owe. Partners are one of the three kinds of user (ACC-010); they never hold staff permissions.
Why this file exists (findings, 2026-09-17 partner portal review)
- Partner sign-up inserted the
Agentrow with staff-only table rights, so it failed or depended on open policies; documents were stored with public links. - A partner login was matched to a partner record by email when no
userIdlink existed — anyone signing up with a partner's address could take over that agency. - The partner booking wizard sent a price typed by the partner; the database stored whatever arrived. Credit limit was shown but never enforced.
- Partners could read every booking, customer (with passport, Aadhaar, PAN), airline block (with PNR and cost) and other partners' B2B purchases.
- Seat resale used two counters (
seatsAvailableand a sum of sales) that disagreed; bed reservations read-then-wrote and could oversell. - Partners could set their own invoices to PAID and edit issued invoices.
Onboarding and status
PTR-001 · Every new partner starts pending
Status: DECIDED 2026-10-02 (owner: "a new business partner has been added and is subject to approval") · Owner: B2B Manager
A partner registers with company name, PAN, optional GSTIN, address and agreement acceptance. Identity documents are stored as private files (AUD-021). A login that already holds a staff role cannot register as a partner. Every new partner is pending, whether it registered itself or the office added it, until it is approved (PTR-095).
Enforced by: partner_register (20260918100000_data_isolation_portals.sql); the Agent.status default pending and the trigger Agent_approval_guard, which forces a partner added from the browser to pending (20261008120000); partner_staff_create, which adds a partner from the staff app as pending (PTR-097, 20261008230000). Tests supabase/tests/data_isolation.sql, supabase/tests/new_partners_and_suppliers_wait_for_approval.sql. The approver checks the documents under Profile → Documents first; the partner's login is told at once through the notification inbox and push (src/lib/api.partnerApproval.test.ts).
PTR-002 · Only active partners transact
Status: PROPOSED · Owner: B2B Manager
Partners with status other than active (pending, suspended, inactive) can sign in and read their own records but cannot create bookings, add passengers, reserve beds, sell seats, issue invoices or raise requests. Staff approve a partner by setting status active.
Enforced by: portal_require_active_agent() in every partner action function.
PTR-003 · A login belongs to one partner, linked explicitly
Status: PROPOSED · Owner: IT
A partner login is linked to its agency by Agent.userId only — never by matching email. Staff link existing agencies to logins explicitly.
Enforced by: auth_agent_id(); api.ts resolveAgentForCurrentUser has no email fallback.
PTR-004 · A partner's login may also lead a departure
Status: DECIDED 2026-09-25 (owner) · Owner: Operations · Source: FLD-001
Staff may give a partner's login the TOUR_LEADER role and name it as a departure's leader (TravelGroup.leadUserId). That login stays a partner: it signs in at the portal door, reads only its own agency, and holds only field.view and field.checkin beyond that — for the groups assigned to it, where it sees the manifest and records who is there (FLD-001). A login that holds a portal role (AGENT or CUSTOMER) is never staff, whatever else it holds; no blanket staff policy opens to it.
Enforced by: auth_is_staff() and auth_identifier_resolve() (20260929100000_the_partner_in_the_app.sql: staff = a staff role and no portal role); isPortalOnlyUser in supabase/functions/_shared/auth.ts (a portal role wins); fld_my_groups(), fld_group_manifest(), fld_record_checkin() (permission + leadUserId). Test: supabase/tests/a_partner_who_leads_a_group.sql.
Isolation
PTR-010 · A partner sees only their own agency
Status: PROPOSED · Owner: IT · Source: ACC-002, ACC-010
A partner reads only: their agency record, customers and bookings of their agency (with those bookings' passengers, payments, invoices, tickets and visa cases), their seat purchases and resales, their partner invoices and documents, hotel beds they reserved, and their own statement entries. Customer lists show passport numbers masked to the last 4 characters (AUD-020).
Enforced by: RLS agency policies; api.ts masks lists.
PTR-011 · No direct writes
Status: PROPOSED · Owner: IT
Partners never write tables directly. Every partner action runs as a database function that takes the partner from the session.
Enforced by: staff-only write policies; is_staff_user() excludes AGENT.
Pricing and bookings
PTR-020 · Price comes from the rate sheet
Status: PROPOSED · Owner: Sales · Source: PRC-001, PAX-030
A partner booking on a group is priced per passenger from the group rate sheet (four service lines plus custom lines) for that passenger's category. For a B2B flight offer the published seat price applies (infants 0). Any price, rate or category sent by the partner is ignored. A group without a published rate sheet cannot be booked by partners.
Enforced by: partner_create_booking, partner_add_passengers, portal_group_rate. The departures offered are public_groups() with the web wizard's status rule (src/lib/groupStatus.ts; the app's groupsForSale in apps/mobile/src/lib/partner.ts); one without a rate sheet is listed with the reason and cannot be booked. The booking is made in the rate sheet's currency (pricing.currency, which public_groups() returns), so the app prints every price in that currency (money() in apps/mobile/src/lib/format.ts) and says Prices in SAR when it is not rupees. Test: supabase/tests/the_partner_app_works.sql; apps/mobile/src/lib/partner.test.ts.
PTR-021 · Category from date of birth on departure
Status: PROPOSED · Owner: Operations · Source: PAX-002/003 (OPEN — proposed cut-offs used)
Date of birth and passport number are required for every partner passenger. Category is calculated on the group departure date (flight offer: flight date): under 2 infant, 2–11 child, 12 and over adult. Infants do not take a seat.
Enforced by: portal_pax_category. Update when PAX-002/003 are decided. The traveller's gender is stored male / female whatever case the wizard sends (portal_insert_partner_passengers, 20260930160000_the_partner_app_works.sql).
PTR-022 · Partner bookings start with operations
Status: PROPOSED · Owner: Operations · Source: LC-002
A partner booking is created in PENDING_OPS, payment policy on account, with the partner login recorded as creator. Because it skips DRAFT, it passes the same readiness check a staff booking passes when it is submitted: a name, date of birth and passport for every traveller (PAX-004), an adult with any child or infant (PAX-020), at most one infant per adult (PAX-011). It is refused with the submit check's own words — "Booking is not ready to submit: …" — and nothing is kept. Passengers can be added only while the booking is DRAFT, PENDING_OPS or NEEDS_CORRECTION (PRC-003). A second identical submission within two minutes returns the first booking.
Enforced by: partner_create_booking (readiness through bl_booking_problems since 20261008140000_booking_screens.sql), partner_add_passengers. Adding passengers later does not re-run the readiness check; operations re-run it when they approve (LC-002). Test: supabase/tests/booking_screens.sql.
PTR-023 · A partner sends its own booking back for approval
Status: DECIDED 2026-10-02 (owner, through Hamid: "yes, sent-back only … the price can't change on re-send") · Owner: Operations · Source: bookings audit, 02/10/2026
When operations or finance send a partner's booking back for correction (NEEDS_CORRECTION), the partner sees Sent back for correction on its bookings list and can Send for approval again with a note saying what was corrected. The booking goes back to the team that sent it back, after the readiness and capacity checks a staff resubmission makes. Only the partner that owns the booking can do this, and only while its account is active and not paused (ACC-070). A booking finance rejected is not resubmitted by the partner — it raises a request and the office puts it right (LC-005). The price cannot change on a re-send: the booking's total with GST is kept when the office sends it back (Booking."sentBackPrice"), and a re-send at any other price — for example after adding a traveller — is refused; the office corrects the price.
Enforced by: partner_resubmit_booking() (ownership under a lock, the price kept at send-back — trigger "Booking_keep_sent_back_price" — then resubmit_booking), and resubmit_booking() accepts the owning partner for a NEEDS_CORRECTION booking in place of bookings.edit (bl_partner_may_resubmit, 20261008140000_booking_screens.sql). Route: POST /portals/agent/bookings/:id/resubmit. The partner app (apps/mobile) calls the same function from the booking screen (Send for approval, resubmitPartnerBooking in apps/mobile/src/lib/partner.ts). Tests: supabase/tests/booking_screens.sql, src/pages/partner/PartnerResubmit.test.tsx, apps/mobile/src/lib/partner.test.ts.
Credit
PTR-030 · Credit limit
Status: DECIDED 2026-10-01 (owner) · Owner: Finance A partner's outstanding balance (unpaid booking totals including GST on non-cancelled bookings, plus open issued group invoices) plus the new booking or added passengers may not exceed their credit limit. A limit of 0 or blank means no limit has been set (current behaviour).
The limit binds staff as well as the partner: a staff booking for the partner, a raised price or added travellers on an edit, a seat-offer booking, a transfer into a partner booking — every write that raises a partner booking's total, or moves a priced booking onto a partner. The refusal reads "This would take agency over its credit limit of ₹X (owed ₹Y). Ask a finance manager to override with a reason."
Override: someone holding partners.credit.override (Finance Manager, CEO, GM, Super Admin) may go over the limit with a written reason of at least 3 characters. The reason is sent with the booking write, checked by the database and recorded in the audit trail (partner_credit_override: who, the limit, what was owed before and after, the reason). It is not stored on the booking. A partner cannot override.
Not checked: GST stamped on a booking after it exists (it counts in what is owed, but stamping it does not trigger the check — refusing it would leave a booking without its revenue voucher), and database sessions with no request behind them (migrations, maintenance, scheduled jobs).
Who sets the terms (amended 2026-10-03, owner: "a partner's credit limit and commission are set by finance and leadership only"). Only someone holding agents.credit_limit.set sets or changes a partner's credit limit, and only someone holding agents.commission.set its commission rate: Finance Manager, CEO, GM, Super Admin. This holds when a partner is added (the desktop's Add Business Partner, the staff app's New partner — a non-zero value is refused) and when one is edited (the desktop's Edit, Edit terms — a changed value is refused). The refusal reads "Only finance or leadership can set a partner's credit limit (agents.credit_limit.set)" (or "commission"). The forms disable or hide the two fields for anyone else.
Still open: should 0 mean "no credit — prepay only"? Until decided, 0 means no limit.
Enforced by: portal_assert_credit on the partner's own functions (row lock on the agency), and the trigger "Booking_credit_limit" (bkg_credit_limit_guard, 20261006120000) on every write to "Booking"; create_booking takes the reason as creditOverrideReason. Test supabase/tests/finance_guards_in_the_database.sql. Who sets the terms: the trigger "Agent_terms_guard" on every browser insert or update of "Agent" (POST /agents, PATCH /agents/:id) and the same check in partner_staff_create(); the grants and bundles in 20261008230000. Test supabase/tests/staff_add_partner.sql.
PTR-031 · Capacity is reserved in the same transaction
Status: PROPOSED · Owner: Operations · Source: INV-002
Seats (adults and children) are reserved against group capacity or the flight offer's available seats in the same transaction that creates the booking; if anything fails nothing is kept.
Enforced by: partner_create_booking, partner_add_passengers.
Holds and resale
PTR-040 · Bed reservations are atomic
Status: PROPOSED · Owner: B2B Manager
Reserving beds from a hotel offer decrements the offer's available beds in one statement; a partner cannot reserve more than remain, or beds from an offer reserved by another partner.
Enforced by: partner_reserve_beds. Called by the web portal's Hotels page and the app's Offers → Hotels → Reserve (apps/mobile/src/lib/partnerOffers.ts); neither writes the table. Reserving does not run the credit check (PTR-030 is on bookings) and posts nothing — the office bills the beds.
PTR-041 · One seat counter for resale
Status: PROPOSED · Owner: B2B Manager
Seats a partner bought are resold against one counter, the offer's available seats, under a row lock. Sales recorded before this rule still count.
Enforced by: partner_sell_seats, partner_seat_inventory. Called by the web portal's Seat Inventory page and the app's Offers → Seats → Sell seats (apps/mobile/src/lib/partnerOffers.ts).
PTR-042 · Hold expiry
Status: OPEN · Owner: B2B Manager · Source: AIR §106
Decide how long partner bed reservations and unconfirmed bookings are held before they are released automatically, and who is alerted.
Until decided: nothing expires and nothing is released by a partner — no partner function releases beds or seats (the offer tables are staff-only writes; the inventory-hold functions need inventory.holds.manage), so the office releases by hand. The app's My holds says so and counts to the check-in or departure instead (holdCountdown in apps/mobile/src/lib/partnerOffers.ts).
Documents and requests
PTR-050 · Partner invoices
Status: PROPOSED · Owner: Finance
Partner-branded invoices to their own customers are numbered atomically, totals are calculated from the line items, status is DRAFT or ISSUED only (PAID comes from recorded payments), and an issued invoice cannot be edited.
Enforced by: partner_upsert_invoice.
PTR-051 · Requests
Status: PROPOSED · Owner: Operations
Partner requests reference one of the partner's own customers or bookings.
Enforced by: partner_create_request.
PTR-060 · Offer lists hide supplier data
Status: PROPOSED · Owner: B2B Manager
Offer lists shown to partners never include airline block cost, PNR, internal notes, hotel contract cost or other buyers.
Enforced by: public_b2b_offers, partner_hotel_offers, partner_seat_inventory — the only reads the web portal and the app's Offers screen use for offers.
Documents, payments and the app
PTR-080 · Registration documents on the company drive
Status: DECIDED 2026-09-25 (owner) · Owner: B2B Manager · Source: PTR-001, AUD-021
A partner files their own registration documents, before approval and after: PAN card, an identity document (Aadhaar, passport or voter card of the owner) and proof of business (trade licence, registration or incorporation) are required; the GST certificate is required when a GSTIN was given; address proof and a cancelled cheque are asked for. A document is a file on the company Google Drive under Partners / "<agency> (<id>)", never a link the partner typed, never public; a partner reads only their own agency's documents; at most 20 per agency. Approval stays with staff — on the desktop partner record or the staff app — and the app shows "pending approval" and what is still missing until Agent.status is active (PTR-002).
The review (amended 2026-09-25). The office checks each document and marks it OK or rejected with a note (partners.edit). The decision, who made it and when are kept on the document (AgentDocument.reviewedAt / reviewedBy / reviewDecision / reviewNote), audited, and the partner is told: an accepted document by title, a rejected one with the note — the note is written for the partner and asks them to upload it again (REQ-002). A rejection without a note is refused. Amended 2026-09-28: staff with partners.edit may also file a document on the partner's behalf and remove one with a reason (PTR-087); the partner still files their own.
Enforced by: partner_add_document() and partner_required_document_types() (20260929100000_the_partner_in_the_app.sql); partner_my_account() returns each file's review and counts a kind whose every file was rejected as missing (20260930160000_the_partner_app_works.sql); partner_document_review() (20260929230000_a_record_links_everything.sql); the upload-partner-doc function (Drive folder, then the database function as the partner); RLS AgentDocument select agency. Tests: supabase/tests/the_partner_in_the_app.sql, supabase/tests/a_record_links_everything.sql.
PTR-081 · A partner reports a payment; finance verifies it
Status: DECIDED 2026-09-25 (owner) · Owner: Finance · Source: FIN-032, FIN-042
A partner may report money they have paid — bank transfer, UPI, cheque, cash or card — against one of their own bookings or one issued group invoice addressed to their agency. It is recorded as a pending payment on that booking or invoice, never verified, never posted; finance verifies it against the bank statement as it does a customer's claim, and only then does it reduce the outstanding balance. Anything but cash carries its reference. The amount may not exceed what is outstanding on the target after the claims already waiting; a cancelled or refused booking, a draft, paid or cancelled invoice, and another agency's booking or invoice are refused. The same claim sent twice within two minutes is recorded once. "Funds on account" without a booking or invoice are not accepted from the partner — an on-account receipt is recorded by finance (agents.allocate_receipt).
Enforced by: partner_submit_payment() (20260929100000_the_partner_in_the_app.sql). Test: supabase/tests/the_partner_in_the_app.sql.
A partner may also pay online (Razorpay) against one of their own bookings priced in rupees, up to its balance — never an invoice or funds on account: the app's razorpay-order checks Booking.agentId = auth_agent_id() (online_payment_actor()), creates the order, and the verified receipt is recorded by razorpay-webhook on Razorpay's signed event, not by the partner (FIN-032; 20260929130000, test supabase/tests/an_online_payment_starts_with_an_order.sql).
Amended 2026-09-29: both actions are on the web partner portal too, through the same functions (PTR-093).
PTR-082 · Registering from the app
Status: DECIDED 2026-09-25 (owner) · Owner: B2B Manager · Source: PTR-001, ACC-051
A partner may register from the app with the same details as the website (agency, contact person, phone, email, password, PAN, optional GSTIN, address, agreement). The app confirms the email first: the partner-signup function creates the login unconfirmed, writes the rows partner_register() writes (the User row, the AGENT role, a pending Agent row with the agreement accepted) and sends the confirmation link itself; the partner then signs in, files the documents (PTR-080) and waits for approval (PTR-002). One message for every outcome; throttled like sign-in.
Enforced by: supabase/functions/partner-signup (validation as partner_register(): PAN, GSTIN, agreement).
PTR-083 · One record per partner
Status: DECIDED 2026-09-25 (owner: "there is no comprehensive profile system … where we can see their documents uploaded, or activity … and it should be all linked together") · Owner: B2B Manager · Source: ACC-071, PTR-080, AUD-004
Every business partner has one record the office reads in one place, and everything that names the partner links to it: the agency and its status with the actions the office takes (approve or reject a registration, suspend, lift a suspension, deactivate, reactivate — each with a reason, the partner told what changed and never why; pause or resume the login, ACC-070), the linked login and its access (active or paused, authenticator, last sign-in), the status history from the audit trail with the reason typed at each change, the registration documents with the office's review (PTR-080), the bookings (a count by status and the latest), the money (outstanding against the credit limit, the latest payments and claims, invoices), the requests, the groups the login leads (PTR-004), and one activity timeline — status changes, bookings, payments recorded and verified, documents filed and reviewed, requests, sign-ins, pauses, tour-leader appointments and the audit rows on the agency or by its login — newest first, each row linking to the booking, group or record it describes. Reading the record needs agents.view; acting on it needs partners.edit (admin.users.edit also pauses). The record shows the action, the changed field names and the actor of an audit row, never its old and new values (AUD-004). No secret (a password hash, a finance PIN, a reset token) is ever part of it.
Enforced by: partner_record(), partner_set_status(), partner_document_review() (20260929230000_a_record_links_everything.sql); the desktop page /partners/:id (GET /partners/:id/record, POST /partners/:id/documents/:docId/review); the app's admin/partners and admin/partners/[id] screens. Test: supabase/tests/a_record_links_everything.sql.
Amended 2026-09-28 (PTR-084 … PTR-089). The same one read also carries the partner's profile (PTR-084), contacts (PTR-085), the office's notes (PTR-088), the performance numbers (PTR-089) and the login's access as the employee record shows it — sessions (device, browser, IP), devices with push alerts and the last sign-ins, with failed sign-ins this week. Profile and contact changes, document removals and notes join the activity timeline. partner_record() as redefined in 20261001234000_a_partner_profile.sql; test supabase/tests/partner_profile.sql.
Amended 2026-09-28 (PTR-091). The record is laid out as a profile page (Timeline first) and its one read also carries the partner's travellers.
The partner profile
Source for PTR-084 … PTR-090: the owner, 2026-09-28 — "employees have now profile system, but how about business partner profiles?", then "Create business partner profile system now"; earlier, "a comprehensive profile system … so that we can see the profiles of everyone in detail … where we can see their documents uploaded, or activity … and it should be all linked together".
PTR-084 · A partner has a business profile
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-083, ACC-071
Beside the agency's own fields (company, contact person, email, phone, PAN, GSTIN, commission, credit limit — unchanged, edited under Edit on the Agency card), each partner has a profile in four parts, every field optional:
- Business — legal name, trade name, business type (proprietorship, partnership, LLP, private limited, other), owner or principal, year established, website.
- Address — line, city, district, state, PIN, country. The old free-text address typed at sign-up stays readable and is shown until the split address is filled; nothing is copied or overwritten.
- Registrations — IATA number, TAAI or other association membership, state tourism registration, Haj/Umrah registration or licence number with its expiry date. An expired licence is shown as expired; nothing is blocked by it.
- Office relationship — the relationship manager (an active staff login — never a partner, customer or deactivated login — linked to their employee record), region or territory, tier (a short free-text label; Gold / Silver / Standard are suggested), onboarded on, source (how they came to us) and internal notes.
The office changes the profile with partners.edit, sending only the fields that changed; each change is one audit row on the agency with the field names and the old and new values (the record shows the field names only — AUD-004). Staff read the whole profile with agents.view.
PAN. A partner's PAN is a business identifier and is shown in full to staff who hold agents.view and to the partner; the last-four rule of AUD-020 is for a person's own identity numbers (an employee's PAN and Aadhaar), not an agency's.
Not built: bank details (account number, IFSC) — sensitive and out of scope; they are not kept on the profile.
Enforced by: PartnerProfile (row security: read with agents.view only; no write policy), partner_update_profile_admin() (allow-list, checks each field and the relationship manager, audits) in 20261001234000_a_partner_profile.sql. Test: supabase/tests/partner_profile.sql.
PTR-085 · Several contacts per partner
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-084, PTY-008
A partner has any number of contacts (at most 20 at a time): name, role (Owner, Accounts, Operations, Reservations, Other), phone, WhatsApp number, email, and one primary contact (the first contact is primary; marking another moves it). A contact needs a name and at least one way to reach them. Removing a contact needs a reason and is soft: the row keeps who removed it, when and why, and leaves the list. Every add, change and removal is audited on the agency. Wherever a contact's number is shown — the desktop record, the partner's My profile and the staff app — it offers Call and WhatsApp (PTY-008); nothing is sent by the office.
Enforced by: PartnerContact (read with agents.view, or by the partner for its own agency; no write policy), partner_contact_save(), partner_contact_remove(). Test: supabase/tests/partner_profile.sql.
PTR-086 · What a partner changes itself
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-011, ACC-071
From My profile (/me and the app), a partner changes its own address (line, city, district, state, PIN, country), website and contacts, besides the trading name and contact person partner_update_profile already allowed. It never changes the business, registration or office-relationship fields, never another agency's anything, and never sees the office-only fields (tier, source, region, onboarded on, internal notes). A paused login (ACC-070) and a closed agency are refused. The change is audited like the office's.
Enforced by: partner_update_my_profile() (allow-list, agency from auth_agent_id()), partner_contact_save() / partner_contact_remove() (a partner login may name only its own agency), profile_of() (a partner's view of the profile has no office-only field). Test: supabase/tests/partner_profile.sql.
PTR-087 · The office files and removes a partner's documents
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-080, ACC-074
A member of staff with partners.edit files a partner's document from the record: the file goes to the same folder on the company Shared Drive (Partners / "<agency> (<id>)"), the AgentDocument row records the member of staff as the one who filed it, and an optional valid until date is kept (a licence, an agreement). A partner login can never file for another agency through this path. Staff remove a document record with a reason; the file stays on the drive. Both are audited; the record marks a document filed by the office and one that has expired.
Enforced by: upload-partner-doc (an agentId is accepted only from a staff login holding partners.edit), partner_add_document_for(), partner_remove_document(). Test: supabase/tests/partner_profile.sql; scripts/deno-check-functions.sh upload-partner-doc.
PTR-088 · The office keeps notes on a partner
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-083
Staff with partners.edit log a dated note on a partner: its kind (call, meeting, WhatsApp, email, visit, other), what was said or agreed (up to 4,000 characters), and an optional follow-up date (today or later). Notes are append-only: nobody changes or deletes one, whatever their rights — a correction is a new note. Notes are read with agents.view, shown on the record's Notes tab and in its activity timeline, and never shown to the partner. A follow-up date is shown on the note; it does not create a task or a reminder (not built).
Enforced by: PartnerNote (read with agents.view; no write policy; a trigger refuses every update and delete except the cascade of a hard-deleted agency), partner_note_add(). Test: supabase/tests/partner_profile.sql.
PTR-089 · A partner's performance on the record
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-083, PRF-010
The record shows, read-only and in the same one read: bookings and travellers this financial year (from 1 April) and last financial year, the revenue of each (booking totals with GST), what was paid and what is due, the average days to pay, cancellations (count and rate) and the date of the last booking. Bookings, travellers and revenue count bookings that are not draft, cancelled or rejected. Average days to pay is taken over bookings paid in full with at least one verified payment: from the day the booking was made to the received date of its last verified payment (never below zero). Sums are in the agency's currency; a booking in another currency is counted, not summed, and the record says how many.
Enforced by: partner_performance() inside partner_record(). Test: supabase/tests/partner_profile.sql.
PTR-090 · The Partners list shows where and who
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-084
The Partners list shows each partner's city, relationship manager and tier, filters by each (including "none set"), and its search box also finds a city or a manager's name. The list is still searched in the browser; the extra columns come from one read (agents.view).
Enforced by: partner_list_extras() (agents.view), GET /agents; filterPartners in src/pages/partners/partnersMath.ts.
Amended 2026-09-28 (PTR-092). The list also names the person each partner is known by — see PTR-092.
PTR-091 · The partner record reads like a company profile
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-083, PTR-084, PRF-010
The owner, on the Business Partners screen: "if you don't know how to make best facebook profile like page for business partner with relevant options inside, tell me". The partner record (/partners/:id) keeps everything PTR-083 … PTR-089 put on it, laid out as a profile:
- Header — a soft band in the brand colour, the agency's initials as its picture, its name, a verified mark, the partner code (copied on a click), the tier, city and district, "Partner since" (month and year) and the relationship manager linked to their employee record.
- Verified means the partner is active and every required document (PTR-080) is on file, accepted by the office and not expired. It is worked out from the record each time; nothing is stored, and the mark decides nothing. The tooltip says why a partner is not verified.
- Action bar — New booking (an active partner, bookings.create: the booking wizard opens with the booking made through this partner), Call and WhatsApp (the primary contact, else the first contact with a number, else the agency's phone — PTY-008), Ledger, and More: Edit profile, Edit terms (commission, credit limit), Access key, Pause or resume the login, Suspend / Lift suspension / Deactivate / Reactivate, Delete. Approve and Reject a pending registration stay on the bar. Each keeps its gate, reason and confirmation.
- Numbers — bookings, travellers, revenue this financial year, outstanding (red only when over the credit limit), credit left, average days to pay (PTR-089).
- Tabs — Timeline first: the activity (PTR-083) as a feed, newest first, with a note composer on top (PTR-088) and, beside it, the Intro (business, owner, established, website, address, GSTIN, PAN), the contacts with Call and WhatsApp, the registrations with the licence's expiry, the documents summary (accepted of required, what is missing) and the relationship. Then About, Bookings (with the performance and the requests), Payments & ledger (a credit gauge), Travellers, Documents (cards), Notes (filtered by kind) and Access.
- 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, in the record's same one read (PRF-010). A booking row on the timeline says how many travellers it has and its group.
Not built: a logo or cover picture for a partner — the header shows initials; a partner's documents have no "logo" type.
Enforced by: partner_record() as redefined in 20261002100000_a_partner_profile_page.sql (travellers, the booking row's detail); src/pages/partners/PartnerRecord.tsx, PartnerProfileLayout.tsx, partnerProfileMath.ts (the verified mark); the booking wizard reads ?agentId=; the app's admin/partners/[id] screen. Test: supabase/tests/partner_profile_page.sql; src/pages/partners/PartnerRecord.test.tsx, partnerProfileMath.test.ts.
PTR-092 · The Partners list names a person, never a placeholder
Status: DECIDED 2026-09-28 (owner) · Owner: B2B Manager · Source: PTR-090, PTR-091
A partner who signs up without a contact name is stored as "New Agent". The list never shows that name, and never shows a partner's internal id. Each partner is named by its primary contact, else the owner on the profile, else the contact person typed at sign-up, else the login's own name when it is a person and not the company again; with none of these the list says "No contact yet". The search box also finds that name. A click on a partner opens its profile (PTR-091); the other actions — Ledger, Edit, Access key, Approve / Reject, Deactivate / Reactivate, Delete — are in one ⋯ menu per partner with the same gates and confirmations. The list shows cards or a table; the choice is kept in the browser.
Enforced by: partner_list_extras() (the owner and the primary contact, agents.view) in 20261002100000_a_partner_profile_page.sql, GET /agents; partnerContactName in src/pages/partners/partnersMath.ts. Test: supabase/tests/partner_profile_page.sql; src/pages/partners/Partners.test.tsx, partnersMath.test.ts.
PTR-093 · The partner pays from the web portal too
Status: DECIDED 2026-09-29 (owner: "How can a business partner add money to his bookings on his web and phone application?") · Owner: Finance · Source: PTR-081, FIN-032, PRF-010
The web partner portal has a Payments page with what the phone app's Money tab has for paying: what the agency owes against its credit limit and the credit left; the bookings with a balance, each with Pay online (rupee bookings) and I have paid; the issued group invoices with a balance, each with I have paid; the payments waiting for the office; and the latest verified payments. A booking opened from the bookings list offers the same two buttons while a balance is due. Both actions go through the functions PTR-081 names and nothing else — partner_submit_payment() records a claim pending until finance verifies it; razorpay-order starts an online payment and razorpay-webhook records it — so the checks, the amounts and the refusals are the same on the web and the phone, and the page shows the database's own sentence when it refuses. Every action asks Yes/No first (PTR-070). The page's reads are sent side by side (one round trip). Razorpay Checkout is loaded only when the partner presses Pay online. A registration under review sees no payment buttons; a suspended partner may still pay. Pay online is the partner's (or the booking's customer's) own action: staff never start an online payment for a partner's booking (DECIDED 2026-09-30, FIN-032).
Not built: a receipt upload, a payment date and a note on a claim (the function takes none; the phone has none either), funds on account without a target (finance records those), and the statement (ledger) on the web.
Enforced by: partner_submit_payment() and online_payment_actor() as in PTR-081; routes /portals/agent/payments* in src/lib/partnerPayments.ts; src/pages/partner/PartnerPayments.tsx, src/components/partner/PartnerPaymentDialogs.tsx; CSP hosts for Checkout in public/_headers. Tests: src/lib/api.partnerPayments.test.ts, src/pages/partner/PartnerPayments.test.tsx, src/services/partnerPaymentsService.test.ts, src/lib/razorpayCheckout.test.ts.
PTR-094 · Flights shows flights
Status: DECIDED 2026-09-29 (owner: "Also the flights are showing some groups there") · Owner: B2B Manager · Source: PTR-060
The partner portal's Flights page lists flights only: the seats the office offers to partners at the published price, with Book (PTR-060). It does not list travel groups; it points to New booking for a Hajj/Umrah package and to Seat Inventory for seats already bought. The question about a departure that the Flights page used to send ("Request Package") is a request on the portal's Requests tab, which can now name a departure (type Package / departure enquiry) — the same partner_create_request(). The phone app's Offers screen never listed groups under flights.
Enforced by: src/pages/partner/PartnerFlights.tsx (no group read), src/pages/partner/PartnerPortal.tsx (the request's departure). Test: src/pages/partner/PartnerFlights.test.tsx.
PTR-095 · A GM, CEO or Admin approves a new partner
Status: DECIDED 2026-10-02 (owner: "they should be approved by either general managers, CEOs, or Admin only") · Owner: Management · Source: PTR-001
Only a General Manager, a CEO or an Admin (IT Admin, Super Admin) approves or rejects a pending partner, with a reason. The same holds for making active a closed partner that was never approved, so a rejected registration cannot be "reactivated" around the approval. The partner desk (partners.edit) still edits, suspends, deactivates and lifts a suspension of an approved partner. The approval is stamped on the partner (approvedAt, approvedBy) and audited (partner.approve / partner.reject).
Enforced by: permission partners.approve (GM, CEO, IT_ADMIN, SUPER_ADMIN); partner_set_status and the trigger Agent_approval_guard (20261008120000); POST /agents/:id/status. Test: supabase/tests/new_partners_and_suppliers_wait_for_approval.sql.
PTR-096 · Sales hear about every new partner
Status: DECIDED 2026-10-02 (owner: "should trigger an email to sales@alhudatravels.in") · Owner: Sales Manager · Source: PTR-001, COMM-039
When a partner is added and waits for approval, the database e-mails sales@alhudatravels.in — the agency, its code, contact, phone, e-mail, its commission and credit limit (amended 2026-10-03, PTR-030), and who added it — and puts a notice (with the commission and credit limit) in the inbox of everyone who can approve it. The approval or rejection e-mails sales@ again, with who decided and why. Once per partner and event; a failed e-mail never stops the partner being added.
Enforced by: triggers Agent_onboarding_notice and partner_set_status → onboarding_notify → notify_enqueue (category onboarding, switchable like any notification category) (20261008120000). Test: supabase/tests/new_partners_and_suppliers_wait_for_approval.sql, supabase/tests/staff_add_partner.sql.
PTR-097 · Staff add a partner from the phone
Status: DECIDED 2026-10-03 (owner: "I don't see adding suppliers or business partners option in the mobile app"; on a shared PAN: "Accept but always highlight it somehow in light dim label") · Owner: Sales · Source: PTR-001, PTR-030, PTR-095, PTR-096
A member of staff holding partners.create adds a business partner from the staff app with the desktop form's fields and checks: company required; PAN required, ten characters in the PAN shape (ABCDE1234F); GSTIN optional, but a valid 15-character GST number when given; commission 0 to 100; credit limit 0 to ₹1,000 crore; an e-mail, when given, shaped like one. The commission and the credit limit are set only by finance and leadership (PTR-030); for anyone else the app hides them and they are 0. The partner is pending like any other (PTR-001), gets its BP- code and its receivable and payable ledgers at once, and sales@ and the approvers are told, with the terms (PTR-096). Only a GM, CEO or Admin approves it (PTR-095). A portal login is not made from the phone; it is added from Partners on the desktop (Access key).
A double tap adds once. The same PAN and company sent again by the same login within two minutes returns the partner that login just added. The PAN is locked while it is checked, so two taps cannot both pass.
A shared PAN or GSTIN is allowed and always shown. A PAN or GSTIN already on another partner is refused until the person confirms with "Add anyway" (two branches of one agency may share a PAN; neither is unique in the database). Someone who may read partners (agents.view) is told which partner holds it; anyone else is told only "This PAN is already registered with Alhuda. Ask the office." — no name, code or id. After "Add anyway", every partner that shares a PAN or GSTIN with another carries a light, dim label on the partner lists, web and phone (Shares PAN with BP-0012), naming the others (agent_shared_tax_ids, a computed field read under the reader's own row policies).
Still open: a GM or CEO holds both partners.create and partners.approve, so they can add a partner and approve it themselves. No maker-checker rule (a different person approves) is decided.
Enforced by: partner_staff_create() (20261008230000_staff_add_partner.sql: partners.create, is_staff_user(), the checks, the terms permissions, pg_advisory_xact_lock on the PAN, Agent."createdBy", the insert that fires Agent_party_code, Agent_approval_guard, Agent_terms_guard and Agent_onboarding_notice, then fin_agent_receivable_account() / fin_agent_payable_account()); the indexes Agent_pan_live_idx and Agent_gstin_live_idx; the app's admin/partners/new screen (apps/mobile/src/lib/newPartner.ts). The desktop's POST /agents makes the same partner from the browser without the duplicate check. Test: supabase/tests/staff_add_partner.sql; apps/mobile/src/lib/newPartner.test.ts.
Confirmation
PTR-070 · Partners confirm every action
Status: DECIDED 2026-09-17 (UX-001) · Owner: Management
Booking creation, adding passengers, reserving beds, selling seats, saving or issuing invoices, requests and profile changes each ask Yes/No with a summary first.
Enforced by: useConfirm in the partner portal pages; Alert.alert Yes/No in the app's partner screens (booking, reserve, sell, request, profile).