Skip to content

Product Templates

ProductTemplate defines what a trip IS - its route, structure, and identity. Content is written in a source locale and can be translated when creating market-specific versions.

  • Define trip itinerary with POI-based locations and multi-segment routes
  • Store base marketing content
  • Calculate trip duration automatically
  • Provide foundation for market-specific products

Table: product_templates

FieldTypePurpose
titlevarcharProduct name
subtitlevarcharMarketing tagline
skuvarcharAuto-generated: <ID>-<DAYS>
source_localevarcharContent language (e.g., en_US)
itineraryjsonbArray of arrival + day items with POIs and routes
durationintTrip length in days (auto-calculated)
highlightsjsonbKey selling points
categoriesjsonbProduct categories (beach, luxury, etc.)
tcai_profile_idFK (nullable)Optional AI personality profile

Source: backend/app/Models/ProductTemplate.php

The itinerary uses a structured format with an arrival item as the entry point followed by stop items with locations, routes, and per-day content.

[
{
"type": "arrival",
"arrival_poi_id": 123
},
{
"locations": [456, 789],
"nights": 2,
"routes": [
{"from": "Nairobi", "to": "Amboseli", "from_id": 456, "to_id": 789, "mode": "arnk"}
],
"days": [
{"day_label": "Day 1 - Amboseli", "title": "Safari Adventure", "details": "Game drives...", "day_image": "path/to/image.jpg"},
{"day_label": "Day 2 - Amboseli", "title": "Morning Safari", "details": "Early drive...", "day_image": null}
]
},
{
"locations": [789, 456, 321],
"nights": 0,
"routes": [
{"from": "Amboseli", "to": "Nairobi", "from_id": 789, "to_id": 456, "mode": "arnk"},
{"from": "Nairobi", "to": "Lake Naivasha", "from_id": 456, "to_id": 321, "mode": "flight"}
],
"days": [
{"day_label": "Day 3", "title": "Transfer & Flight", "details": "Morning drive to Nairobi...", "day_image": null}
]
}
]

Each stop contains a days[] array with per-day content. The array length follows the rule: days.length = max(nights, 1) — departure stops (0 nights) still get 1 day entry.

FieldTypePurpose
day_labelstringDisplay label (e.g., “Day 1 - Medellín”)
titlestringDay title
detailsstringDay description/activities
day_imagestring or nullImage path (stored on template only, not in translations)

When the nights value changes in the admin form, the days array auto-resizes: growing appends empty entries, shrinking truncates from the end. Existing day content is preserved by index.

Legacy support: Old itineraries without days[] are normalized on load. Stop-level title, details, and day_image fields seed the first day entry.

TypeFieldsPurpose
Arrivaltype, arrival_poi_idEntry point to the trip (first item)
Stoplocations, nights, routes, days[]Journey stop with per-day content

Routes use the RouteMode enum to distinguish transport types:

ModeValueDescription
FlightflightAir travel between locations
SurfacearnkGround transport (road, rail, etc.)

The arnk value follows airline terminology for “Arrival Not Known” - used for surface segments in multi-city itineraries.

Source: backend/app/Enums/RouteMode.php

ItinerarySchema is the single source of truth for itinerary structure. Use it when working with itinerary data programmatically.

use App\Support\ItinerarySchema;
// Create items
ItinerarySchema::createArrivalItem($poiId);
ItinerarySchema::createDayItem($locationIds, $nights, $routes, $title, $details);
ItinerarySchema::createRoute($from, $to, RouteMode::Flight, $fromId, $toId);
// Per-day content
ItinerarySchema::createDayEntry($dayLabel, $title, $details, $dayImage);
ItinerarySchema::buildDaysArray($nights, $existingDays); // Resize days to match nights
ItinerarySchema::ensureDaysArray($item); // Normalize stop (add days if missing)
// Translation merge (text from translation, images from template)
ItinerarySchema::buildTranslationDays($templateStop, $translatedStop);
// Day image management (flat view for admin form)
ItinerarySchema::flattenDaysWithPointers($itinerary); // Flatten for display
ItinerarySchema::mergeDayImagesIntoItinerary($itinerary, $rows); // Write images back
// Check item types
ItinerarySchema::isArrival($item);
ItinerarySchema::isFlightRoute($route);

Source: backend/app/Support/ItinerarySchema.php

Duration is automatically calculated from itinerary day items:

duration = total_nights + 1

Arrival items are skipped (no nights). Example: Day 1 (2 nights) + Day 2 (1 night) = 3 nights + 1 = 4 days.

Source: backend/app/Filament/Resources/ProductTemplates/Support/ItineraryCalculator.php

The itinerary enforces route continuity: each day must start where the previous day ended. When using AI generation, the service automatically inserts connecting segments to maintain a continuous route graph.

Example: If Day 1 ends in Amboseli and Day 2 starts with Nairobi, a connecting segment is added automatically.

Helper class for itinerary computations:

use App\Filament\Resources\ProductTemplates\Support\ItineraryCalculator;
ItineraryCalculator::calculateDuration($itinerary); // Total days
ItineraryCalculator::getArrivalLocation($itinerary); // Entry city name
ItineraryCalculator::getAllLocations($itinerary); // All unique location names
ItineraryCalculator::hasFlightSegments($itinerary); // Has any flights?
ItineraryCalculator::generateHotelAssignments($itinerary); // Hotel assignment array

Source: backend/app/Filament/Resources/ProductTemplates/Support/ItineraryCalculator.php

The ItineraryRepeater provides the Filament form interface:

  • First item is always arrival type (cannot be deleted)
  • POI multi-select for day locations with Google Places search
  • Auto-generated routes from location sequence
  • Route mode selection (flight/surface)
  • Nested days repeater within each stop for per-day day_label, title, details, and optional day_image
  • Days array auto-resizes when nights value changes (preserves existing content by index)
  • On hydration, old itineraries without days[] are normalized via ensureDaysArray()
  • Duration auto-updates on change

Day images in Visual & Media step: The SupplierTour form presents a derived flat repeater (_day_images) that reads from the itinerary structure using flattenDaysWithPointers(). On save, images are merged back into itinerary[stop].days[day].day_image via mergeDayImagesIntoItinerary(). This keeps image management in the media step while storing images within the itinerary JSON.

Source: backend/app/Filament/Resources/ProductTemplates/Schemas/Components/ItineraryRepeater.php

Templates support AI-powered content generation with automatic POI resolution:

  1. TCAI Chat Modal (primary flow) — Paste raw trip info in the SupplierTour form, click “Generate with TCAI”. An interactive chat opens where the agent confirms destinations, airports, and city/nights distribution before generating. Content is returned to the form without persisting to DB. See AI System - TCAI Chatbox Modal.
  2. Itinerary-only Generation — “Generate Itinerary with TCAI” button generates just the itinerary from raw input (no marketing content), useful when only the route structure is needed.
  3. Per-field Refinement — Sparkles icons on form fields for targeted AI edits using FieldContentAgent.

The AI service:

  • Generates itinerary in the structured schema format
  • Resolves location names to POI IDs via database and Google Places
  • Ensures route continuity between days
  • Handles multi-segment days (e.g., drive + flight)
  • Includes fallback parsing for shorthand formats (5N, 7D, dash-separated N City)

See AI System for full agent architecture and configuration.

Source: backend/app/Services/ProductTemplateAIService.php

ProductTemplate has an optional tcai_profile_id FK to TcaiProfile. TCAI profiles provide custom style instructions that influence AI content generation tone and structure. The AI service uses the template’s assigned profile when available, falling back to the default profile otherwise.

Individual fields (subtitle, descriptions, highlights, etc.) can be refined using AI without regenerating all content. Sparkles icon buttons on form fields open a modal where the user provides instructions and selects a TCAI profile. The refinement is handled by ProductTemplateAIService::refineField() via FieldContentAgent.

Source: backend/app/Filament/Resources/ProductTemplates/Actions/RefineFieldAction.php

The FlightRouteConfigGenerator analyzes itineraries to extract flight segments for booking configuration:

  • Identifies arrival location (first international flight destination)
  • Extracts routes with mode: flight for domestic segments
  • Deduplicates consecutive stops sharing the same airport
  • Generates multi-city and separate flight options

FlightRouteValidationService uses this generator to validate that international legs have actual flight availability by performing a test Aerticket API search. See AerTicket - Validate Flight Route for details.

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

  • SKU is auto-generated on save (cannot be manually edited)
  • Duration auto-calculates from itinerary nights
  • First itinerary item must be arrival type
  • Route continuity is enforced (no gaps in route graph)
  • Templates with associated ProductByMarket records are “locked”
  • Itinerary changes propagate to market products