Skip to content

Booking Wizard

The Booking Wizard is the guided, multi-step flow for creating a new booking. It is the canonical path — every other entry (lead conversion, quotation acceptance, CSV import) ultimately funnels through the same POST /sales/bookings endpoint the wizard calls.

Where it lives

  • Route: /sales/bookings/new — gated by bookings.create (src/App.tsx:161)
  • Page wrapper: src/pages/sales/BookingWizard.tsx:4
  • Main component: src/components/bookings/wizard/WizardShell.tsx:29
  • State shape + helpers: src/components/bookings/wizard/wizardTypes.ts
  • Per-passenger subform: src/components/bookings/wizard/PassengerForm.tsx

1. Shape

WizardShell renders a 4-step linear form with a step indicator at the top (WizardShell.tsx:425) and a single Back / Next / Confirm footer (WizardShell.tsx:958). Navigation is blocked until canProceed passes for the current step (WizardShell.tsx:223).

State is held in a single WizardState object (wizardTypes.ts:47) — one source of truth for everything the wizard collects. A set(key, value) helper mutates it (WizardShell.tsx:80); any changes to the service-rate fields (ticketRate, visaRate, hotelRate, mealsRate, groundRate) auto-recompute adultRate as their sum.

2. Flow diagram

flowchart TD
    Start([User lands on /sales/bookings/new]) --> LoadRef[Load groups, agents, customers, currencies]
    LoadRef --> S0[Step 1: Source & Group]
    S0 -->|Pick tour type + sub-type| S0a{Source?}
    S0a -->|direct| S0c[Pick travel group]
    S0a -->|agent| S0b[Pick partner] --> S0c
    S0c --> S1[Step 2: Passengers]

    S1 --> PaxLookup[Passport lookup against existing customers]
    PaxLookup --> PaxValidate{All pax valid?}
    PaxValidate -->|no| S1
    PaxValidate -->|yes| S2[Step 3: Package & Payment]

    S2 --> Sync[Auto-sync rates from group rate sheet silently]
    Sync --> RateBreakdown[User edits ticket/visa/hotel/meals/ground rates]
    RateBreakdown -->|Click Sync button| SyncOverride[syncGroupRates overwrite=true]
    SyncOverride --> RateBreakdown
    RateBreakdown --> GST[GST toggle + rate]
    GST --> FirstPay{Source agent?}
    FirstPay -->|agent| S3[Step 4: Review]
    FirstPay -->|direct| FirstPayForm[First payment amount + method + reference] --> S3

    S3 --> Confirm[Click Confirm & Create Booking]
    Confirm --> Submit[handleSubmit]
    Submit --> CreateCust[For each passenger: POST /sales/customers]
    CreateCust --> CreateBooking[POST /sales/bookings with passengers array]
    CreateBooking --> PaymentCheck{First payment > 0 and direct?}
    PaymentCheck -->|yes| Pay[POST /finance/payments verified] --> Done
    PaymentCheck -->|no| Done
    Done([Toast + navigate to /sales/bookings])

3. Step 1 — Source & Group

Renders the tour category picker, the booking source, and the travel group selector.

Field Captures Validation
Tour category PILGRIMAGE / GENERAL (WizardShell.tsx:451) Required
Sub-type (pilgrimage) HAJ, HAJ_ZIYARAT, UMRAH, UMRAH_ZIYARAT, ZIYARAT (WizardShell.tsx:474) Required if PILGRIMAGE
Sub-type (general) DOMESTIC, INTERNATIONAL (WizardShell.tsx:500) Required if GENERAL
Booking source direct or agent (WizardShell.tsx:523) Required
Business partner agentId (WizardShell.tsx:535) Required when source=agent
Travel group groupId (WizardShell.tsx:552) Required; filtered by selected tour type and status in (open, planning)

Hajj disclosure

Selecting HAJ or HAJ_ZIYARAT flips bookingType to 'hajj' (WizardShell.tsx:93) which surfaces mandatory Blood Group + PAN + Aadhaar fields for every adult/child passenger in Step 2.

Once a group is picked, a compact info strip shows departure, return, duration, capacity, and available seats (WizardShell.tsx:571).

4. Step 2 — Passengers

The passenger count field at the top (WizardShell.tsx:589) controls the array length; handlePassengerCountChange (WizardShell.tsx:187) clamps between 1 and 50 and grows / shrinks passengers[] accordingly.

Each passenger is rendered through PassengerForm, which captures:

  • Title (auto-assigned from DOB + gender via calculateTitlewizardTypes.ts:125)
  • First/Last name (uppercased on submit)
  • DOB — drives passengerCategory (adult / child / infant) via calculateCategory (wizardTypes.ts:113; rule: infant < 2 yrs, child 2–12 yrs, adult ≥ 12 yrs, measured against the group's departure date)
  • Gender
  • Passport No (uppercased, dedup-lookup happens live — lookupPassport, WizardShell.tsx:198)
  • Passport Issued Date + Expiry (mandatory except for GENERAL_DOMESTIC)
  • Nationality (default INDIAN)
  • Phone, email, address block
  • sameAddressAsFirst on passengers 2+ — copies P1's address (WizardShell.tsx:211)
  • Infant-only: mother/guardian name — one-click chips suggest adult-female names from the same booking (WizardShell.tsx:260)
  • Hajj-only (when bookingType === 'hajj'): Blood Group, PAN, Aadhaar

Passport dedup

When the user types an 8-character passport number, lookupPassport scans the already-fetched customer cache. If a match exists, the form can auto-fill from the existing Customer row — this is the mechanism that prevents duplicate customers on repeat travellers. At submit time, if the server still rejects with HTTP 409 (duplicate), the wizard falls back to the existing ID (WizardShell.tsx:318).

Validation (step 2)

See WizardShell.tsx:230. Required per passenger:

  • First name, last name, DOB, gender, passport no
  • Passport dates unless tour is GENERAL_DOMESTIC
  • Adults: title must be confirmed
  • Infants: motherName must be filled
  • Hajj bookings: bloodGroup, panCard, aadhaarNumber for every adult/child

5. Step 3 — Package & Payment

This is where the money is shaped. Three zones stacked top-to-bottom:

5.1 Rate Breakdown (per adult) and the Sync button

WizardShell.tsx:628. Five inputs — Ticket, Visa, Hotel, Meals, Ground — feed the adult rate. On any change the set() helper auto-sums them into adultRate (WizardShell.tsx:83).

The Sync button sitting to the right of this section is the recent feature worth calling out:

const syncGroupRates = useCallback(async (opts: { overwrite: boolean; silent?: boolean }) => {
  // GET /groups/{groupId}/pricing
  // If opts.overwrite, replace every rate; otherwise only fill EMPTY fields.
  // If opts.silent, no toast + no spinner on the button.
}, [wizard.groupId]);

Source: WizardShell.tsx:106.

It runs in two modes:

Trigger overwrite silent Behaviour
Group is selected (useEffect WizardShell.tsx:146) false true Silently prefills any empty rate cell from the group's rate sheet. Existing user edits are preserved.
User clicks the Sync button (WizardShell.tsx:640) true false Overwrites every rate, shows a spinner on the button, toasts "Rates synced from group" on success.

Under the hood it calls GET /groups/{groupId}/pricing and maps lineItems.{flight,visa,hotel,groundServices}.{adult,child,infant} onto the corresponding wizard fields. ticketRateflight.adult, visaRatevisa.adult, hotelRatehotel.adult, groundRategroundServices.adult. Child and infant rollup rates are the sum of each service's child/infant tier. There is no meals line in the group rate sheet today — mealsRate stays at whatever the user typed.

Why two modes

The silent auto-fill prevents a blank wizard the moment a group is picked. The manual Sync button is the escape hatch for when a group's rates changed after the operator already entered the wizard — one click refreshes every tier. An italic caption ("Prefilled from group rate sheet — override as needed") appears when a sheet is loaded (WizardShell.tsx:633).

5.2 Child / Infant rate overrides

Two inputs; blank or 0 means "same as adult" (WizardShell.tsx:688).

5.3 Room type, currency, payment policy

  • Room type — single / double / triple / quad / quint / general (WizardShell.tsx:704)
  • Currency — from /admin/currencies or default INR (WizardShell.tsx:720)
  • Payment policypartial / full / advance / on_account. Forced to on_account and disabled when source==='agent' (WizardShell.tsx:735)

5.4 Breakdown preview

WizardShell.tsx:762 — renders an itemised preview (per-adult service breakdown, child × rate, infant × rate) with subtotal, GST line (if enabled), and grand total. For INR, the Indian-words total is shown below (WizardShell.tsx:811).

5.5 GST

WizardShell.tsx:818. Default on. When disabled, gstAmount is cleared. When enabled, Rate (%) and GST Amount are both optional — blank → server falls back to global finance config.

5.6 First Payment (direct bookings only)

Hidden for source==='agent' (WizardShell.tsx:872). Three fields: amount, method (cash / bank transfer / UPI / card / cheque), reference. When submitted, a POST /finance/payments with status: 'verified' follows the booking create (WizardShell.tsx:394).

Validation (step 3)

totalAmount > 0 — i.e. at least one rate is non-zero and there is at least one paying passenger (WizardShell.tsx:251).

6. Step 4 — Review & Confirm

Three summary cards: Source & Group, Passengers, Package & Payment (WizardShell.tsx:907). Clicking Confirm & Create Booking runs handleSubmit.

7. How travellers get added

The wizard treats passenger and customer as separate concepts:

  1. Adding — the passenger count input grows the passengers[] array with a blank emptyPassenger() (wizardTypes.ts:78).
  2. Removing — shrinking the count truncates the array.
  3. Dedup — passport lookup surfaces existing customers; the wizard links them by ID rather than creating a duplicate.
  4. Submit — for each passenger without an existingCustomerId, POST /sales/customers creates a new Customer record (WizardShell.tsx:284). The resulting customerIds[] array is then attached to the booking.

To add passengers after creation, use the Add Passengers dialog from the list or detail view (src/components/bookings/AddPassengersDialog.tsx).

8. Flight linking + multi-city route display

The wizard itself does not link flights — flight assignment is a group-level concern (see Groups §4). After the booking is created and the customer is attached to the group, the per-passenger PassengerFlightsPanel on the detail page (src/components/bookings/PassengerFlightsPanel.tsx) surfaces the group's flights with the multi-city route string built from the block or FIT's legs[] array.

Example display for a 3-leg block: SXR → DEL → JED. See src/pages/groups/Groups.tsx:3579 for the chainLegs() helper that produces the display string.

9. Data flow on submit

handleSubmit (WizardShell.tsx:270) executes in a fixed order. A failure at any step aborts the rest; partial customers may have been created — this is a known trade-off (the customers endpoint is idempotent by passport, so retries are safe).

sequenceDiagram
    participant UI as WizardShell
    participant API as /lib/api
    participant SupaCustomers as POST /sales/customers
    participant SupaBookings as POST /sales/bookings
    participant SupaFinance as POST /finance/payments

    UI->>UI: Build customerIds[] (empty)
    loop For each passenger
        alt Has existingCustomerId
            UI->>UI: Reuse ID
        else New
            UI->>API: apiFetch('/sales/customers', POST, {...})
            API->>SupaCustomers: Insert customer
            SupaCustomers-->>API: { id }
            API-->>UI: customerIds.push(id)
            Note over UI: On 409 duplicate, look up existing passport
        end
    end

    UI->>UI: Build passengers[] with customerIds
    UI->>API: apiFetch('/sales/bookings', POST, { customerId, groupId, agentId?, rates, passengers })
    API->>SupaBookings: Insert booking + BookingPassenger rows
    SupaBookings-->>API: { id: bookingId }
    API-->>UI: { id }

    alt First payment > 0 AND source != 'agent'
        UI->>API: apiFetch('/finance/payments', POST, { bookingId, amount, method, status: 'verified' })
        API->>SupaFinance: Insert Payment + post journal
    end

    UI->>UI: Toast success + navigate('/sales/bookings')

Server-side expectations

  • POST /sales/bookings requires bookings.createrequirePermission('bookings.create') fires before any work (src/lib/api.ts).
  • The handler is idempotent for near-duplicate retries within the same second (double-click guard).
  • On success, Booking.status starts at draft. Ops and Finance approval runs through the Approvals module (/approvals).

10. Permissions

Action Permission
Access the wizard route bookings.create
Create the customer for a new passenger customers.create
Record the first payment finance.create

See docs/PERMISSIONS.md §6.4 for the full matrix.