Customers, Partners & Suppliers
Party-level endpoints: end customers, B2B partners (agents), suppliers, leads, and the unified "people" surface.
Customers
Handlers: handleSalesCustomers at src/lib/api.ts:6992,
handleSalesCustomersById at src/lib/api.ts:7220,
handleCustomerSync at src/lib/api.ts:7123,
handleSalesCustomerPassengers at src/lib/api.ts:7303,
handleSalesCustomerDocuments at src/lib/api.ts:7381,
handleSalesCustomerDriveDocuments at src/lib/api.ts:7455,
handleSalesCustomerLedger at src/lib/api.ts:7515,
handleCustomerPasswordRoute at src/lib/api.ts:8014.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/sales/customers |
GET | (read-only) | List customers |
/sales/customers |
POST | customers.create |
Create or revive soft-deleted customer (keyed by passport) |
/sales/customers/sync |
POST | customers.edit |
Sync customer rows with auth users (mirror User → Customer) |
/sales/customers/:id |
GET | (read-only) | One customer with full profile |
/sales/customers/:id |
PATCH | customers.edit |
Update customer profile |
/sales/customers/:id |
DELETE | customers.delete |
Soft-delete (deletedAt) |
/sales/customers/:id/passengers |
GET, POST | customers.edit (POST) |
Saved passengers linked to the customer (family members) |
/sales/customers/:id/passengers/:pid |
PATCH, DELETE | customers.edit |
Update or remove a saved passenger |
/sales/customers/:id/documents |
GET | (read-only) | Documents attached to the customer |
/sales/customers/:id/drive-documents |
GET, POST | customers.edit (POST) |
Google-Drive-backed docs |
/sales/customers/:id/drive-documents/:docId |
DELETE | customers.edit |
Remove a drive doc |
/sales/customers/:id/ledger |
GET | (read-only) | Customer ledger with running balance |
/sales/customers/:id/password |
POST | customers.edit |
Admin resets customer portal password |
POST /sales/customers
Cite: src/lib/api.ts:7037.
Input — full personal profile: { title?, firstName, lastName, dob?, gender?,
fatherName?, bloodGroup?, passportNo, passportIssuedDate?, passportExpiry?, nationality?,
panCard?, aadhaarNumber?, phone?, email?, address?, district?, state?, country?,
pinCode?, packagePrice?, currency?, roomType?, source?, sourceAgentId?, agentId?,
gstNumber? }.
Special rules:
- Passport uniqueness — if
passportNomatches an existing row: - If the existing row is soft-deleted, it is revived (ID reused; fields updated).
- If active, returns
409 Conflict— "A customer already exists with this passport number." gstNumberis uppercased + trimmed defensively.sourcedefaults toDirect. Partners' clients setsource='Through Business Partner'withsourceAgentIdpointing to the partner.
GET /sales/customers/:id/ledger
Returns a customer's full ledger — every payment, invoice, booking commitment, on-account receipt allocation — with running balance per row and summary totals.
Per-customer passengers (family members)
Customers can register saved passengers (spouse, children) that get auto-populated when
booking. CRUD is at /sales/customers/:id/passengers/[:pid] — all writes require
customers.edit. Cite: src/lib/api.ts:7303.
Partners (agents)
Handlers: handleAgents at src/lib/api.ts:12646,
handleAgentsById at src/lib/api.ts:12801,
handleAgentLedger at src/lib/api.ts:7713.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/agents |
GET | (read-only) | List partners with totalBilled / totalPaid / outstanding / totalBookings rollups |
/agents |
POST | partners.create |
Create partner; optionally provisions a Supabase auth user + AGENT role; auto-ensures A/R + A/P GL accounts |
/agents/:id |
PATCH | partners.edit |
Update partner; optionally createLogin=true provisions login; supports re-linking to existing auth user |
/agents/:id |
DELETE | partners.delete |
Soft-delete; rejected (409) if partner has bookings or ledger entries |
/agents/:id/ledger |
GET | (read-only) | Agent ledger with running balance |
POST /agents
Cite: src/lib/api.ts:12704.
Input — { name, company?, email?, phone?, commissionRate?, panCard?, gstNumber?,
address?, contactPerson?, createUser?, password? }.
Side-effects:
- If
createUser=true, callinvokeAdminUsers({ action: 'create_user', ... })to provision the auth user, assign AGENT role, and linkAgent.userId. - If the auth user already exists (email collision), link the existing user and reset the password.
- Ensure the partner's receivable (A/R) and payable (A/P) sub-ledger accounts exist
in the chart of accounts (
ensureAgentReceivableAccount,ensureAgentPayableAccount).
Returns — { id, name, tempPassword?, loginError? }.
DELETE /agents/:id
Delete is blocked if history exists
Returns 409 Conflict with a clear message if the partner has any:
- Bookings (
Booking.agentId = :id) - Ledger entries on their A/R or A/P GL accounts
Deactivate via PATCH /agents/:id with status='inactive' instead. Cite:
src/lib/api.ts:12807.
Suppliers
Handler: handleSuppliers at src/lib/api.ts:17065.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/suppliers |
GET | (read-only) | List (supports ?category=, ?includeInactive=true) |
/suppliers |
POST | suppliers.create |
Create supplier; duplicate-detects by category + name (+ airline for ticketing) |
/suppliers/:id |
PATCH | suppliers.edit |
Update supplier |
/suppliers/:id |
DELETE | suppliers.delete |
Soft-delete |
/suppliers/:id/ledger |
GET | (read-only) | Supplier ledger with running balance; filters SupplierTransaction rows whose journalEntryId is not yet approved |
/suppliers/:id/transactions |
GET | (read-only) | All transactions |
/suppliers/:id/transactions |
POST | suppliers.edit |
Create supplier transaction (debit / credit / refund); posts journal; optionally deducts TDS |
/suppliers/transactions/:txnId |
PATCH | suppliers.edit |
Edit transaction; auto-reposts journal delta |
/suppliers/transactions/:txnId |
DELETE | suppliers.delete |
Delete transaction; reverses journal |
/suppliers/transactions/:txnId/allocate |
POST | suppliers.edit |
Apply a credit/refund to a specific debit |
/suppliers/quota-payments |
GET | inventory.view |
Supplier payments tied to airline quota blocks |
Duplicate detection
POST /suppliers first calls findSupplierDuplicate which checks:
- For
category='ticketing'with anairlineId: any existing active supplier for that airline. - For all categories: any existing supplier with the same category + normalized name (case-insensitive, whitespace-collapsed).
If a duplicate is found it is returned as 409 Conflict with the existing row's id.
TDS inside supplier flows
Cite: src/lib/api.ts:17650. When a supplier payment qualifies
for TDS deduction, the handler calls requirePermission('finance.tds.deduct') inline
before recording the deduction and TDS-liability journal.
Airline quota block outstanding guard
Cite: src/lib/api.ts:17200. When creating a supplier credit/refund
linked to an AirlineQuotaBlock, the handler computes outstanding = (seats × pricePerSeat)
- already-paid and rejects (400) if the new payment would exceed the outstanding amount.
Leads
Handlers: handleLeads at src/lib/api.ts:20023,
handleLeadById at src/lib/api.ts:20076,
handleLeadConversion at src/lib/api.ts:19897.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/leads or /sales/leads |
GET | (read-only) | List leads |
/leads or /sales/leads |
POST | leads.edit |
Create lead |
/leads/:id |
GET | (read-only) | One lead |
/leads/:id |
PATCH, DELETE | leads.edit |
Update / delete |
/leads/:id/convert |
POST | leads.edit |
Convert lead → customer (+ optional booking) |
POST /leads/:id/convert accepts { createBooking?: boolean, bookingPayload? } and when
createBooking=true delegates to handleSalesBookings('POST', ...).
People (unified party query)
The "People" frontend page reads from /sales/customers for customers and /agents for
partners — there is no /people handler. Dedicated to the People UI:
GET /sales/customersfor customer list.GET /agentsfor partner list.GET /usersfor staff users (see Admin).
Customer portal password management
POST /sales/customers/:id/password — creates or resets the customer portal login. Cite:
src/lib/api.ts:8014. Supports two modes depending on body:
{ action: 'create', password }— provisions a Supabase auth user for the customer (linked viaCustomer.userId) and assigns theCUSTOMERrole.{ action: 'reset', password }— resets the existing linked user's password.