Skip to content

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 inputscustomerId, 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 new Customer row via handleSalesCustomers('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.

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:

  1. GroupInvoice totals (line-items × pax snapshot) — see below.
  2. 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

DRAFTISSUEDPAID 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)