Bookings, Groups & Sales API
The sales and operations surface: booking CRUD + import + invoice, wizard quotations, travel groups, group-invoices, flight/hotel linking for passengers, and the ops/finance approval lifecycle.
Sales bookings
Handlers: handleSalesBookings at src/lib/api.ts:3730,
handleSalesBookingsById at src/lib/api.ts:6108,
handleSalesBookingsImport at src/lib/api.ts:3611,
handleBookingInvoice at src/lib/api.ts:4420.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/sales/bookings |
GET | (read-only, agent-scoped) | List bookings; agents see only their own; supports ?page= pagination envelope |
/sales/bookings |
POST | bookings.create |
Create booking with passengers, per-category rates, B2B seat reservation |
/sales/bookings/import |
POST | no explicit gate — delegates to handleSalesBookings('POST') which requires bookings.create |
Bulk import bookings (sync customer passports, auto-create Customer rows) |
/sales/bookings/:id |
GET | (read-only) | One booking + passengers + payments + customer/payer/group/agent + resolved flight labels |
/sales/bookings/:id |
PATCH | bookings.edit |
Update fields, status transitions, passenger roster |
/sales/bookings/:id |
DELETE | bookings.delete |
Soft-delete (sets deletedAt); reverses all booking-scoped journal entries |
/sales/bookings/:id/invoice |
GET | (read-only) | Generate invoice payload for the booking |
/sales/bookings/:id/passengers/:pid |
PATCH | bookings.edit |
Update one passenger's PNR, flightId, or passportNo |
POST /sales/bookings
Cite: src/lib/api.ts:3934.
Key inputs — customerId, groupId?, agentId?, bookingType: 'umrah'|'hajj',
roomType, per-category rates (adultRate, childRate, infantRate) + counts, service
rates (ticketRate, visaRate, hotelRate, mealsRate, groundRate), passengers: [],
b2bOfferId?, paymentPolicy, quotationId?.
Work:
1. Compute totalAmount from per-category rates × counts (or fall back to explicit
totalAmount).
2. Mint bookingNo from FinanceConfig.bookingPrefix + timestamp.
3. Reject duplicate (same customer + same group).
4. If b2bOfferId, check seat availability and decrement B2BFlightOffer.seatsAvailable
atomically (matched-by-version UPDATE). Restore seats if subsequent steps fail.
5. Insert Booking, then BookingPassenger rows.
6. Audit + group history logs.
POST /sales/bookings/import
Cite: src/lib/api.ts:3611. Bulk-import shape:
{ bookings: [{ bookingRef, passengers: [...], ...bookingFields }] }.
- Resolves each passenger by
passportNo. Creates a newCustomerrow viahandleSalesCustomers('POST', ...)if none exists. - Updates existing customers in-place with any new data from the booking form.
- Reports per-booking results:
{ ref, ok, bookingId?, error? }.
No single permission check
handleSalesBookingsImport itself does not call requirePermission, but every
delegated call to handleSalesBookings('POST') does require bookings.create, and
every customer insert requires customers.create. The effective permission for the
import is bookings.create + customers.create.
PATCH /sales/bookings/:id
Cite: src/lib/api.ts:6299. Permission: bookings.edit.
Handles status transitions (validated via normalizeBookingStatus), passenger roster
updates (diff-based audit), balance recomputation, and group-history logging.
DELETE /sales/bookings/:id
Cite: src/lib/api.ts:6929. Permission: bookings.delete.
Destructive — reverses journals
Soft-deletes the booking (sets deletedAt) and then posts a booking_reversal
journal for every booking / booking_gst entry tied to the booking. This is a
compensating transaction, not a cascade delete. If the reversal posting fails, the
delete still persists (audited as a warning).
Per-passenger flights & hotels
Handlers: handlePassengerFlights at src/lib/api.ts:5709,
handlePassengerHotels at src/lib/api.ts:5963.
| Route | Method | Permission |
|---|---|---|
/sales/bookings/:id/passengers/:pid/flights |
GET | bookings.view |
/sales/bookings/:id/passengers/:pid/flights |
POST | bookings.edit |
/sales/bookings/:id/passengers/:pid/flights/:assignmentId |
PATCH, DELETE | bookings.edit |
/sales/bookings/:id/passengers/:pid/hotels |
GET | bookings.view |
/sales/bookings/:id/passengers/:pid/hotels |
POST | bookings.edit |
/sales/bookings/:id/passengers/:pid/hotels/:rowId |
PATCH, DELETE | bookings.edit |
These feed the passenger-level itinerary on the booking detail page. The flight handler
uses evaluateNewLeg / analyseItinerary from passengerFlightIntelligence.ts to validate
route continuity; the hotel handler uses the matching stay analyser.
Operations approval
Handler: handleOperationsBookings at src/lib/api.ts:13370.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/operations/bookings/:id/ops-approve |
POST | Ops → finance; idempotent if already approved | |
/operations/bookings/:id/ops-send-back |
POST | Send booking back for correction | |
/operations/bookings/:id/resubmit |
POST | Resubmit corrected booking to finance |
Cite: src/lib/api.ts:13370-13761. Note that these handlers do
not call requirePermission — they rely on UI-level <PermissionGate> +
<ProtectedRoute> on the approvals queue. Flag for follow-up hardening (add
requirePermission('approvals.approve') or similar).
Idempotency (ops-approve) — returns { ok: true, alreadyApproved: true } if
opsApprovalStatus === 'approved'. Auto-creates visa cases for passengers with
needsVisa=true after approval.
Quotations (sales wizard)
Handler: handleSalesQuotations at src/lib/api.ts:9677.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/sales/quotations |
GET | (read-only, agent-scoped) | List quotations; partners see their own |
/sales/quotations |
POST | quotations.create |
Create quotation with line items, mint quotationNo |
/sales/quotations/:id |
GET | (read-only) | Full quotation with line items, customer, agent, group |
/sales/quotations/:id |
PATCH | quotations.create |
Update status/notes/totals/line-items |
/sales/quotations/:id/send |
POST | (no explicit gate — read-only precondition) | Mark status=sent and email customer |
Quotation PATCH requires quotations.create
Cite: src/lib/api.ts:9908. The PATCH reuses the create
permission rather than a separate quotations.edit. Intentional — editing an unsent
quotation is effectively the same authority as creating one.
Travel groups
Handler: handleGroups at src/lib/api.ts:9946.
Group CRUD
| Route | Method | Permission | Purpose |
|---|---|---|---|
/groups |
GET | (read-only) | List groups (supports ?scope=active|archived|all) |
/groups |
POST | groups.create |
Create group; groupCode and name auto-minted by Postgres trigger |
/groups/status |
GET | (read-only) | Group status summary |
/groups/flights-summary |
GET | (read-only) | Summary of all group flights |
/groups/:id |
PATCH | groups.edit |
Update group (type/code are immutable post-create; only dates, capacity, metadata, itinerary, status override) |
/groups/:id |
DELETE | groups.delete |
Soft-delete (isActive=false) |
/groups/:id/bookings |
GET | (read-only) | Bookings in the group with live payment recomputation |
/groups/:id/expenses |
GET | (read-only) | Group expense/P&L report (flights, hotels, meals, ground, commissions) |
/groups/:id/available-hotels |
GET | bookings.view |
Hotels available to assign to this group |
/groups/:id/available-flights |
GET | bookings.view |
Available airline blocks + FIT rows |
Group pricing
| Route | Method | Permission | Purpose |
|---|---|---|---|
/groups/:id/pricing |
GET | groups.view |
Current rate sheet |
/groups/:id/pricing |
PATCH | group_pricing.edit |
Save rate sheet |
/groups/:id/pricing/refresh-from-inventory |
POST | group_pricing.edit |
Preview defaults from linked inventory (does not save) |
Cite: src/lib/api.ts:10473-10657.
Group flights (link inventory)
Cite: src/lib/api.ts:10827.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/groups/:id/flights |
GET | (read-only) | All flights linked to the group |
/groups/:id/flights |
POST | groups.edit |
Link an AirlineQuotaBlock or FITInventory (exclusivity enforced — one block per group) |
/groups/:id/flights/:flightId |
PATCH | groups.edit |
Update PNR / notes |
/groups/:id/flights/:flightId |
DELETE | groups.edit |
Unlink |
Group misc expenses
Cite: src/lib/api.ts:10998.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/groups/:id/misc-expenses |
GET | (read-only) | List |
/groups/:id/misc-expenses |
POST | groups.edit |
Record expense; posts journal if sourceAccountId supplied |
/groups/:id/misc-expenses/:expenseId |
PATCH, DELETE | groups.edit |
Edit or delete (reverses journal on delete) |
Group rate sync to bookings
The group-pricing rate sheet drives both:
- GroupInvoice totals (line-items × pax snapshot) — see below.
- Booking defaults when bookings are created under the group.
Refreshing pricing from inventory (POST /groups/:id/pricing/refresh-from-inventory)
reads linked flights/hotels/meals/transfers, converts non-INR amounts via per-contract
exchangeRate, flattens per-assignment costs into per-pax rates, and returns the computed
rate sheet without saving. The caller PATCHes to commit.
Group invoices
Handler: handleGroupInvoices at src/lib/api.ts:9095.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/group-invoices |
GET | group_invoices.view |
List (supports ?groupId=, ?status=, ?openOnly=1) |
/group-invoices |
POST | group_invoices.create |
Create DRAFT invoice for a payer |
/group-invoices/:id |
GET | group_invoices.view |
One invoice + payer name |
/group-invoices/:id |
PATCH | group_invoices.edit |
Edit DRAFT only — recomputes taxes and totals server-side |
/group-invoices/:id |
DELETE | group_invoices.cancel |
Delete DRAFT only |
/group-invoices/:id/issue |
POST | group_invoices.issue |
DRAFT → ISSUED; mints invoiceNumber, posts DR receivable / CR revenue + tax journal |
/group-invoices/:id/cancel |
POST | group_invoices.cancel |
ISSUED → CANCELLED; posts reversing credit-note journal |
/group-invoices/:id/supplementary |
POST | group_invoices.create |
Create a child DRAFT for pax delta (parent must be ISSUED/PAID) |
State machine
DRAFT → ISSUED → PAID or CANCELLED.
- PATCH is rejected (400) on anything except
DRAFT. - DELETE is rejected on anything except
DRAFT("cancel instead"). - Issue is idempotent — an already-ISSUED invoice returns the current row.
- Cancel is idempotent — an already-CANCELLED invoice returns the current row.
Cancel posts a credit note, doesn't just flip status
POST /group-invoices/:id/cancel fetches the original JournalLine rows and posts
an inverted journal so the GL balances are restored. Pre-conditions: the invoice
must have a journalEntryId (i.e. was ISSUED, not just DRAFT).
Supplementary invoices
Child of an ISSUED/PAID parent, used when additional passengers join the group after the
parent is issued. Priced using the parent's current rate sheet times the pax delta.
Shares the PAID status with the parent on full payment. Cite:
src/lib/api.ts:9371.
Group status
Handler: handleGroupStatus at src/lib/api.ts:5325.
GET /groups/status — summary counts for the Groups dashboard (active, departing soon,
completed).
Portals (agent + customer)
Partner and customer portal routes are JWT-authenticated but un-gated at the
permission layer. They scope every read/write to the authenticated party's own rows via
resolveAgentForCurrentUser() / customer email lookup.
Agent portal
Handlers: handleAgentPortal at src/lib/api.ts:9455,
handleAgentInvoices at :8821, handleAgentPortalLedger at :8549,
handlePartnerSeatSales at :8706.
| Route | Method | Purpose |
|---|---|---|
/portals/agent |
GET | Partner dashboard snapshot |
/portals/agent/profile |
GET, PATCH | Self-profile (partners can only update name + contactPerson) |
/portals/agent/invoices |
GET | Agent's invoices |
/portals/agent/invoices/:id |
GET, PATCH | One invoice |
/portals/agent/ledger |
GET | Agent ledger with running balance |
/portals/agent/seat-sales |
GET, POST | Partner seat sales (B2B flight offer sales) |
/portals/agent/seat-sales/:id |
GET, PATCH | One sale |
/portals/agent/requests |
GET, POST | Partner's own service requests |
Customer portal
Handlers: handleCustomerPortal at src/lib/api.ts:8321,
handleCustomerQuotations at :8082, handleCustomerProfile at :8277,
handleCustomerPortalRequests at :8154, handleCustomerPortalCreateRequest at :8202.
| Route | Method | Purpose |
|---|---|---|
/portals/customer |
GET | Customer dashboard |
/portals/customer/profile |
GET, PATCH | Self-profile |
/portals/customer/quotations |
GET | Customer's quotations |
/portals/customer/quotations/:id/accept |
POST | Accept own quotation |
/portals/customer/quotations/:id/reject |
POST | Reject own quotation |
/portals/customer/requests |
GET, POST | Customer's own requests |
/portals/customer/requests/new |
POST | Create a new request |
Service requests
Handler: handleRequests at src/lib/api.ts:20115 (staff-facing)
+ inline at src/lib/api.ts:27920 for creation.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/requests |
GET | (read-only) | List staff-facing service requests |
/requests |
POST | requests.create |
Staff creates a request (customers use portal endpoint instead) |
/requests/:id |
PATCH | requests.edit |
Update status, priority, notes |
/requests/:id/notes |
POST | requests.edit |
Append an internal note |
Leads
Handler: handleLeads at src/lib/api.ts:20023,
handleLeadById at :20076, handleLeadConversion at :19897.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/leads or /sales/leads |
GET, POST | leads.edit (POST) |
List / create lead |
/leads/:id |
GET, PATCH, DELETE | leads.edit |
CRUD |
/leads/:id/convert |
POST | leads.edit |
Convert lead → customer (+ optional booking) |