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.
Purpose
Section titled “Purpose”- 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
Data Model
Section titled “Data Model”Table: product_templates
| Field | Type | Purpose |
|---|---|---|
| title | varchar | Product name |
| subtitle | varchar | Marketing tagline |
| sku | varchar | Auto-generated: <ID>-<DAYS> |
| source_locale | varchar | Content language (e.g., en_US) |
| itinerary | jsonb | Array of arrival + day items with POIs and routes |
| duration | int | Trip length in days (auto-calculated) |
| highlights | jsonb | Key selling points |
| categories | jsonb | Product categories (beach, luxury, etc.) |
| tcai_profile_id | FK (nullable) | Optional AI personality profile |
Source: backend/app/Models/ProductTemplate.php
Itinerary Schema
Section titled “Itinerary Schema”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.
Schema Format
Section titled “Schema Format”[ { "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} ] }]Per-Day Content
Section titled “Per-Day Content”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.
| Field | Type | Purpose |
|---|---|---|
day_label | string | Display label (e.g., “Day 1 - Medellín”) |
title | string | Day title |
details | string | Day description/activities |
day_image | string or null | Image 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.
Item Types
Section titled “Item Types”| Type | Fields | Purpose |
|---|---|---|
| Arrival | type, arrival_poi_id | Entry point to the trip (first item) |
| Stop | locations, nights, routes, days[] | Journey stop with per-day content |
Route Modes
Section titled “Route Modes”Routes use the RouteMode enum to distinguish transport types:
| Mode | Value | Description |
|---|---|---|
| Flight | flight | Air travel between locations |
| Surface | arnk | Ground 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
Schema Helper Class
Section titled “Schema Helper Class”ItinerarySchema is the single source of truth for itinerary structure. Use it when working with itinerary data programmatically.
use App\Support\ItinerarySchema;
// Create itemsItinerarySchema::createArrivalItem($poiId);ItinerarySchema::createDayItem($locationIds, $nights, $routes, $title, $details);ItinerarySchema::createRoute($from, $to, RouteMode::Flight, $fromId, $toId);
// Per-day contentItinerarySchema::createDayEntry($dayLabel, $title, $details, $dayImage);ItinerarySchema::buildDaysArray($nights, $existingDays); // Resize days to match nightsItinerarySchema::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 displayItinerarySchema::mergeDayImagesIntoItinerary($itinerary, $rows); // Write images back
// Check item typesItinerarySchema::isArrival($item);ItinerarySchema::isFlightRoute($route);Source: backend/app/Support/ItinerarySchema.php
Duration Calculation
Section titled “Duration Calculation”Duration is automatically calculated from itinerary day items:
duration = total_nights + 1Arrival 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
Route Continuity
Section titled “Route Continuity”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.
Itinerary Calculator
Section titled “Itinerary Calculator”Helper class for itinerary computations:
use App\Filament\Resources\ProductTemplates\Support\ItineraryCalculator;
ItineraryCalculator::calculateDuration($itinerary); // Total daysItineraryCalculator::getArrivalLocation($itinerary); // Entry city nameItineraryCalculator::getAllLocations($itinerary); // All unique location namesItineraryCalculator::hasFlightSegments($itinerary); // Has any flights?ItineraryCalculator::generateHotelAssignments($itinerary); // Hotel assignment arraySource: backend/app/Filament/Resources/ProductTemplates/Support/ItineraryCalculator.php
Admin Form Component
Section titled “Admin Form Component”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 optionalday_image - Days array auto-resizes when nights value changes (preserves existing content by index)
- On hydration, old itineraries without
days[]are normalized viaensureDaysArray() - 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
AI Content Generation
Section titled “AI Content Generation”Templates support AI-powered content generation with automatic POI resolution:
- 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.
- 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.
- 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-separatedN City)
See AI System for full agent architecture and configuration.
Source: backend/app/Services/ProductTemplateAIService.php
TCAI Profile Integration
Section titled “TCAI Profile Integration”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.
Per-Field AI Refinement
Section titled “Per-Field AI Refinement”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
Flight Route Extraction
Section titled “Flight Route Extraction”The FlightRouteConfigGenerator analyzes itineraries to extract flight segments for booking configuration:
- Identifies arrival location (first international flight destination)
- Extracts routes with
mode: flightfor 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
Business Rules
Section titled “Business Rules”- 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
Related
Section titled “Related”- POI System - Points of Interest database
- Products by Market - Market-specific configuration
- Admin Panel - Filament management
- AI System - Agent architecture and configuration
- Database Relationships - Entity diagram and FK references