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 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.
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