Skip to content

Bookings

Bookings track the full lifecycle from checkout start through payment, admin operations, and trip completion. A Checkout booking is created when checkout begins, enabling funnel drop-off analysis before any payment occurs.

Booking
├── Offer (the package being booked)
├── Client (who is booking)
├── Currency (payment currency)
├── FlightBookings (one-to-many, one per leg)
│ └── booking_id, leg_index, flight_type
├── Passengers (many-to-many via booking_passenger)
│ └── is_lead_passenger (pivot field)
└── Upsells (optional extras)
└── Hotels, Activities, Transfers, Flight Upgrades

Table: bookings

ColumnTypeDescription
idbigintPrimary key
offer_idbigintFK to offers (cascade delete)
client_idbigintFK to clients (nullable, cascade delete)
booking_referencevarchar(16)Unique reference (BK-XXXXXXXX)
statusvarchardraft / checkout / pending_payment / payment_processing / pending_flight_booking / flight_booking_in_progress / flight_booking_failed / flights_confirmed / pending_land_confirmation / confirmed / awaiting_balance / fully_paid / completed / cancelled / expired
checkout_stepvarcharCurrent funnel step (see CheckoutStep enum)
checkout_started_attimestampWhen checkout began (nullable)
checkout_completed_attimestampWhen checkout completed payment (nullable)
checkout_session_idvarcharPHP session ID at checkout start (nullable)
checkout_snapshotjsonStep timestamps and session data snapshot (nullable)
contact_emailvarcharContact email captured at main_contact step (nullable)
contact_phonevarcharContact phone captured at main_contact step (nullable)
contact_namevarcharContact name captured at main_contact step (nullable)
expires_attimestampDraft booking expiration (nullable)
number_of_travelerssmallintPassenger count (default: 2)
total_amountdecimal(10,2)Booking total (base + extras)
base_pricedecimal(10,2)Offer price at booking time (nullable)
extras_pricedecimal(10,2)Sum of upsell prices (default: 0)
currency_idbigintFK to currencies (restrict delete)
payment_method_codevarchar(30)Payment method used at checkout (nullable)
payment_gateway_codevarchar(30)Gateway that processed payment (nullable)
deposit_amountdecimal(12,2)Initial deposit amount (nullable)
balance_amountdecimal(12,2)Remaining balance (nullable)
balance_due_attimestampBalance payment deadline (nullable)
flight_selectionjsonbFlight selection with enriched data - economy or business (nullable)
notestextOptional notes
booked_attimestampWhen booking was made

Indexes:

  • (status, booked_at) - For status-filtered date queries
  • (client_id, status) - For client booking history
  • (balance_due_at) - For upcoming balance deadline queries

Table: booking_passenger

ColumnTypeDescription
booking_idbigintFK to bookings (cascade delete)
passenger_idbigintFK to passengers (cascade delete)
is_lead_passengerbooleanLead passenger flag (default: false)

Constraint: Unique (booking_id, passenger_id)

Table: booking_upsells

Stores selected extras for a booking using a selective FK pattern - only one supplier FK is populated per row (except for flight upgrades which use JSON storage).

ColumnTypeDescription
idbigintPrimary key
booking_idbigintFK to bookings (cascade delete)
typevarchar(20)‘hotel’, ‘activity’, ‘transfer’, ‘flight_upgrade’, ‘insurance’
supplier_hotel_idbigintFK to supplier_hotels (null on delete)
supplier_activity_idbigintFK to supplier_activities (null on delete)
supplier_transfer_idbigintFK to supplier_transfers (null on delete)
supplier_insurance_idbigintFK to supplier_insurances (null on delete)
unit_pricedecimal(10,2)Price per unit in EUR at booking time
quantitysmallintNumber of units (default: 1)
total_pricedecimal(10,2)unit_price × quantity
daysmallintItinerary day number (nullable)
flight_search_paramsjsonFlight details for upgrades (nullable)

Selective FK Pattern: The type column determines which FK is populated:

  • type='hotel'supplier_hotel_id is set, others null
  • type='activity'supplier_activity_id is set, others null
  • type='transfer'supplier_transfer_id is set, others null
  • type='insurance'supplier_insurance_id is set, others null
  • type='flight_upgrade' → No FK, uses flight_search_params JSON instead

Index: (booking_id, type) - For filtering upsells by type

Table: booking_status_transitions

Immutable audit trail for every booking status transition.

ColumnTypeDescription
idbigintPrimary key
booking_idbigintFK to bookings (cascade delete)
from_statusvarchar(30)Previous status (nullable for initial creation)
to_statusvarchar(30)New status
user_idbigintFK to users (nullable, null for system transitions)
notestextOptional operator notes
metadatajsonStructured context (error details, references)
transitioned_attimestampTransition timestamp

Indexes:

  • (booking_id, transitioned_at) - Timeline queries
  • (to_status) - Status reporting queries

Booking and payment statuses are unified in a single BookingStatus enum.

Checkout Flow:
Draft/Checkout -> PendingPayment -> PaymentProcessing
| | |
+-> Cancelled +-> Expired +-> Cancelled
+-> PendingPayment (retry)
Post-Payment Routing:
PaymentProcessing -> PendingFlightBooking -> FlightBookingInProgress -> FlightsConfirmed
| | |
+-> Cancelled +-> FlightBookingFailed+-> PendingLandConfirmation
| |
+-> FlightBookingInProgress
|
+-> AwaitingBalance / FullyPaid
Land-Only Routing:
PaymentProcessing -> PendingLandConfirmation -> AwaitingBalance / FullyPaid
Payment Completion:
AwaitingBalance -> FullyPaid -> Completed
| | |
+-> Cancelled +-> Cancelled+-> (final)
StatusValueDescription
DraftdraftLegacy pre-checkout status kept for compatibility in transition rules
CheckoutcheckoutActive checkout session in progress
PendingPaymentpending_paymentCheckout finalized, waiting for payment confirmation
PaymentProcessingpayment_processingPayment confirmation in progress
PendingFlightBookingpending_flight_bookingPayment succeeded and booking includes flights; waiting for admin to trigger flight booking
FlightBookingInProgressflight_booking_in_progressFlight booking job running
FlightBookingFailedflight_booking_failedLast flight booking attempt failed; retry available
FlightsConfirmedflights_confirmedFlights booked successfully; waiting for land service confirmation
PendingLandConfirmationpending_land_confirmationWaiting for admin confirmation of land services
ConfirmedconfirmedAll booked services confirmed (legacy, not actively used in current flow)
AwaitingBalanceawaiting_balanceDeposit paid, balance outstanding
FullyPaidfully_paidFull payment received
CompletedcompletedTrip completed (final state)
CancelledcancelledBooking cancelled (final state)
ExpiredexpiredDraft booking expired without payment (final state)

Transition rules: See BookingStatus::canTransitionTo() for allowed transitions.

Source: backend/app/Enums/BookingStatus.php

Checkout-flow bookings (draft / checkout) track which checkout step each user reaches, enabling drop-off analysis.

Steps are ordered by CheckoutStep::ordinal():

StepValueOrdinalDescription
Flightsflights0Initial step (set on checkout booking creation)
Hotelshotels1Hotel upgrade selection
Activitiesactivities2Activity add-on selection
Transferstransfers3Transfer selection
MainContactmain_contact4Booking contact person data
Travelerstravelers5Passenger personal data
Summarysummary6Review and payment
Completedcompleted7Payment successful (terminal state)

Source: backend/app/Enums/CheckoutStep.php

The checkout_step column always reflects the furthest step reached (forward-only). A separate checkout_snapshot['current_step'] field tracks the user’s actual position, including backward navigation.

Two mechanisms update checkout funnel data:

  1. trackStep() — Called via the internal API (PATCH /api/internal/bookings/{id}/checkout-step) during Astro SSR page rendering. Advances checkout_step forward-only. Always updates checkout_snapshot['current_step'] to the actual step being viewed (even on backward navigation). Records a timestamp in step_timestamps if one doesn’t already exist for that step.
  2. advanceStep() — Called on PUT selection endpoints (hotels, activities, transfers). Records a step timestamp in checkout_snapshot, syncs pricing from the session, and advances checkout_step forward-only as a fallback when SSR tracking fails. Only called on forward navigation (controlled by the advance request flag, defaults to true). Flights, contact, and travelers endpoints always call advanceStep.

Contact capture happens at the main_contact step via captureContact(), which also extends the checkout booking expiration from booking.expiration.anonymous_days (10) to booking.expiration.lead_days (30).

On successful checkout finalization (before charge confirmation), promoteToPendingPayment() transitions the booking from checkout flow status to pending_payment, sets checkout_step to Completed, and records checkout_completed_at.

Source: backend/app/Services/Checkout/BookingFunnelService.php

The checkout_snapshot JSON stores step timestamps, pricing, and the user’s current position:

{
"step_timestamps": {
"flights": "2026-02-15T10:30:00+00:00",
"hotels": "2026-02-15T10:32:15+00:00",
"activities": "2026-02-15T10:35:00+00:00"
},
"current_step": "activities",
"base_price": 2499.00,
"extras_price": 150.00,
"total_price": 2649.00
}

Checkout-flow bookings expire automatically:

  • Anonymous (no contact): booking.expiration.anonymous_days (10 days)
  • With contact: booking.expiration.lead_days (30 days)

Config: backend/config/booking.php

Auto-generated on creation with format BK-XXXXXXXX (8 uppercase alphanumeric characters).

// Generation logic in Booking model
$reference = 'BK-' . strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));

Source: backend/app/Models/Booking.php

$booking->offer; // BelongsTo Offer
$booking->client; // BelongsTo Client
$booking->currency; // BelongsTo Currency
$booking->flightBookings; // HasMany FlightBooking (one per leg)
$booking->flightBooking; // HasOne FlightBooking (oldest, backward-compat)
$booking->passengers; // BelongsToMany Passenger (with pivot)
$booking->leadPassenger(); // Single Passenger or null
$booking->upsells; // HasMany BookingUpsell
$booking->payments; // HasMany Payment
$booking->statusTransitions; // HasMany BookingStatusTransition
$booking->latestTransition; // HasOne BookingStatusTransition
$booking->canBeCancelled(); // true if status allows cancellation
$booking->canBeConfirmed(); // true if PendingPayment
$booking->canBeCompleted(); // true if Confirmed or FullyPaid
$booking->requiresBalancePayment(); // true if AwaitingBalance
$booking->getSuccessfulPayments(); // Collection of successful Payment records

Resource: backend/app/Filament/Resources/Bookings/BookingResource.php

PageDescription
ListView all bookings with filters
CreateNew booking with offer/client/passenger selection
ViewBooking details infolist with flight and checkout funnel sections
EditStatus changes only

Permissions: ViewBooking, CreateBooking, UpdateBooking, DeleteBooking

  1. Select an active Offer
  2. Select a Client
  3. Attach passengers from client’s saved passengers
  4. Designate one lead passenger
  5. Select optional extras (hotels, activities, transfers)
  6. Review pricing (base + extras = total)

Only the status field can be modified after creation, following the allowed transitions.

Shows checkout progression for bookings with tracking data. Includes:

  • Furthest Step badge with color coding (the highest step reached, from checkout_step)
  • Checkout started/elapsed time with completion timestamp
  • Draft expiration date
  • Visual step timeline via custom CheckoutFunnelEntry component — shows “(active)” on the step the user is currently viewing (from checkout_snapshot['current_step']), while reached/completed steps are determined by furthest step
  • Contact info row (name, email, phone) for quick agent outreach on abandoned drafts

Only visible when checkout_started_at is set or checkout_step is not Completed.

Source: backend/app/Filament/Resources/Bookings/Schemas/CheckoutFunnelSection.php

The View page includes a collapsible Flight section that displays cabin class, outbound/inbound legs (route, date, times, flight numbers, airlines, stops), and FlightBooking statuses (booking, ticket, source). If any FlightBooking is linked, a header action links to its detail page.

Per-Leg Breakdown: When flight_selection contains a legs array, a “Per-Leg Breakdown” subsection renders an individual Section per leg with heading “Leg N: Type — Route”. International and domestic legs have distinct layouts:

  • International legs: Summary row (type, cabin, route, raw fare price per pax) plus outbound/inbound direction rows showing schedule (date + departure/arrival times), flight numbers with airline names, and stops with stopover IATA codes
  • Domestic legs: Summary row (type, cabin, route, raw fare price per pax) plus a one-way direction row showing schedule, flight numbers with airline names, and stops with stopover IATA codes
  • Direction rows and fare price are hidden when the leg is booked — a “View” header action links to the FlightBooking detail page instead
  • If not booked: “In progress…” badge when flight_booking_in_progress, or error message from the last failed transition metadata (matched by leg_index)
  • Per-leg actions: Each unbooked leg has its own “Book Flight” or “Retry” header action that dispatches a single CreateCheckoutFlightBookingJob for that leg

Fallback: Economy bookings without a legs array show a “Flight Details” section with outbound/inbound direction rows (route, schedule, flights, stops) instead of per-leg breakdown.

Flight data is resolved from three sources in priority order via BookingFlightData:

  1. flight_selection JSON (checkout bookings) - richest data with enriched airports, airline names (preferred over airline codes), and per-leg breakdown
  2. FlightBooking model (admin-created or manually linked) - uses booking fields directly
  3. Offer flight cache (legacy/admin fallback) - reconstructs from DynamicFlightCache segments

This means every booking shows flight details regardless of how it was created.

Source: backend/app/Filament/Resources/Bookings/Schemas/BookingFlightSection.php, backend/app/Filament/Resources/Bookings/Schemas/BookingFlightData.php

Bookings can include optional extras (upsells) from the tour itinerary. These are fetched and priced by BookingUpsellPriceService.

TypePricingDescription
HotelFlat per roomPrice difference between guaranteed and upgrade hotel
ActivityPer personUnit price × number of travelers
TransferFlat per tripSingle price for the transfer
InsurancePer policyTravel insurance policy price
Flight UpgradePer personBusiness class upgrade cost × number of travelers

Flight upgrades store selection data in flight_search_params JSON for later verification:

{
"outbound": {
"departure_date": "2026-02-15",
"departure_time": "09:10",
"arrival_time": "15:15",
"departure_airport": "MAD",
"arrival_airport": "NBO",
"flight_numbers": ["EK123", "EK456"],
"airlines": [{"code": "EK", "name": "Emirates"}],
"stops": 1,
"stopover_airports": ["DXB"],
"arrivalDayOffset": 1
},
"inbound": {
"departure_date": "2026-02-22",
"departure_time": "08:00",
"arrival_time": "14:30",
"departure_airport": "NBO",
"arrival_airport": "MAD",
"flight_numbers": ["EK789"],
"airlines": [{"code": "EK", "name": "Emirates"}],
"stops": 0,
"stopover_airports": [],
"arrivalDayOffset": 0
},
"apihubflowid": "4844a05b-3ce2-45b1-b907-1669e574e94c"
}

This data enables:

  • Re-searching to verify flight availability
  • Historical reference for flight details
  • Tracking via apihubflowid (Aerticket API Hub flow ID for booking lookup)

Upsells are sourced from the offer’s tour itinerary:

  1. Service fetches itinerary days with their optional hotels, activities, transfers
  2. Prices are calculated using rate periods covering the departure date
  3. Only upsells with valid rates for the date are shown

Source: backend/app/Services/Checkout/BookingUpsellPriceService.php

Source: backend/app/Models/BookingUpsell.php

$upsell->booking; // BelongsTo Booking
$upsell->hotel; // BelongsTo SupplierHotel (if type=hotel)
$upsell->activity; // BelongsTo SupplierActivity (if type=activity)
$upsell->transfer; // BelongsTo SupplierTransfer (if type=transfer)
$upsell->insurance; // BelongsTo SupplierInsurance (if type=insurance)
$upsell->getItem(); // Returns supplier entity (null for flight_upgrade)
$upsell->getName(); // Returns display name or flight summary
$upsell->getFlightSummary(); // Returns "MAD → NBO / NBO → MAD" for flights

Flight upgrade display: getName() returns "Business Class Upgrade". Route details are available via getFlightSummary() which returns route summary like MAD → NBO / JNB → MAD (supports open-jaw flights where return airport differs).

When a customer completes checkout payment, BookingFinalizationService populates the booking:

  1. Passengers - Created from checkout session’s traveler_data, attached via booking_passenger
  2. Flight Selection - Economy and business flights stored in flight_selection column with enriched airport data
  3. Upsells - Created from session’s hotel/activity/transfer/flight selections using session’s actual_pax_count
  4. Flight Upgrades - If business class selected (business_extra_price_per_person > 0), stores flight data in booking_upsells.flight_search_params

Variable passenger count: The finalization service uses actual_pax_count from the session for calculating activity and flight upgrade prices. Hotel upsells ALWAYS use 2A pricing regardless of actual_pax_count.

Source: backend/app/Services/Booking/BookingFinalizationService.php

This happens automatically after successful payment in PaymentController::confirm(). After finalization completes, the checkout session is cleared via CheckoutSessionService::clear() to prevent session reuse.

After successful payment, booking status is routed by PaymentService::updateBookingPaymentStatus():

  • Flight bookings (cabin_class = ECONOMY / BUSINESS) transition to pending_flight_booking
  • Land-only bookings transition to pending_land_confirmation

Flight bookings are operator-controlled in Filament. Each flight leg is booked independently:

  1. pending_flight_booking -> Book All Flights (multi-leg) or Book Flight (single-leg) dispatches one CreateCheckoutFlightBookingJob per unbooked leg and transitions to flight_booking_in_progress
  2. Each job creates its own FlightBooking record with independent PNR, status, and retrieve flow
  3. Only when ALL legs have FlightBooking records does the booking transition to flights_confirmed
  4. Job failure transitions to flight_booking_failed with structured error metadata including leg_index (see below)
  5. flight_booking_failed -> Retry All Flights (multi-leg) or Retry (per-leg) re-dispatches jobs for unbooked legs only
  6. Admin clicks Confirm Services → booking moves to awaiting_balance (if balance due) or fully_paid (if paid in full)

All transitions are recorded in booking_status_transitions and shown in the booking Status Timeline on the view page.

Failure metadata structure:

When a flight booking fails, the metadata JSON on the transition includes structured error context from CheckoutFlightBookingException::$context, spread alongside the base fields:

  • error_type - Failure classification from CheckoutFlightBookingFailure enum (e.g., no_matching_fare, verify_failed, booking_failed)
  • message - Human-readable error reason (displayed in the status timeline UI)
  • leg_index - Which flight leg failed (0-based)
  • attempt - Which job attempt failed
  • sub_type - Aerticket-specific error category (e.g., price_change, fare_expired, timeout, api_error)
  • Additional fields vary by sub_type: original_price/new_price/currency for price changes, fare_id for verify/booking errors, provider_errors for API failures, validation_errors for validation failures

Status timeline display: The Blade component (booking-status-timeline-entry.blade.php) renders the message field from failed transition metadata directly under the status badge, so admins can see the failure reason without opening logs.

Source: backend/app/Services/Booking/BookingStatusService.php, backend/app/Jobs/CreateCheckoutFlightBookingJob.php, backend/app/Filament/Resources/Bookings/Pages/ViewBooking.php

Related: Checkout API - Payment Flow

Flight selections (both economy and business) are stored directly on the booking with enriched data:

{
"cabin_class": "BUSINESS",
"outbound": {
"departure_date": "2026-03-15",
"departure_time": "10:30",
"arrival_time": "22:45",
"departure_airport": { "iata_code": "MAD", "city": "Madrid" },
"arrival_airport": { "iata_code": "CMB", "city": "Colombo" },
"flight_numbers": ["UX1234", "EK456"],
"airlines": [{"code": "UX", "name": "Air Europa"}, {"code": "EK", "name": "Emirates"}],
"stops": 1,
"stopover_airports": [{ "iata_code": "DXB", "city": "Dubai" }],
"total_duration_minutes": 735,
"arrivalDayOffset": 1
},
"inbound": { "..." : "..." },
"business_extra_price_per_person": 450.00,
"legs": [
{
"leg_index": 0,
"type": "international",
"cabin_class": "BUSINESS",
"fare_price": 1250.00,
"flight_numbers": ["UX1234", "EK456"],
"route": "MAD-CMB-MAD"
},
{
"leg_index": 1,
"type": "domestic",
"cabin_class": "ECONOMY",
"fare_price": 85.00,
"cached_price": 85.00,
"flight_numbers": ["1582"],
"route": "CMB-TRZ",
"departure_date": "2026-03-16",
"departure_time": "14:30",
"arrival_time": "15:45",
"airline_names": ["SriLankan Airlines"],
"stopover_airports": []
}
]
}

The service enriches raw session data by:

  • Looking up airport city names from database
  • Resolving airline IATA codes to {code, name} objects
  • Calculating total flight duration (handles overnight flights)
  • Computing arrivalDayOffset (days between departure and arrival, e.g. 1 for overnight flights)
  • Structuring stopovers with city information

Both economy and business class selections include a per-leg breakdown in the legs array. Business legs are built by CheckoutSessionService::enrichWithLegsBreakdown() and economy legs by buildEconomyLegs(). Each leg records its type (international/domestic), leg_index, cabin class, fare_price (per-person), flight_numbers, and route. Domestic legs additionally include departure_time, arrival_time, airline_names (full names), and stopover_airports (IATA codes) extracted from cached flight segments during search. Economy legs use actual API-searched prices stored in session metadata (international_fare_price and per-leg searched_price) rather than deriving by subtraction, with fallback to the old subtraction method for backward compatibility. Domestic legs that fell back to ECONOMY (no business fare available) include a cached_price field with the economy search price. Per-leg fare_price is used during checkout flight booking for price matching (fare_price * pax_count compared to Aerticket fare->totalPrice).

Business class selections are also stored in the BookingUpsell record (type flight_upgrade) for financial tracking.

Bookings automatically trigger client.updateBookingAnalytics() on save to keep client statistics current.