Products by Market
ProductByMarket represents how a ProductTemplate is sold in a specific market. It connects the base product to a market, adds localized content, and configures flight search parameters.
Purpose
Section titled “Purpose”- Assign products to markets (ES, DE, FR)
- Store market-specific locale (es_ES, ca_ES for Spain)
- Configure departure airports and flight routes
- Define search date ranges for flight caching
- Hold translated content via ProductByMarketTranslation
Data Model
Section titled “Data Model”Table: products_by_market
| Field | Type | Purpose |
|---|---|---|
| product_template_id | FK | Reference to ProductTemplate |
| market_id | FK | Reference to Market |
| locale | varchar | Product locale (e.g., es_ES) |
| sku | varchar | Market SKU: ES-138-10-ES1 |
| status | varchar | draft, active, inactive |
| trip_duration_days | int | Override template duration (API falls back to ProductTemplate.duration when null) |
| sort_order | int | Display ordering (default: 0) |
| search_start_date | date | Flight search period start |
| search_end_date | date | Flight search period end |
| excluded_dates | jsonb | Dates to skip in searches |
Unique Constraint: product_template_id + market_id + locale
Source: backend/app/Models/ProductByMarket.php
SKU Format
Section titled “SKU Format”<MARKET>-<TEMPLATE_ID>-<DAYS>-<LANG><VERSION>Examples:
ES-138-10-ES1- Spain, template 138, 10 days, Spanish, version 1ES-138-10-CA1- Spain, template 138, 10 days, Catalan, version 1DE-138-10-DE1- Germany, template 138, 10 days, German, version 1
Source: backend/app/Models/ProductByMarket.php:268-360
Translations
Section titled “Translations”Translated content is stored in ProductByMarketTranslation:
| Field | Purpose |
|---|---|
| title, subtitle | Localized product name |
| short_description, long_description | Marketing copy |
| itinerary | Translated stop content including per-day days[] (day_label, title, details) |
| url_slug | SEO-friendly URL path |
| meta_title, meta_description | SEO metadata |
Itinerary Translation Merge
Section titled “Itinerary Translation Merge”When publishing to a market or editing translations, the itinerary is merged from the ProductTemplate structure and the translated content using ItinerarySchema::buildTranslationDays(). The merge follows these rules:
- Text (
day_label,title,details) comes from the translation, falling back to the template - Images (
day_image) are never stored in translations — they come from the ProductTemplate only - Activities (
activities[]) come from the SupplierTour itinerary day assignments, not from translations - The API resource (
ProductByMarketResource) merges bothday_imageandactivitiesinto the response at request time
Images and activities are only merged on detail endpoints (by ID, slug, SKU, preview) — the listing endpoint skips them to avoid N+1 queries.
This merge happens in 4 locations: PublishToMarketAction, EditProductByMarket, CreateProductByMarket, and ProductByMarketForm.
AI Translation: Use “Translate from Product” button in admin to auto-translate all fields. The AI prompt handles nested days[] with day_label, title, and details per day.
Source: backend/app/Services/ProductByMarketTranslationService.php, backend/app/Support/ItinerarySchema.php
Flight Configuration
Section titled “Flight Configuration”Each ProductByMarket can have multiple departure airports, each with its own route configuration.
Flight Config Hierarchy
Section titled “Flight Config Hierarchy”ProductByMarket ├── search_start_date, search_end_date (shared) ├── excluded_dates (shared) │ └── ProductByMarketFlightConfig (per departure airport) ├── airport_id (MAD, BCN, etc.) ├── type (multi_city or separate) ├── is_active │ └── ProductByMarketFlightLeg[] (route segments) ├── leg_index ├── origin_airport_id ├── destination_airport_id ├── origin_city ├── destination_city ├── day_offset └── flight_type (international, domestic, arnk)Flight Types
Section titled “Flight Types”| Type | Description | API Calls |
|---|---|---|
| multi_city | Single booking for all legs | 1 per date |
| separate | Individual bookings per leg | N per date |
Leg Types
Section titled “Leg Types”| Type | Description |
|---|---|
| international | Cross-border flight (requires API search) |
| domestic | Same-country flight (requires API search) |
| arnk | Ground transport (no flight search needed) |
Source: backend/app/Models/ProductByMarketFlightConfig.php, backend/app/Models/ProductByMarketFlightLeg.php
Flight Config to Offer Mapping
Section titled “Flight Config to Offer Mapping”When creating an offer, the wizard uses flight config legs to determine required selections:
- International legs are combined into a single multi-city search
- Domestic legs are searched separately (each creates an OfferFlight record)
- ARNK legs are skipped (no flight selection needed)
The leg_index from ProductByMarketFlightLeg carries through to OfferFlight, maintaining the trip sequence.
See Offers for the selection workflow and OfferFlight model.
Search Date Configuration
Section titled “Search Date Configuration”Flight searches are configured with:
- search_start_date - First departure date to search
- search_end_date - Last departure date to search
- excluded_dates - Specific dates to skip (holidays, blackouts)
$product->shouldSearchDate($date); // Combined check$product->isDateInSearchRange($date); // Range check only$product->isDateExcluded($date); // Exclusion check only$product->getSearchableDaysCount(); // Total searchable daysSource: backend/app/Models/ProductByMarket.php:219-265
API Calls Estimation
Section titled “API Calls Estimation”// Per flight config$config->getApiCallsPerDate(); // 1 for multi_city, N for separate$config->getTotalEstimatedApiCalls(); // Days * calls per date
// Per product by market (all configs)$product->getTotalEstimatedApiCalls(); // Sum across all configsPreview URLs
Section titled “Preview URLs”ProductByMarket supports generating preview URLs for viewing draft/inactive products on the frontend.
// Generate signed API URL (expires in 60 minutes by default)$apiUrl = $product->generatePreviewUrl(60);
// Generate frontend URL with embedded preview token$frontendUrl = $product->getFrontendPreviewUrl(60);// Returns: https://frontend.com/es/circuito/slug?preview=<base64_signed_url>The preview URL uses Laravel’s signed URL feature (HMAC-SHA256) to authenticate access without requiring user login.
Source: backend/app/Models/ProductByMarket.php:455-519
Business Rules
Section titled “Business Rules”- One ProductByMarket per template + market + locale combination
- Templates cannot be edited once they have market products (locked)
- Flight configs are generated based on itinerary when product is created
- Search dates are shared across all departure airports
- ARNK legs do not trigger flight searches
Related
Section titled “Related”- Product Templates - Base definitions
- Admin Panel - Filament management
- Flight Search - Search integration
- Database Relationships - Entity diagram and FK references