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.
Data Model
Section titled “Data Model”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 UpgradesDatabase Schema
Section titled “Database Schema”Table: bookings
| Column | Type | Description |
|---|---|---|
| id | bigint | Primary key |
| offer_id | bigint | FK to offers (cascade delete) |
| client_id | bigint | FK to clients (nullable, cascade delete) |
| booking_reference | varchar(16) | Unique reference (BK-XXXXXXXX) |
| status | varchar | draft / 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_step | varchar | Current funnel step (see CheckoutStep enum) |
| checkout_started_at | timestamp | When checkout began (nullable) |
| checkout_completed_at | timestamp | When checkout completed payment (nullable) |
| checkout_session_id | varchar | PHP session ID at checkout start (nullable) |
| checkout_snapshot | json | Step timestamps and session data snapshot (nullable) |
| contact_email | varchar | Contact email captured at main_contact step (nullable) |
| contact_phone | varchar | Contact phone captured at main_contact step (nullable) |
| contact_name | varchar | Contact name captured at main_contact step (nullable) |
| expires_at | timestamp | Draft booking expiration (nullable) |
| number_of_travelers | smallint | Passenger count (default: 2) |
| total_amount | decimal(10,2) | Booking total (base + extras) |
| base_price | decimal(10,2) | Offer price at booking time (nullable) |
| extras_price | decimal(10,2) | Sum of upsell prices (default: 0) |
| currency_id | bigint | FK to currencies (restrict delete) |
| payment_method_code | varchar(30) | Payment method used at checkout (nullable) |
| payment_gateway_code | varchar(30) | Gateway that processed payment (nullable) |
| deposit_amount | decimal(12,2) | Initial deposit amount (nullable) |
| balance_amount | decimal(12,2) | Remaining balance (nullable) |
| balance_due_at | timestamp | Balance payment deadline (nullable) |
| flight_selection | jsonb | Flight selection with enriched data - economy or business (nullable) |
| notes | text | Optional notes |
| booked_at | timestamp | When 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
| Column | Type | Description |
|---|---|---|
| booking_id | bigint | FK to bookings (cascade delete) |
| passenger_id | bigint | FK to passengers (cascade delete) |
| is_lead_passenger | boolean | Lead 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).
| Column | Type | Description |
|---|---|---|
| id | bigint | Primary key |
| booking_id | bigint | FK to bookings (cascade delete) |
| type | varchar(20) | ‘hotel’, ‘activity’, ‘transfer’, ‘flight_upgrade’, ‘insurance’ |
| supplier_hotel_id | bigint | FK to supplier_hotels (null on delete) |
| supplier_activity_id | bigint | FK to supplier_activities (null on delete) |
| supplier_transfer_id | bigint | FK to supplier_transfers (null on delete) |
| supplier_insurance_id | bigint | FK to supplier_insurances (null on delete) |
| unit_price | decimal(10,2) | Price per unit in EUR at booking time |
| quantity | smallint | Number of units (default: 1) |
| total_price | decimal(10,2) | unit_price × quantity |
| day | smallint | Itinerary day number (nullable) |
| flight_search_params | json | Flight details for upgrades (nullable) |
Selective FK Pattern: The type column determines which FK is populated:
type='hotel'→supplier_hotel_idis set, others nulltype='activity'→supplier_activity_idis set, others nulltype='transfer'→supplier_transfer_idis set, others nulltype='insurance'→supplier_insurance_idis set, others nulltype='flight_upgrade'→ No FK, usesflight_search_paramsJSON instead
Index: (booking_id, type) - For filtering upsells by type
Table: booking_status_transitions
Immutable audit trail for every booking status transition.
| Column | Type | Description |
|---|---|---|
| id | bigint | Primary key |
| booking_id | bigint | FK to bookings (cascade delete) |
| from_status | varchar(30) | Previous status (nullable for initial creation) |
| to_status | varchar(30) | New status |
| user_id | bigint | FK to users (nullable, null for system transitions) |
| notes | text | Optional operator notes |
| metadata | json | Structured context (error details, references) |
| transitioned_at | timestamp | Transition timestamp |
Indexes:
(booking_id, transitioned_at)- Timeline queries(to_status)- Status reporting queries
Status Lifecycle
Section titled “Status Lifecycle”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)| Status | Value | Description |
|---|---|---|
| Draft | draft | Legacy pre-checkout status kept for compatibility in transition rules |
| Checkout | checkout | Active checkout session in progress |
| PendingPayment | pending_payment | Checkout finalized, waiting for payment confirmation |
| PaymentProcessing | payment_processing | Payment confirmation in progress |
| PendingFlightBooking | pending_flight_booking | Payment succeeded and booking includes flights; waiting for admin to trigger flight booking |
| FlightBookingInProgress | flight_booking_in_progress | Flight booking job running |
| FlightBookingFailed | flight_booking_failed | Last flight booking attempt failed; retry available |
| FlightsConfirmed | flights_confirmed | Flights booked successfully; waiting for land service confirmation |
| PendingLandConfirmation | pending_land_confirmation | Waiting for admin confirmation of land services |
| Confirmed | confirmed | All booked services confirmed (legacy, not actively used in current flow) |
| AwaitingBalance | awaiting_balance | Deposit paid, balance outstanding |
| FullyPaid | fully_paid | Full payment received |
| Completed | completed | Trip completed (final state) |
| Cancelled | cancelled | Booking cancelled (final state) |
| Expired | expired | Draft booking expired without payment (final state) |
Transition rules: See BookingStatus::canTransitionTo() for allowed transitions.
Source: backend/app/Enums/BookingStatus.php
Checkout Funnel Tracking
Section titled “Checkout Funnel Tracking”Checkout-flow bookings (draft / checkout) track which checkout step each user reaches, enabling drop-off analysis.
Funnel Steps
Section titled “Funnel Steps”Steps are ordered by CheckoutStep::ordinal():
| Step | Value | Ordinal | Description |
|---|---|---|---|
| Flights | flights | 0 | Initial step (set on checkout booking creation) |
| Hotels | hotels | 1 | Hotel upgrade selection |
| Activities | activities | 2 | Activity add-on selection |
| Transfers | transfers | 3 | Transfer selection |
| MainContact | main_contact | 4 | Booking contact person data |
| Travelers | travelers | 5 | Passenger personal data |
| Summary | summary | 6 | Review and payment |
| Completed | completed | 7 | Payment successful (terminal state) |
Source: backend/app/Enums/CheckoutStep.php
How Tracking Works
Section titled “How Tracking Works”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:
trackStep()— Called via the internal API (PATCH /api/internal/bookings/{id}/checkout-step) during Astro SSR page rendering. Advancescheckout_stepforward-only. Always updatescheckout_snapshot['current_step']to the actual step being viewed (even on backward navigation). Records a timestamp instep_timestampsif one doesn’t already exist for that step.advanceStep()— Called on PUT selection endpoints (hotels, activities, transfers). Records a step timestamp incheckout_snapshot, syncs pricing from the session, and advancescheckout_stepforward-only as a fallback when SSR tracking fails. Only called on forward navigation (controlled by theadvancerequest flag, defaults totrue). Flights, contact, and travelers endpoints always calladvanceStep.
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
Checkout Snapshot
Section titled “Checkout Snapshot”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 Expiration
Section titled “Checkout Expiration”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
Booking Reference
Section titled “Booking Reference”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
Relationships
Section titled “Relationships”$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 BookingStatusTransitionStatus Methods
Section titled “Status Methods”$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 recordsFilament Admin
Section titled “Filament Admin”Resource: backend/app/Filament/Resources/Bookings/BookingResource.php
| Page | Description |
|---|---|
| List | View all bookings with filters |
| Create | New booking with offer/client/passenger selection |
| View | Booking details infolist with flight, payment, and checkout funnel sections |
| Edit | Status changes only |
Permissions: ViewBooking, CreateBooking, UpdateBooking, DeleteBooking
Creating a Booking
Section titled “Creating a Booking”- Select an active Offer
- Select a Client
- Attach passengers from client’s saved passengers
- Designate one lead passenger
- Select optional extras (hotels, activities, transfers)
- Review pricing (base + extras = total)
Edit Restrictions
Section titled “Edit Restrictions”Only the status field can be modified after creation, following the allowed transitions.
View Page - Checkout Funnel Section
Section titled “View Page - Checkout Funnel Section”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
CheckoutFunnelEntrycomponent — shows “(active)” on the step the user is currently viewing (fromcheckout_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
View Page - Flight Section
Section titled “View Page - Flight Section”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 byleg_index) - Per-leg actions: Each unbooked leg has its own “Book Flight” or “Retry” header action that dispatches a single
CreateCheckoutFlightBookingJobfor 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.
View Page - Payment Section
Section titled “View Page - Payment Section”Shows payment summary and individual payment records. Visible when deposit_amount is set or any Payment records exist for the booking.
Summary grid (split-payment bookings only): Deposit amount, balance due date, total paid (computed from succeeded payments only), and remaining amount with color indicators (green when fully paid, red when outstanding).
Payments table: Lists all payment records regardless of status (Succeeded, Pending, Failed, Refunded, Cancelled) with type badge, amount with currency, status badge, gateway name (linked to gateway dashboard when available), card info, and charge date.
Total paid is computed from the eager-loaded payments collection filtered by PaymentStatus::Succeeded, avoiding extra queries.
Source: backend/app/Filament/Resources/Bookings/Schemas/BookingPaymentSection.php
Flight data is resolved from three sources in priority order via BookingFlightData:
flight_selectionJSON (checkout bookings) - richest data with enriched airports, airline names (preferred over airline codes), and per-leg breakdownFlightBookingmodel (admin-created or manually linked) - uses booking fields directly- Offer flight cache (legacy/admin fallback) - reconstructs from
DynamicFlightCachesegments
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
Upsells
Section titled “Upsells”Bookings can include optional extras (upsells) from the tour itinerary. These are fetched and priced by BookingUpsellPriceService.
Upsell Types
Section titled “Upsell Types”| Type | Pricing | Description |
|---|---|---|
| Hotel | Flat per room | Price difference between guaranteed and upgrade hotel |
| Activity | Per person | Unit price × number of travelers |
| Transfer | Flat per trip | Single price for the transfer |
| Insurance | Per policy | Travel insurance policy price |
| Flight Upgrade | Per person | Business class upgrade cost × number of travelers |
Flight Upgrade Data
Section titled “Flight Upgrade Data”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)
Price Calculation
Section titled “Price Calculation”Upsells are sourced from the offer’s tour itinerary:
- Service fetches itinerary days with their optional hotels, activities, transfers
- Prices are calculated using rate periods covering the departure date
- Only upsells with valid rates for the date are shown
Source: backend/app/Services/Checkout/BookingUpsellPriceService.php
BookingUpsell Model
Section titled “BookingUpsell Model”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 flightsFlight 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).
Checkout Finalization
Section titled “Checkout Finalization”When a customer completes checkout payment, BookingFinalizationService populates the booking:
- Passengers - Created from checkout session’s
traveler_data, attached viabooking_passenger - Flight Selection - Economy and business flights stored in
flight_selectioncolumn with enriched airport data - Upsells - Created from session’s hotel/activity/transfer/flight selections using session’s
actual_pax_count - Flight Upgrades - If business class selected (
business_extra_price_per_person > 0), stores flight data inbooking_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.
Status Routing After Payment
Section titled “Status Routing After Payment”After successful payment, booking status is routed by PaymentService::updateBookingPaymentStatus():
- Flight bookings (
cabin_class=ECONOMY/BUSINESS) transition topending_flight_booking - Land-only bookings transition to
pending_land_confirmation
Admin-Driven Flight Booking (Per-Leg)
Section titled “Admin-Driven Flight Booking (Per-Leg)”Flight bookings are operator-controlled in Filament. Each flight leg is booked independently:
pending_flight_booking-> Book All Flights (multi-leg) or Book Flight (single-leg) dispatches oneCreateCheckoutFlightBookingJobper unbooked leg and transitions toflight_booking_in_progress- Each job creates its own
FlightBookingrecord with independent PNR, status, and retrieve flow - Only when ALL legs have
FlightBookingrecords does the booking transition toflights_confirmed - Job failure transitions to
flight_booking_failedwith structured error metadata includingleg_index(see below) flight_booking_failed-> Retry All Flights (multi-leg) or Retry (per-leg) re-dispatches jobs for unbooked legs only- Admin clicks Confirm Services → booking moves to
awaiting_balance(if balance due) orfully_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 fromCheckoutFlightBookingFailureenum (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 failedsub_type- Aerticket-specific error category (e.g.,price_change,fare_expired,timeout,api_error)- Additional fields vary by
sub_type:original_price/new_price/currencyfor price changes,fare_idfor verify/booking errors,provider_errorsfor API failures,validation_errorsfor 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 Selection Storage
Section titled “Flight Selection Storage”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.1for 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.
Client Analytics
Section titled “Client Analytics”Bookings automatically trigger client.updateBookingAnalytics() on save to keep client statistics current.
Related
Section titled “Related”- Checkout API - Checkout flow, funnel tracking internal API, and finalization
- Offers - Offer creation and pricing
- Product Relationships - Product entity structure
- Suppliers - Supplier hotels, activities, transfers