Skip to content

Dynamic Flight Cache

Pre-fetches and stores flight pricing data for travel products to enable fast price lookups without real-time API calls. Supports hybrid configurations combining international multi-city flights with domestic flights.

  • Displaying flight prices on product listing pages
  • Calculating total trip costs for offers
  • Price trend analysis and monitoring
  • Batch pricing for seasonal campaigns

The system uses PostgreSQL as the cache storage backend (not Redis or file cache). Flight pricing data is stored in normalized tables with relationships between routes, cache entries, and flight segments.

ProductByMarket
└── ProductByMarketFlightConfig (per departure airport)
└── DynamicFlightCacheRoute (unique route + duration)
└── DynamicFlightCache (pricing per date/CUG)
└── DynamicFlightCacheSegment (flight details)
  1. Route Preparation - Creates route definitions from product flight configs
  2. Cache Entry Creation - Creates pending entries for each searchable date
  3. Search Execution - Calls Aerticket API and stores pricing results
  4. Price Retrieval - Queries completed cache entries for display

Routes Table (dynamic_flight_cache_routes)

Section titled “Routes Table (dynamic_flight_cache_routes)”

Defines unique flight routes for caching. Routes are product-agnostic and identified by flight segments + trip duration.

ColumnTypeDescription
idbigintPrimary key
flight_routejsonbArray of segments with origin, destination, day_offset
trip_duration_dayssmallintTotal trip duration
is_domesticbooleanDomestic flight flag
is_multi_citybooleanMulti-city booking flag
segment_indexsmallintOrder in trip (1-based)
is_activebooleanRoute active for caching

Flight Route JSON Structure:

[
{"origin": "BCN", "destination": "CMB", "day_offset": 0},
{"origin": "CMB", "destination": "MLE", "day_offset": 3},
{"origin": "MLE", "destination": "BCN", "day_offset": 10}
]

Indexes:

  • GIN index on flight_route for JSONB queries
  • Unique constraint on (trip_duration_days, is_multi_city)

Cache Entries Table (dynamic_flight_caches)

Section titled “Cache Entries Table (dynamic_flight_caches)”

Stores pricing data for specific dates and routes.

ColumnTypeDescription
idbigintPrimary key
route_idbigintFK to routes table
provider_idbigintFK to providers table
departure_datedateTrip start date
return_datedateTrip end date (nullable)
arrival_datedateArrival date at destination (for land rate matching)
statusvarcharCache entry status
searched_attimestampLast search execution time
cug_typevarcharClosed User Group type
request_identifiervarcharAerticket API flow_id
fare_positionsmallintPrice ranking (1=cheapest)
currencyvarcharPrice currency (EUR)
base_pricenumericBase fare amount
taxnumericTax amount
total_pricenumericTotal price
expires_attimestampCache expiration (nullable)
api_response_time_msintegerAPI response time

Indexes:

  • Composite index on (route_id, departure_date, return_date, total_price)
  • Index on status for batch processing
  • Index on cug_type for filtering
  • Index on expires_at for expiration queries
StatusDescriptionCan Search
pendingEntry created, awaiting searchYes
searchingAPI call in progressNo
completedSuccessfully cachedNo
failedSearch failedYes
expiredCache expiredYes

Segments Table (dynamic_flight_cache_segments)

Section titled “Segments Table (dynamic_flight_cache_segments)”

Stores detailed flight information for cached fares.

ColumnTypeDescription
idbigintPrimary key
flight_cache_idbigintFK to caches table
leg_sequencesmallintLeg number (1=outbound)
segment_numberintegerSegment within leg
flight_numbervarcharAirline + flight number
cabin_classvarcharCabin class
departure_airport_idbigintFK to airports
departure_timetimestampDeparture datetime
arrival_airport_idbigintFK to airports
arrival_timetimestampArrival datetime
operating_carriervarcharOperating airline code
baggage_allowancevarcharBaggage info
duration_minutesintegerFlight duration

Main service for preparing routes and executing searches.

Source: backend/app/Services/Flights/DynamicFlightCachePopulatorService.php

$service = app(DynamicFlightCachePopulatorService::class);
// Prepare routes for a single config
$result = $service->prepareRoutesForConfig($config);
// Returns: routes_created, routes_reused, segments, errors
// Prepare routes for entire product
$result = $service->prepareRoutesForProduct($product);
// Returns: configs_processed, routes_created, routes_reused, errors
// Create pending entries for date range
$result = $service->createCacheEntriesForConfig($config);
// Returns: entries_created, entries_existing, errors
// Combined: prepare routes AND create entries
$result = $service->prepareForConfig($config);
// Execute all pending searches
$result = $service->executeSearches();
// Execute specific entries
$result = $service->executeSearches(collect([$cacheEntry]));
// Returns: total_searches, successful_searches, failed_searches, fares_cached, segments_cached, errors

Handles caching search results from manual flight searches.

Source: backend/app/Services/Flights/DynamicFlightCacheService.php

$service = app(DynamicFlightCacheService::class);
// Cache results from a search (only if matching route exists)
$faresCached = $service->cacheSearchResults($searchRequest, $searchResponse);
  • Maximum fares per search: 5 (positions 1-5 by price)
  • Provider: Aerticket (aerticket code)

Cache entries are created with:

  • CUG Type: ALL (default)
  • Currency: EUR
  • Fare Position: 1-5 (1 = cheapest)

The current implementation stores cache entries without automatic expiration (expires_at = null). Entries are manually refreshed through:

  1. Manual Refresh - Admin triggers re-search
  2. Status-Based Refresh - Re-search pending, failed, or expired entries
// Get non-expired entries
DynamicFlightCache::active()->get();
// Get expired entries
DynamicFlightCache::expired()->get();
// Check if entry is expired
$cache->isExpired(); // false if expires_at is null

The DynamicFlightRefreshSchedule model supports scheduled cache refresh with different triggers:

TriggerDescription
scheduledRegular TTL-based refresh
gap_triggeredPrice gap detection
sales_triggeredSales event
manualOperator-initiated
calibrationAccuracy measurement

Source: backend/app/Models/DynamicFlightRefreshSchedule.php

// Find due schedules
$schedules = DynamicFlightRefreshSchedule::due()->get();
// Execute and mark complete
$schedule->markRunning();
// ... execute refresh ...
$schedule->markExecuted();

Helper methods extract timing information from the first leg segments:

$cache = DynamicFlightCache::find(1);
// First segment departure time
$cache->getOutboundDepartureTime(); // Carbon instance or null
// Last segment arrival time (at destination)
$cache->getOutboundArrivalTime(); // Carbon instance or null
// Total duration in minutes
$cache->getOutboundDurationMinutes(); // int or null
// Formatted duration
$cache->getFormattedOutboundDuration(); // "14h 30m" or null

Source: backend/app/Models/DynamicFlightCache.php

use App\Models\DynamicFlightCache;
use App\Enums\FlightCugType;
$totalPrice = DynamicFlightCache::getTotalFlightPrice(
flightRoute: 'BCN -> CMB, CMB -> MLE, MLE -> BCN',
tripDuration: 11,
tripStartDate: Carbon::parse('2025-03-01'),
cugType: FlightCugType::ALL
);
// Filter by status
DynamicFlightCache::pending()->get();
DynamicFlightCache::completed()->get();
DynamicFlightCache::searchable()->get(); // pending, failed, expired
// Filter by route type
DynamicFlightCache::domestic()->get();
DynamicFlightCache::international()->get();
// Filter by CUG type
DynamicFlightCache::forCug(FlightCugType::ALL)->get();
// Filter by date range
DynamicFlightCache::forDepartureDateRange($from, $to)->get();

Two Filament resources provide access to flight cache data, both under “Inventory Management” navigation group.

URL: /admin/dynamic-flight-caches

Browse and manage individual cache entries (pricing records).

Features:

  • Route tabs: Top 10 most-used routes displayed as quick-filter tabs
  • Product routes tab: Dynamic tab when navigating from ProductByMarket (shows only relevant routes)
  • Default grouping by route with collapsible groups showing route name and duration
  • Searchable multi-select route filter (replaces text input)
  • Execute searches for pending entries

URL Parameters:

  • ?tab=route_{id} - Filter to single route (used for products with one matching route)
  • ?product_routes=12,13,14 - Filter to multiple routes (comma-separated IDs)

Source: backend/app/Filament/Resources/DynamicFlightCaches/

URL: /admin/flight-cache-routes

Browse routes (unique flight patterns) and their associated cache entries.

Features:

  • Route-centric view with flight segments, duration, domestic/international flags
  • View page with infolist and caches relation manager
  • Read-only resource (no create/edit)

Source: backend/app/Filament/Resources/DynamicFlightCacheRoutes/

The ProductByMarket view page includes a “View Cached Flights” action (warning color, server stack icon) that links directly to the DynamicFlightCaches resource filtered by all matching routes.

Visibility: Only shown when at least one active flight config has a matching DynamicFlightCacheRoute.

URL Generation:

  • Single matching route: ?tab=route_{id} for cleaner URL
  • Multiple matching routes: ?product_routes=12,13 with comma-separated IDs

This supports products with multiple departure airports (BCN, MAD) and domestic flights, collecting ALL matching route IDs from active flight configs.

Source: backend/app/Filament/Resources/ProductsByMarket/Pages/ViewProductByMarket.php

Execute Searches (Queue) - Dispatch selected entries to the flight-searches queue:

Source: backend/app/Filament/Resources/DynamicFlightCaches/Actions/ExecuteSearchesBulkAction.php

Execute Single Search - Process individual entry:

Source: backend/app/Filament/Resources/DynamicFlightCaches/Actions/ExecuteSingleSearchAction.php

Prepare Flight Searches - Create routes and entries from product:

Source: backend/app/Filament/Resources/ProductsByMarket/Actions/PrepareFlightSearchesAction.php

Immutable value object representing a multi-segment flight route.

Source: backend/app/ValueObjects/FlightRoute.php

// Create from string
$route = FlightRoute::fromString('BCN -> CMB, CMB -> MLE');
// Create from array
$route = FlightRoute::fromArray([
['origin' => 'BCN', 'destination' => 'CMB', 'day_offset' => 0],
['origin' => 'CMB', 'destination' => 'MLE', 'day_offset' => 3],
]);
// Query methods
$route->getOrigin(); // 'BCN'
$route->getDestination(); // 'MLE'
$route->getSegmentCount(); // 2
$route->isMultiSegment(); // true
$route->containsSegment('BCN', 'CMB'); // true

The FlightRouteConfigGenerator analyzes product template itineraries to generate flight configuration options.

Source: backend/app/Services/Flights/FlightRouteConfigGenerator.php

Option A: Multi-city + Domestic Flights (Hybrid)

Section titled “Option A: Multi-city + Domestic Flights (Hybrid)”

One API call for international multi-city, plus individual calls for domestic flights extracted from itinerary routes with mode='flight'.

Example Route (Thailand trip from Madrid):

International multi-city: MAD → BKK → HKT → MAD (1 API call)
Domestic flights: BKK → CNX, CNX → KBV (2 API calls)
Total: 3 API calls

When to use:

  • Most cost-effective for multi-destination trips
  • Domestic flights are derived from itinerary routes with mode: 'flight'

Route creation:

  • 1 route with is_multi_city=true, is_domestic=false for international
  • N routes with is_multi_city=false, is_domestic=true for domestic

Each leg is searched individually as separate one-way flights.

Example Route:

MAD → BKK (international)
BKK → CNX (domestic)
CNX → KBV (domestic)
KBV → MAD (international)
Total: 4 API calls

Route creation:

  • 1 route per leg with is_multi_city=false
  • is_domestic flag set based on flight_type of each leg

Main airports are prioritized when resolving cities to airports:

// Prioritizes iata_code = iata_city_code
// Bangkok: BKK (Suvarnabhumi) preferred over DMK (Don Mueang)
Airport::where('city', 'ILIKE', $city)
->orderByRaw('CASE WHEN iata_code = iata_city_code THEN 0 ELSE 1 END')
->first();

The generator tracks the final itinerary location for the return flight, regardless of transport mode. If the last leg uses surface transport (ARNK), the return still departs from the actual final location.

Example: Itinerary ends in Phuket via ground transport from Krabi → Return flight departs from HKT (Phuket), not KBV (Krabi).

For processing large batches of searches, use the queue-based approach instead of synchronous execution.

Executes a single flight search asynchronously via the flight-searches queue.

Source: backend/app/Jobs/SearchFlightCacheJob.php

Configuration:

  • Queue: flight-searches
  • Tries: 3
  • Timeout: 90 seconds
  • Backoff: 30s, 60s, 120s

Usage:

use App\Jobs\SearchFlightCacheJob;
// Dispatch single search
SearchFlightCacheJob::dispatch($cacheId, $userId)
->onQueue('flight-searches');
// Dispatch via service (recommended)
$service = app(DynamicFlightCachePopulatorService::class);
$result = $service->dispatchSearchJobs($cacheEntries, $userId);
// Returns: ['dispatched' => int]

Select cache entries in the Dynamic Flight Caches table and use Execute Searches bulk action. This dispatches jobs to the queue with user attribution for activity logging.

All flight search operations are logged to both the database (Activity Logs) and structured logs (Grafana Loki).

Centralized service for consistent logging across all flight search operations.

Source: backend/app/Services/Flights/FlightSearchActivityLogger.php

Log Name: flight-searches

EventTriggerSubject
preparedPrepareFlightSearchesActionProductByMarket
dispatchedExecuteSearchesBulkActionNone (batch operation)
completedSearchFlightCacheJob successDynamicFlightCache
failedSearchFlightCacheJob failureDynamicFlightCache

All logs include:

  • action - Event type (prepare_searches, dispatch_searches, search_completed, search_failed)
  • user_id, user_name - User attribution
  • cache_id, route_id - Resource identifiers
  • route - Flight route string
  • departure_date, return_date - Search dates
use Spatie\Activitylog\Models\Activity;
// Get all flight search activities
Activity::inLog('flight-searches')->get();
// Get activities for specific user
Activity::inLog('flight-searches')
->causedBy($user)
->get();
// Get activities for specific cache entry
Activity::inLog('flight-searches')
->forSubject($cacheEntry)
->get();
// Get recent failures
Activity::inLog('flight-searches')
->where('event', 'failed')
->latest()
->limit(10)
->get();
# All flight search operations
{app="volare"} |= "flight-searches"
# Failed searches
{app="volare"} | json | action="search_failed"
# Searches by user
{app="volare"} | json | user_id="123"
# Specific route
{app="volare"} | json | route=~".*BCN.*CMB.*"
  • Maximum 5 fares cached per search (positions 1-5 by price)
  • Routes are product-agnostic - shared across products with same flight pattern
  • Day offsets determine actual flight dates from trip start date
  • Multi-city routes stored as single combined entry for international legs
  • Domestic routes stored separately for individual pricing
  • CUG type filtering - searches can target specific fare groups
  • Main airports prioritized - BKK over DMK when resolving cities

Issue: DMK selected instead of BKK for Bangkok

Cause: Query returning first match instead of main airport

Solution: Verify airport has matching iata_code and iata_city_code:

SELECT * FROM airports
WHERE city ILIKE 'Bangkok'
ORDER BY CASE WHEN iata_code = iata_city_code THEN 0 ELSE 1 END;

Issue: Return flight departing from Krabi instead of Phuket

Cause: Last destination in itinerary reached by ARNK (surface transport), not tracked

Solution: The generator now always includes final itinerary location regardless of transport mode.

Domestic Flights Not Appearing in Option A

Section titled “Domestic Flights Not Appearing in Option A”

Issue: Option A shows only international legs

Cause: Itinerary routes missing mode: 'flight'

Check: Verify itinerary has routes with mode='flight':

$itinerary = $template->itinerary;
foreach ($itinerary as $item) {
$routes = $item['routes'] ?? [];
foreach ($routes as $route) {
if ($route['mode'] === 'flight') {
// This creates a domestic leg
}
}
}

Issue: Option B creates routes with wrong is_domestic flag

Cause: Flight leg flight_type not set correctly

Check: Verify legs have correct flight_type (‘international’, ‘domestic’, ‘arnk’):

$config->legs->each(fn ($leg) => dump($leg->flight_type));