Skip to content

Offers

An Offer is a bookable travel package that combines one or more flights with a land component (tour). Each offer has a unique SKU, calculated pricing, and lifecycle status.

Offers are created through a 3-step wizard in the admin panel.

Select the product configuration:

  • Product by Market: The product and target market (e.g., “India fun 8 days” for Spain)
  • Tour Rate: Date period with pricing (e.g., Mar 1 - Jul 31, 2026)
  • Room Type: Passenger configuration (e.g., “2 Adults”)

Choose flight source using the toggle:

Cached Flights (default) - Select from pre-cached flight pricing:

  • Filtered to match tour rate period dates
  • Filtered to allowed travel weekdays (e.g., Fri, Sat, Sun)
  • Blackout dates excluded

Manual Entry - Enter externally purchased flights (charter, direct airline bookings):

  • Select departure/arrival airports
  • Enter flight times for outbound and return
  • Optionally specify flight number and notes
  • Creates a FlightBooking with source: manual

Source: backend/app/Enums/FlightBookingSource.php defines api | manual values.

For products with domestic legs (e.g., international flight + internal domestic flight), the wizard shows a unified table with both flight types:

  • International flights: Cross-border flights (main journey)
  • Domestic flights: Same-country internal flights (e.g., Delhi to Goa)

Selection Rules:

  • Only ONE flight per leg type can be selected
  • All required legs must be selected before proceeding
  • Domestic flights are matched to their route based on the flight config
  • The wizard auto-detects leg type from the route’s is_domestic flag

Validation:

  • If multiple flights of the same leg type are selected, an error is shown
  • Missing legs are listed in the notification when selection is incomplete

Source: backend/app/Filament/Resources/Offers/Pages/CreateOffer.php:848-927

Preview offer before creation:

  • All selected flight legs with individual prices
  • Tour Services breakdown (hotels + included activities with prices)
  • Combined total and margin calculation

The offer detail page shows all components organized in sections.

Basic identification and dates:

  • SKU: Unique identifier (e.g., ES-173-10-ES1-MAD-260301-01)
  • Status: Draft or Active
  • Number of Pax: Passenger count (e.g., 2)
  • Departure/Return Dates: Trip window

Shows the price breakdown and margin calculation:

FieldExampleDescription
Flight Price691.99Flight cost for all passengers
Land Price388.00Tour cost for all passengers
Total Base Price1,079.99Combined flight + land
Margin20%Markup percentage
Price per Person649.00Per-pax price (rounded to xx49/xx99)
Final Price1,298.00Total price (per-pax × pax count)

Tour details when a rate is linked:

  • Supplier: Tour provider (e.g., Condor Travel)
  • Tour: Product name (e.g., India fun 8 days)
  • Room Type: Selected configuration
  • Rate Period: Valid date range
  • Travel Window: Allowed weekdays
  • Allotment: Used vs. total capacity

Services breakdown showing individual prices for the selected room type:

Service TypeIconPrice Calculation
Hotel🏨Per-night × nights at location
Activity🎫Per-person × number of travelers

Only included activities appear here. Upsell activities are optional upgrades not in the base price.

Source: backend/app/Filament/Resources/Offers/Schemas/OfferPreviewData.php (getTourServicesBreakdown())

Day-by-day hotel schedule showing:

  • Day number with actual date
  • Destination city
  • Guaranteed hotel (default, included in base price)
  • Upsell hotel (premium upgrade option)

Link to the associated product configuration with market and status.

Flight details from cache or manual booking. For multi-leg offers, shows each leg:

FieldDescription
Flight TypeInternational or Domestic
RouteAirport codes (e.g., MAD-DEL-BKK)
Departure DateFlight departure
PriceCost for this leg
CUG TypeFare category
Sourcecache or manual
Total Flight Price = Sum of all flight leg prices
Land Price = Σ(Hotel Services) + Σ(Included Activity Services)
Base Price = Total Flight Price + Land Price
Raw Total = Base Price × (1 + Margin%)
Per-Pax Price = roundUp(Raw Total / Pax Count) ← rounded to xx49/xx99
Final Price = Per-Pax Price × Pax Count

Prices are rounded per person first, then multiplied back to get the total. This ensures clean per-person prices on the website (e.g., “€2,399/persona” instead of “€2,399.50/persona”).

Example calculation:

Base Price: €3,957.86 (for 2 pax)
With 20% margin: €3,957.86 × 1.20 = €4,749.43 total
Per-pax raw: €4,749.43 / 2 = €2,374.72
Per-pax rounded: €2,399 (rounded to xx99)
Final price: €2,399 × 2 = €4,798

The marketing_price_per_pax field stores the clean per-person price for display on the website.

Pax count is derived from room_type (e.g., “2A” → 2 pax, “2A+1CH” → 3 pax). Defaults to 2 when room_type is null.

ComponentCalculationRoom Type Lookup
Hotel Servicesrate_price × nightsSelected room type (e.g., 2A)
Activity Servicesrate_price × travelersAlways per_person

Note: Only included activities are counted. Upsell activities are optional and not in base price.

Prices round UP to marketing-friendly values ending in 49 or 99:

  • €2,374.72 → €2,399 (per person)
  • €300.00 → €349 (per person)
  • €50.00 → €99 (per person)

Pattern: <ProductByMarket SKU>-<Airport>-<Date>-<Sequence>

Example: ES-173-10-ES1-MAD-260301-01

PartValueMeaning
ES-173-10-ES1ProductByMarket SKUSpain, Product 173, 10 days, template 1
MADAirport IATAMadrid departure
260301YYMMDDMarch 1, 2026
01SequenceFirst offer for this combination
StatusEditableDescription
DraftYes (margin only)Work in progress
ActiveNoPublished and locked

Once active, offers cannot be modified. Create a new offer instead.

Tracks multiple flight legs per offer.

Table: offer_flights

FieldTypeDescription
offer_idFKParent offer
leg_indextinyintOrder within trip (0=intl, 1+=domestic)
flight_typestringinternational or domestic
source_typestringcache or manual
dynamic_flight_cache_idFK (nullable)Cached flight reference
flight_booking_idFK (nullable)Manual booking reference
pricedecimalPrice for this leg

Unique constraint: (offer_id, leg_index) - one flight per leg position.

Source: backend/app/Models/OfferFlight.php

Offer
└── OfferFlight[] (hasMany, ordered by leg_index)
├── DynamicFlightCache (when source_type='cache')
└── FlightBooking (when source_type='manual')
// Offer model
$offer->hasLandComponent(); // Has tour linked?
$offer->getRoomTypeLabel(); // "2 Adults" from "2A"
$offer->getReturnDate(); // Departure + trip duration
$offer->getPaxCount(); // Parse room_type for pax count (default: 2)
$offer->isEditable(); // True if draft
$offer->hasMultipleFlightLegs(); // Has 2+ legs?
$offer->getTotalFlightPrice(); // Sum of all leg prices
$offer->calculateFinalPrice(); // Calculates per-pax rounded price
$offer->marketing_price_per_pax; // Clean per-person price for website
// OfferFlight model
$leg->isInternational(); // flight_type check
$leg->isDomestic(); // flight_type check
$leg->isCachedFlight(); // source_type check
$leg->isManualFlight(); // source_type check
$leg->getFlightSource(); // Returns cache or booking
$leg->getRouteString(); // Route from source