Suppliers Service
Manages land service providers (DMCs), their hotel inventory, and tour packages with independent per-hotel pricing.
When to Use
Section titled “When to Use”- Managing external companies that provide ground services (hotels, tours, transfers)
- Creating tour packages (the primary entry point for product creation)
- Publishing tours to markets with AI translation
- Setting up per-hotel pricing with rate periods and room types
- Setting up pricing periods with weekday restrictions and blackout dates
- Configuring multi-tenant access for supplier managers
Data Architecture
Section titled “Data Architecture”Supplier (land service provider, has source_locale for content language)├── SupplierHotel[] (hotel inventory)│ ├── SupplierHotelTranslation[] (per-locale content)│ └── SupplierService (1:1 - per-hotel pricing)│ ├── SupplierServiceTranslation[] (per-locale content)│ └── SupplierServiceRate[] (rate periods)│ └── SupplierServiceRatePrice[] (room type prices)├── SupplierActivity[] (excursions, day tours)│ ├── SupplierActivityTranslation[] (per-locale content)│ └── SupplierService (1:1 - per-activity pricing)│ └── SupplierServiceRate[] → SupplierServiceRatePrice[]├── SupplierTransfer[] (airport/city transfers)│ ├── SupplierTransferTranslation[] (per-locale content)│ └── SupplierService (1:1 - per-transfer pricing)│ └── SupplierServiceRate[] → SupplierServiceRatePrice[]├── SupplierContract[] (formal agreements)│ └── SupplierContractService[] (line items with price snapshots)│ ├── → SupplierService + SupplierServiceRate (service-based)│ └── → SupplierTourRate (tour-rate-based)└── SupplierTour[] (tour packages — M:N with suppliers via pivot) ├── supplier_supplier_tour (pivot: supplier_id, supplier_tour_id) ├── ProductTemplate (1:1 - defines itinerary structure) ├── SupplierTourItinerary[] (assignments per day) │ ├── selection_hotel_id, luxury_hotel_id, grand_luxury_hotel_id │ ├── supplier_tour_itinerary_activities (pivot) │ │ ├── type='included' → base tier activities (in package price) │ │ ├── type='extra' → optional add-on activities │ │ └── type='substitution' → upgrade replacement activities │ └── supplier_tour_itinerary_transfers (pivot) │ ├── type='selection' → base tier transfers │ ├── type='luxury' → luxury upgrade transfers │ └── type='grand_luxury' → premium upgrade transfers ├── package_service_supplier_tour (pivot: supplier_service_id, supplier_tour_id) └── SupplierTourRate[] (pricing periods - legacy)Key Concepts
Section titled “Key Concepts”Service Tiers (Hotels & Transfers)
Section titled “Service Tiers (Hotels & Transfers)”Hotels and transfers use a 3-tier luxury categorization system (ServiceTier):
| Tier | Enum Value | Label | Description |
|---|---|---|---|
| Selection | selection | 5 Star Selection | Base tier, included in package price |
| Luxury | luxury | 5 Star Luxury | Upgrade tier, shown as optional upgrades |
| Grand Luxury | grand_luxury | 5 Star Grand Luxury | Premium upgrade tier |
Source: backend/app/Enums/ServiceTier.php
Activity Tiers
Section titled “Activity Tiers”Activities use a dedicated ActivityTier enum, separate from the hotel/transfer tiers:
| Tier | Enum Value | Label | Description |
|---|---|---|---|
| Included | included | Included | Base activities included in package price |
| Extra | extra | Extra | Optional add-on activities (shown as “Añadir experiencia”) |
| Substitution | substitution | Substitution | Upgrade replacements for included activities (shown as “Mejorar experiencia” with gem badge) |
Source: backend/app/Enums/ActivityTier.php
Tier Relationships
Section titled “Tier Relationships”Each tour itinerary day can have hotels and transfers at each service tier, and activities at each activity tier:
// Hotel relationships (ServiceTier)$itinerary->selectionHotel; // Base tier hotel$itinerary->luxuryHotel; // Upgrade tier hotel$itinerary->grandLuxuryHotel; // Premium tier hotel
// Activity relationships (ActivityTier, many-to-many)$itinerary->includedActivities; // Base activities in package price$itinerary->extraActivities; // Optional add-on activities$itinerary->substitutionActivities; // Upgrade replacement activities
// Transfer relationships (ServiceTier, many-to-many)$itinerary->selectionTransfers; // Base tier transfers$itinerary->luxuryTransfers; // Upgrade tier transfers$itinerary->grandLuxuryTransfers; // Premium tier transfersCheckout Flow
Section titled “Checkout Flow”In checkout, Selection tier hotels/transfers and Included tier activities are included in the base price. Luxury and Grand Luxury hotels and Extra/Substitution activities appear as optional upgrades with prices calculated server-side. Extra and Substitution activities with a minimum_pax value are filtered out when the offer’s pax count is below the threshold; Included activities are never filtered. The frontend hotel selector uses a tabbed 3-tier UI where users can browse Selection (included), Luxury, and Grand Luxury hotels per time period, with mutual exclusivity per period (only one upgrade at a time).
Source: backend/app/Services/Checkout/CheckoutHotelService.php, backend/app/Services/Checkout/CheckoutActivityService.php, backend/app/Services/Checkout/CheckoutTransferService.php
SupplierService Architecture
Section titled “SupplierService Architecture”SupplierService provides independent, per-hotel pricing separate from the legacy tour-based rates. Each hotel can have its own service with flexible pricing models.
Source: backend/app/Models/SupplierService.php
Supplement Pricing Mode
Section titled “Supplement Pricing Mode”Hotel services have an is_supplement_pricing flag (default false). When enabled, stored prices are treated as per-person/night supplements instead of full room rates. The calculator multiplies the total by pax count derived from the room type.
| Mode | is_supplement_pricing | Calculation | Example (2A, 150€/night, 3 nights) |
|---|---|---|---|
| Room | false | rate × nights | 150 × 3 = 450€ |
| Supplement | true | rate × nights × pax | 150 × 3 × 2 = 900€ |
When to use: Package tours where the base hotel is bundled in a closed price and extra hotel nights are priced as per-person supplements.
Pax count is parsed from room type via SupplierServiceRatePrice::getPaxCountFromRoomType() (e.g., 2A = 2, 2A+1CH = 3, 2A+1B = 3).
Admin toggle: Visible only for Hotel-type services in the service form.
Source: backend/app/Services/Checkout/HotelPriceCalculatorService.php, backend/app/Filament/Resources/Suppliers/SupplierServices/Schemas/SupplierServiceForm.php
Service Types
Section titled “Service Types”| Type | Description | Use Case | FK Link |
|---|---|---|---|
| Hotel | Accommodation pricing | Per-hotel rates | supplier_hotel_id |
| Activity | Day tours, excursions | Per-person pricing | supplier_activity_id |
| Transfer | Airport/city transfers | Per-trip pricing | supplier_transfer_id |
Source: backend/app/Enums/SupplierServiceType.php
SupplierActivity
Section titled “SupplierActivity”Activities represent excursions, tours, and experiences that can be assigned to tour itineraries.
Source: backend/app/Models/SupplierActivity.php
Activity Fields
Section titled “Activity Fields”| Field | Description |
|---|---|
name | Activity name (e.g., “Great Wall Day Tour”) |
city | Location in CitySelect format |
time_slot | When activity occurs (morning, afternoon, full_day, lunch, dinner) |
description | Detailed description |
images | Photo gallery |
minimum_pax | Optional minimum passenger count. When set, Extra and Substitution activities are hidden in checkout if the offer’s pax count is below this value. Included activities are exempt. |
Time Slots
Section titled “Time Slots”Activities can specify when they occur during the day:
| Slot | Description |
|---|---|
morning | Morning activity |
afternoon | Afternoon activity |
full_day | All-day activity |
lunch | Lunch experience |
dinner | Dinner experience |
Source: backend/app/Enums/ActivityTimeSlot.php
Edit Lock
Section titled “Edit Lock”Activities used in supplier tours cannot be edited. The edit form is disabled and the save button hidden. Check $activity->isUsedInTours() to determine if locked.
Activity → Service Link
Section titled “Activity → Service Link”Each activity has ONE linked SupplierService for pricing:
// Activity has one service$activity->service; // SupplierService with pricing
// Service belongs to one activity$service->activity; // SupplierActivityWhen creating a service for an activity, the pricing model auto-switches to PerPerson.
SupplierTransfer
Section titled “SupplierTransfer”Transfers represent transportation services (airport pickups, city transfers) that can be assigned to tour itineraries.
Source: backend/app/Models/SupplierTransfer.php
Transfer Fields
Section titled “Transfer Fields”| Field | Description |
|---|---|
name | Transfer name (e.g., “Airport to Hotel Transfer”) |
city | Location in CitySelect format |
vehicle_type | Vehicle description (e.g., “Private Car”, “Minivan”) |
duration_minutes | Estimated duration |
description | Detailed description |
images | Photo gallery |
Transfer → Service Link
Section titled “Transfer → Service Link”Each transfer has ONE linked SupplierService for pricing:
// Transfer has one service$transfer->service; // SupplierService with pricing
// Service belongs to one transfer$service->transfer; // SupplierTransferActivity and Transfer Assignment Types
Section titled “Activity and Transfer Assignment Types”Transfers use ServiceTier (see Service Tiers). Activities use ActivityTier (see Activity Tiers):
Transfer tiers:
| Tier | Pivot Type | Description | In Offer Price? |
|---|---|---|---|
| Selection | selection | Base package transfers | Yes |
| Luxury | luxury | Upgrade transfers | No |
| Grand Luxury | grand_luxury | Premium transfers | No |
Activity tiers:
| Tier | Pivot Type | Description | In Offer Price? |
|---|---|---|---|
| Included | included | Base package activities | Yes |
| Extra | extra | Optional add-on activities | No |
| Substitution | substitution | Upgrade replacement activities | No |
// Assign activity to tour itinerary (Included tier)$itinerary->includedActivities()->attach($activity->id, [ 'sort_order' => 0, 'type' => 'included']);
// Assign transfer to tour itinerary (Luxury tier)$itinerary->luxuryTransfers()->attach($transfer->id, [ 'sort_order' => 0, 'type' => 'luxury']);The same activity/transfer CAN be assigned to multiple tiers on the same day (unique constraint is on itinerary_id, item_id, type).
Source: backend/app/Models/SupplierTourItinerary.php
Activity Time Slot Validation
Section titled “Activity Time Slot Validation”When assigning activities to tour itineraries, time slot conflicts are validated per day and tier:
Conflict Rules:
full_dayconflicts with ALL other slots (can’t have full_day + morning)- Same slots conflict (can’t have two morning activities)
- Different slots are allowed (morning + afternoon + dinner is OK)
- Activities without
time_slotare ignored in validation
Validation Scope:
- Each tier is validated independently (Included, Extra, Substitution)
- A morning Included activity does NOT conflict with a morning Extra activity
- Conflicts within the same tier are blocked
Error Format:
Day 3 (included): "Great Wall Tour" (Full Day) conflicts with "Temple Visit" (Morning)Source: backend/app/Services/Validation/ActivityTimeSlotOverlapValidator.php
Supplier Entity Translations
Section titled “Supplier Entity Translations”Supplier entities support per-locale translations so content displays in the market’s language during checkout and on the product detail page. Each translatable entity has a dedicated translation table and model.
All four entities implement HasSupplierTranslations and use the HasTranslations trait, which provides:
getTranslation(string $locale): ?Model— find translation record for a localetranslated(string $field, ?string $locale): mixed— return translated value with fallback to the source attribute when locale is null or no translation exists
Contract: backend/app/Contracts/HasSupplierTranslations.php
Trait: backend/app/Traits/HasTranslations.php
Translatable fields per entity:
| Entity | Translated Fields | Not Translated |
|---|---|---|
| SupplierActivity | name, description, inclusions, warnings, reasons (JSON), amenities (JSON) | city, time_slot, images |
| SupplierHotel | description, reasons (JSON), amenities (JSON) | hotel_name, address (proper nouns) |
| SupplierTransfer | name, description, vehicle_type | city, duration_minutes, images |
| SupplierService | name, description | pricing fields |
Translation tables: supplier_activity_translations, supplier_hotel_translations, supplier_transfer_translations, supplier_service_translations. Each has a FK with cascade delete, a locale column (string 10), and a unique constraint on (entity_FK, locale).
Locale resolution: The ResolveMarket middleware sets locale from the URL path (/api/{market}/{lang}/...) into request()->attributes->get('locale'). Checkout services accept ?string $locale = null and call $entity->translated('field', $locale). When locale is null (e.g., admin context), the source attribute is returned directly.
N+1 prevention: All checkout and product API queries eager-load .translations on supplier entities (e.g., selectionHotel.translations, extraActivities.translations). The trait reads from the already-loaded collection.
Admin UI: Each entity’s Filament edit page includes a “Manage Translations” header action (via HandlesSupplierTranslation trait). Modal with locale picker (from active Markets’ supported_locales), source content reference, and updateOrCreate save. Amenity icons are disabled (preserved from source) — only label and description are editable.
AI auto-translation: The locale picker exposes an “Auto-translate with AI” hint action. When clicked, SupplierTranslationService sends the record’s translatable source fields to SupplierTranslationAgent (Laravel AI structured output, OpenRouter). The agent uses a dynamic schema built from translatableFields() so only the entity’s actual fields are requested and all are required() — this prevents the provider from skipping fields. Amenity icons are stripped before the AI call and re-merged from the source by index afterwards (with a Log::warning when the returned item count diverges). The result pre-populates the modal form so the admin can review and edit before saving.
Source: backend/app/Filament/Concerns/HandlesSupplierTranslation.php, backend/app/Services/SupplierTranslationService.php, backend/app/Ai/Agents/SupplierTranslationAgent.php
Used by: EditSupplierActivity, EditSupplierHotel, EditSupplierTransfer, EditSupplierService
Pricing Models
Section titled “Pricing Models”| Model | Calculation | Example |
|---|---|---|
| PerNight | price × nights | Hotel accommodation |
| Package | Fixed price | All-inclusive tour |
| PerPerson | price × travelers | Group activities |
| PerTrip | Fixed price | Airport transfer |
Source: backend/app/Enums/ServicePricingModel.php
Rate Periods (SupplierServiceRate)
Section titled “Rate Periods (SupplierServiceRate)”Each service has rate periods defining when prices apply:
- Date range:
start_datetoend_date - Weekdays: Array of allowed days (e.g.,
["mon", "thu", "sat"]) - Blackout Dates: Date ranges when service is unavailable
- Allotment: Available inventory slots.
AllotmentConsumptionrecords track consumed slots per rate per booking.AllotmentServiceenforces availability at two points: (1) pre-configurator batch check to hide sold-out offers from the PDP, and (2) at payment time with pessimistic locking to prevent concurrent overbooking.
Methods:
isDateAvailable($date)- Checks period, weekday, and blackout datesisDateExcluded($date)- Checks blackout ranges onlygetPriceForRoomType($roomType)- Returns price record
Source: backend/app/Models/SupplierServiceRate.php, backend/app/Services/Allotment/AllotmentService.php
Room Type Prices (SupplierServiceRatePrice)
Section titled “Room Type Prices (SupplierServiceRatePrice)”Prices per room configuration within a rate period:
| Code | Description |
|---|---|
| 1A | 1 Adult |
| 2A | 2 Adults |
| 3A | 3 Adults |
| 4A | 4 Adults |
| 1A+1CH | 1 Adult + 1 Child |
| 1A+2CH | 1 Adult + 2 Children |
| 1A+3CH | 1 Adult + 3 Children |
| 2A+1CH | 2 Adults + 1 Child |
| 2A+2CH | 2 Adults + 2 Children |
| 2A+1B | 2 Adults + 1 Baby |
| 2A+1CH+1B | 2 Adults + 1 Child + 1 Baby |
| 3A+1CH | 3 Adults + 1 Child |
| per_person | Per Person (Activity) |
| per_trip | Per Trip (Transfer) |
Special room types:
per_person- Used for activity pricing, multiplied by number of travelersper_trip- Used for transfer pricing, fixed price regardless of travelers
Currency auto-assignment: When creating prices without a currency, the system automatically assigns the supplier’s default currency.
Source: backend/app/Models/SupplierServiceRatePrice.php
SupplierTour and ProductTemplate Relationship
Section titled “SupplierTour and ProductTemplate Relationship”Tours are linked 1:1 with ProductTemplates. The Tour wizard creates both the SupplierTour and its ProductTemplate in a single flow. The ProductTemplate defines the itinerary structure (cities, nights, titles), while SupplierTour adds supplier-specific data (hotels, rates, guide info).
ProductTemplate is no longer managed as a standalone resource — it is created and edited through Tours. See Admin Panel for the full workflow.
Source: backend/app/Models/SupplierTour.php
Multi-Supplier Tours
Section titled “Multi-Supplier Tours”A SupplierTour can have multiple suppliers via a many-to-many relationship. This enables multi-country tours where different suppliers manage different regions (e.g., India + Thailand).
Pivot table: supplier_supplier_tour (supplier_id, supplier_tour_id)
Relationship: SupplierTour::suppliers() (belongsToMany)
Helpers:
$tour->hasSupplier($supplierId)— checks pivot membership (uses loaded relation when available)$tour->supplier_ids— accessor returningarray<int>of all associated supplier IDs
Admin form: Multi-select supplier field replaces the old single-supplier dropdown.
Service assignment scoping: Hotels, activities, and transfers in the tour form are filtered to show items from ALL associated suppliers, not just one.
Supplier manager visibility: A supplier manager sees all tours that include their supplier in the pivot.
Policy: SupplierTourPolicy checks pivot-based membership via hasSupplier() instead of the old direct FK.
Source: backend/app/Models/SupplierTour.php, backend/app/Policies/SupplierTourPolicy.php
Multi-Package Services
Section titled “Multi-Package Services”A SupplierTour can have multiple package services (flat base pricing) via a many-to-many relationship. This allows combining packages from different suppliers into one tour price.
Pivot table: package_service_supplier_tour (supplier_service_id, supplier_tour_id)
Relationship: SupplierTour::packageServices() (belongsToMany to SupplierService)
Helpers:
$tour->hasPackageServices()— returns true if any package services are linked
Admin form: Multi-select package field shows packages from all associated suppliers.
Pricing: Base land price = sum of all selected package service prices for the given room type and date. See Offers - Land Price for the calculation.
Source: backend/app/Models/SupplierTour.php, backend/app/Services/Offers/AutoOfferGeneratorService.php
TourStatus
Section titled “TourStatus”Tours have an auto-derived status (TourStatus enum) based on 6 completion steps:
| Status | Value | Description |
|---|---|---|
| Draft | draft | One or more required steps incomplete |
| Complete | complete | All 6 steps pass — ready to publish to market |
Status is computed by SupplierTourService::determineStatusFromFormData() on every save. There is no manual status toggle.
Source: backend/app/Enums/TourStatus.php
SupplierTourService
Section titled “SupplierTourService”Centralized business logic for tour operations:
- Status derivation —
determineStatusFromFormData()/getCompletionProgressForTour()compute the 6-step completion progress - Hotel assignments —
generateHotelAssignments(),syncHotelAssignments(),saveHotelAssignments(),loadGroupedHotelAssignments() - Activity assignments —
saveActivityAssignments(),loadActivityAssignments()with time slot overlap validation - Transfer assignments —
saveTransferAssignments(),loadTransferAssignments() - Rate periods —
saveRatePeriods(),syncRatePeriods(),loadRatePeriods()
Source: backend/app/Services/SupplierTourService.php
Rate Date Validation
Section titled “Rate Date Validation”Tour rates have specific validity rules checked by isDateAvailable():
- Date within
start_date-end_daterange - Weekday in allowed
weekdaysarray (e.g.,["mon", "thu"]) - Date not in
excluded_date_ranges(blackout periods)
Source: backend/app/Models/SupplierTourRate.php
Room Type Codes
Section titled “Room Type Codes”Standardized room type codes used across the system:
| Code | Description |
|---|---|
| 1A | 1 Adult |
| 2A | 2 Adults |
| 3A | 3 Adults |
| 4A | 4 Adults |
| 1A+1CH | 1 Adult + 1 Child |
| 1A+2CH | 1 Adult + 2 Children |
| 1A+3CH | 1 Adult + 3 Children |
| 2A+1CH | 2 Adults + 1 Child |
| 2A+2CH | 2 Adults + 2 Children |
| 2A+1B | 2 Adults + 1 Baby |
| 2A+1CH+1B | 2 Adults + 1 Child + 1 Baby |
| 3A+1CH | 3 Adults + 1 Child |
Source: backend/app/Models/SupplierTourRateRoomPrice.php (roomTypeOptions())
SupplierHotel Fields
Section titled “SupplierHotel Fields”| Field | Description |
|---|---|
hotel_name | Hotel name |
city | Location in CitySelect format (“City, Country”) |
address | Street address |
category | ServiceTier enum (selection, luxury, grand_luxury) |
meal_plan | HotelMealPlan enum |
room_types | Available room configurations (array) |
room_type | Legacy single room type field |
images | Photo gallery |
description | Optional text description, displayed in PDP hotel detail modal and checkout hotel cards |
reasons | Array of selling points (e.g., “Beachfront location”) |
amenities | Array of amenity objects with icon, label, and description |
Source: backend/app/Models/SupplierHotel.php
SupplierHotel Room Types
Section titled “SupplierHotel Room Types”Hotels use a different room type configuration defined as model constants:
| Code | Description |
|---|---|
1 pax | 1 Pax (Single) |
2 pax | 2 Pax (Double) - Required |
3 pax | 3 Pax (Triple) |
2 pax 1 baby | 2 Pax + 1 Baby |
2 pax 1 child | 2 Pax + 1 Child |
2 pax 2 children | 2 Pax + 2 Children |
4 pax | 4 Pax (Quad) |
family | Family Room |
suite | Suite |
All hotels must include the “2 pax” room type.
Source: backend/app/Models/SupplierHotel.php (ROOM_TYPES, REQUIRED_ROOM_TYPE)
Connection to Offers
Section titled “Connection to Offers”Offers use SupplierService pricing from hotels AND included activities in the tour itinerary:
ProductByMarket → ProductTemplate ← SupplierTour → SupplierTourItinerary[] ↓ ┌─────────────┴─────────────┐ ↓ ↓ SupplierHotel[] SupplierActivity[] (included) ↓ ↓ SupplierService[] SupplierService[] ↓ ↓ Rate → Prices Rate → Prices ↓ ↓ └───────────┬───────────────┘ ↓ Offer (land_base_price)Offer Creation Flow
Section titled “Offer Creation Flow”- Select ProductByMarket (includes ProductTemplate and linked SupplierTour)
- System extracts all hotels from the tour’s itinerary
- System extracts all included activities (not upsells)
- System finds SupplierService for each hotel and activity
- Room type dropdown shows types available at ALL hotels (intersection)
- Price displayed is the TOTAL across all services for selected room type
- Offer stores the combined
land_base_price
Price Calculation
Section titled “Price Calculation”The land price sums hotel and included activity services, converting each to the market currency individually:
Land Price = Σ(convert(service_price, service_currency, market_currency))
Hotel Service Price = rate_price x nights (per_night model) With supplement pricing: rate_price x nights x pax_from_room_typeActivity Service Price = rate_price x travelers (per_person model)Room type lookup:
- Hotels: Use selected room type (e.g.,
2A,2A+1CH) - Activities: Always use
per_personroom type
Currency conversion: When a tour combines packages or services in different currencies (e.g., JPY + THB for a EUR market), each price is converted to the market currency before summing. See Offers - Mixed-Currency Conversion for details.
Source: backend/app/Services/Offers/AutoOfferGeneratorService.php (calculateLandPrice())
See Offers documentation for combined pricing details.
Multi-Tenant Access Control
Section titled “Multi-Tenant Access Control”SupplierManager Role
Section titled “SupplierManager Role”Users with SupplierManager role are automatically scoped to their assigned supplier. The Filament resources filter queries via the supplier_supplier_tour pivot — a manager sees all tours containing their supplier.
Source: backend/app/Filament/Resources/Suppliers/SupplierTourResource.php
Permissions
Section titled “Permissions”| Permission | Description |
|---|---|
| supplier.view_any | List suppliers |
| supplier.view | View supplier details |
| supplier.create | Create suppliers (Admin only) |
| supplier.update | Update supplier |
| supplier.delete | Delete supplier (Admin only) |
| supplier_tour.* | Tour CRUD operations |
| supplier_tour_rate.* | Rate CRUD operations |
| supplier_hotel.* | Hotel CRUD operations |
Filament Admin Resources
Section titled “Filament Admin Resources”Navigation Group: “Suppliers”
Section titled “Navigation Group: “Suppliers””| Resource | Nav Label | Model | Description |
|---|---|---|---|
| Suppliers | Suppliers | Supplier | Company management |
| Hotels | Hotels | SupplierHotel | Hotel inventory |
| Activities | Activities | SupplierActivity | Excursions, day tours |
| Transfers | Transfers | SupplierTransfer | Airport/city transfers |
| Supplier Services | Supplier Services | SupplierService | Per-hotel/activity/transfer pricing |
| SupplierTourResource | Tours | SupplierTour | Tour packages (primary product entry point) |
| Supplier Contracts | Supplier Contracts | SupplierContract | Contract management with status workflow |
| Release Dates | Release Dates | SupplierTourRate | Tour rate periods (legacy) |
Source: backend/app/Filament/Resources/Suppliers/
Tour Creation Wizard
Section titled “Tour Creation Wizard”Creating a SupplierTour uses an embedded wizard that also creates the ProductTemplate:
- Product Template Step: AI parser, title, duration, itinerary (cities/nights)
- Tour Details Step: Guide info, meals, transport, images
- Services Assignments Step: Three tabs:
- Hotels Tab: Grouped by stop with Selection/Luxury/Grand Luxury hotel selections
- Activities Tab: Per-day activity assignments across Included/Extra/Substitution tiers
- Transfers Tab: Per-day transfer assignments across Selection/Luxury/Grand Luxury tiers
AI Trip Parser
Section titled “AI Trip Parser”Paste raw trip information to auto-generate all template fields. Appears as a collapsible section at the top of Step 1. Each generation is logged to activity_log with log name ai_tour_parser, recording the raw input, source locale, TCAI profile, generated title, and total stops.
Example input:
Asia India 9 nights - 2 Delhi - 1 Jodhpur - 2 Udaipur - 2 Jaipur - 1 Agra - 1 DelhiGenerates:
- Tour title and descriptions (short/long)
- Complete itinerary with POI locations resolved via Google Places
- Duration auto-calculated from total nights
- Hotel assignments regenerated from itinerary
Content is generated in the selected source language. Shows confirmation modal before replacing existing values.
Source: backend/app/Services/ProductTemplateAIService.php (generateFromRawInput())
Flight Route Validation (Create Wizard)
Section titled “Flight Route Validation (Create Wizard)”Step 1 includes a “Validate flight routes before continuing” checkbox (checked by default). When checked, clicking Next triggers FlightRouteValidationService to perform a test Aerticket API search against both international and domestic flight legs. If validation fails, a notification is shown and the wizard is halted — uncheck the checkbox to skip validation.
This only applies to the Create wizard. Edit and View pages use a header button instead (see below).
Flight Route Validation (Edit/View)
Section titled “Flight Route Validation (Edit/View)”Edit and View pages have a “Validate Flight Route” header button that performs a test flight search via the Aerticket API. The confirmation modal shows the route summary, duration, and test parameters (2 adults, sample date ~60 days out, origin MAD). Results appear as per-segment notifications: each international and domestic leg gets an individual success/failure notification.
The validation service separates international and domestic legs from the route config, validates them independently, and returns domestic_results alongside the main international results.
The button is only visible when the tour has an itinerary defined.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Actions/ValidateFlightRouteAction.php
Service: backend/app/Services/Flights/FlightRouteValidationService.php, backend/app/Services/Flights/FlightRouteValidationNotifier.php
Publish to Market (Edit Page)
Section titled “Publish to Market (Edit Page)”The “Publish to Market” header action on the Tour edit page creates a ProductByMarket from the tour’s template. See Admin Panel - Publish to Market for details.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Actions/PublishToMarketAction.php
Tour View Page
Section titled “Tour View Page”The view page shows the TourCompletionWidget (6-step progress), a “Published Markets” section listing linked market products, and three footer widgets with read-only service assignment tables (hotels, activities, transfers).
See Admin Panel - Tour View Page for full details.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Pages/ViewSupplierTour.php
Hotel Selection Performance
Section titled “Hotel Selection Performance”Hotel dropdowns use lazy loading to prevent N+1 issues with large inventories:
- Hotels fetched on-demand when user searches (not preloaded)
- Search filtered by itinerary location
- Results limited to 50 per search
Cascade behavior: Deleting SupplierTour also deletes its ProductTemplate. Deleting Supplier cascades to tours and hotels.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Schemas/SupplierTourForm.php
Supplier Contracts
Section titled “Supplier Contracts”Manages formal agreements with suppliers, tracking negotiation status and locking in service prices and allotments. Contracts snapshot pricing from existing services/tours so agreed rates are preserved independently of future rate changes.
Use Cases
Section titled “Use Cases”- Formalizing agreed pricing with a supplier before a season
- Tracking contract negotiation and signing workflow
- Checking whether a supplier has an active contract covering a specific date
- Auto-generating a contract from an existing tour’s service assignments
Status Lifecycle
Section titled “Status Lifecycle”Contracts follow a strict state machine with guarded transitions:
Draft → Sent → Negotiating → Signed → Active → Expired ↓ ↓ ↓ ↓ ↓ └───────┴─────────┴───────────┴────────┴──→ Terminated| Status | Editable | Can Add Services | Final |
|---|---|---|---|
| Draft | Yes | Yes | No |
| Sent | Yes | Yes | No |
| Negotiating | Yes | Yes | No |
| Signed | No | No | No |
| Active | No | No | No |
| Expired | No | No | Yes |
| Terminated | No | No | Yes |
Transitions set timestamps automatically: sent_at when moving to Sent, terminated_at when moving to Terminated. The signed_at and signed_by fields are captured automatically when signed via Signaturit e-signature, or manually via a modal form.
Source: backend/app/Enums/SupplierContractStatus.php
Reference Numbers
Section titled “Reference Numbers”Auto-generated sequential pattern: SC-{YYYY}-{0001}. The sequence resets each year and does not reuse numbers from soft-deleted contracts (the lookup includes trashed rows so audit traceability is preserved).
Source: backend/app/Models/SupplierContract.php (generateReferenceNumber())
Contract Services (Line Items)
Section titled “Contract Services (Line Items)”Each contract has line items (SupplierContractService) that snapshot a price and allotment from either a SupplierService + SupplierServiceRate pair or a SupplierTourRate. Price priority for snapshots: 2A room type, then per_person, then per_trip, then first available.
Source: backend/app/Models/SupplierContractService.php
SupplierContractService (Business Logic)
Section titled “SupplierContractService (Business Logic)”The service class provides:
transitionStatus()— validates transitions and sets timestampsgetServicesForQuotation()— returns contract line items applicable on a specific date (filters by rate date availability)isSupplierCovered()— checks if a supplier has an active contract valid on a datecollectServicesFromTour(SupplierTour $tour, int $supplierId)— returns the services (hotels, activities, transfers, package services) referenced by the tour and owned by$supplierId. Filters every query bysupplier_idso a multi-supplier tour never leaks another supplier’s services into the contract.buildContractDataFromTour(SupplierTour $tour, int $supplierId)— composes repeater-compatible form data (title, currency, validity range, line items) for the given supplier. ThrowsInvalidArgumentExceptionif the supplier isn’t attached to the tour.
Source: backend/app/Services/SupplierContractService.php
Admin UI (Filament)
Section titled “Admin UI (Filament)”Create Page
Section titled “Create Page”The “Generate from Tour” dropdown (visible after selecting a supplier) auto-fills the contract title, validity dates, and the service line items from the tour’s itinerary assignments. Only services owned by the selected supplier are pulled in — on a multi-supplier tour, services from other suppliers stay on their own contracts.
Currency is inherited from the supplier and not editable on the contract. The currency Select is rendered disabled and shows the supplier’s currency for context. On save, both mutateFormDataBeforeCreate and mutateFormDataBeforeSave derive currency_id from the contract’s supplier_id, so a tampered or stale form value cannot diverge from the supplier’s currency.
Edit Page Workflow Actions
Section titled “Edit Page Workflow Actions”Header actions enforce the status lifecycle:
| Action | Visible When | Modal |
|---|---|---|
| Send for Signature | Draft/Sent, no pending signature | Generates PDF, sends to Signaturit |
| Resend Email | Pending signature exists | Triggers a Signaturit reminder for the same signaturit_signature_id; updates signaturit_sent_at |
| Cancel Signature | Pending signature exists | Cancels via Signaturit API |
| Download Contract PDF | contract_pdf_path is set | Downloads generated PDF |
| Download Signed PDF | signed_pdf_path is set | Downloads Signaturit-signed PDF |
| Mark as Sent | Draft | Status-only flag (no email, no PDF, no Signaturit call) — use when the contract was delivered outside the Signaturit flow |
| Mark as Signed | Sent or Negotiating | Prompts for signed_at and signed_by |
| Activate | Signed | Confirmation only |
| Terminate | Any non-final status | Prompts for termination_reason |
See Signaturit E-Signature for the full digital signature flow.
Source: backend/app/Filament/Resources/Suppliers/SupplierContracts/
CLI: suppliers:contracts:generate
Section titled “CLI: suppliers:contracts:generate”Auto-generate a contract from a tour’s service assignments via Artisan.
# Interactive (prompts for tour selection)./vendor/bin/sail artisan suppliers:contracts:generate
# Direct with tour ID./vendor/bin/sail artisan suppliers:contracts:generate 42
# Preview without creating./vendor/bin/sail artisan suppliers:contracts:generate 42 --dry-runResolves the target supplier (auto-picks if the tour has a single supplier, otherwise prompts), collects only that supplier’s services from the tour itinerary, previews line items in a table, and creates the contract with status Draft (and the supplier’s currency) inside a DB transaction.
Source: backend/app/Console/Commands/GenerateSupplierContractCommand.php
Bulk Template Import/Export
Section titled “Bulk Template Import/Export”The Supplier Services list page provides Excel template export/import for bulk-creating hotels or activities with their full entity chain.
Location
Section titled “Location”Header actions on Supplier Services list page (/admin/supplier-services):
- Export Template — generates an XLSX template
- Import Template — uploads a filled XLSX and creates records
Both actions prompt for a Supplier and a Type (Hotel or Activity).
Source: backend/app/Filament/Resources/Suppliers/SupplierServices/Actions/ExportTemplateAction.php, ImportTemplateAction.php
Hotels Template
Section titled “Hotels Template”Columns across entity fields, rate period, pricing type, and prices:
| Column | Field | Required | Notes |
|---|---|---|---|
| A | Hotel Name | Yes | Creates SupplierHotel |
| B | City | Yes | |
| C | Address | No | |
| D-G | Category, Room Config, Room Type, Meal Plan | No | See cell comments for format |
| H | Rate Start Date | Yes | DD/MM/YYYY |
| I | Rate End Date | Yes | DD/MM/YYYY |
| J | Rooms/Day | Yes | Allotment, minimum 1 |
| K | Release Days | No | |
| L | Operating Days | No | Comma-separated 3-letter codes (defaults to all days) |
| M | Blackout Start | No | DD/MM/YYYY |
| N | Blackout End | No | DD/MM/YYYY |
| O | Pricing Type | No | Dropdown: “Room” (default, pre-filled) or “Supplement”. Accepts old values (“Room Price”, “Supplement per Person”) on import. |
| P+ | Price columns | Min 1 | 1A, 2A, 3A, 4A, 6A, 2A+1CH, 2A+2CH, 2A+1B |
One row creates: SupplierHotel → SupplierService (type=Hotel, pricing=PerNight) → SupplierServiceRate → SupplierServiceRatePrice (one per filled price column).
Pricing Type validation: All rows for the same hotel identity (matching name, city, address, category, meal plan, and room config) must use the same Pricing Type. Conflicting rows produce a validation error.
Activities Template
Section titled “Activities Template”10 columns:
| Column | Field | Required | Notes |
|---|---|---|---|
| A | Name | Yes | Creates SupplierActivity |
| B | Description | No | Max 500 chars |
| C | City | Yes | |
| D | Time Slot | No | Dropdown: Morning, Afternoon, Full Day, Lunch, Dinner |
| E | Rate Start Date | Yes | DD/MM/YYYY |
| F | Rate End Date | Yes | DD/MM/YYYY |
| G | Operating Days | No | Comma-separated 3-letter codes (defaults to all days) |
| H | Blackout Start | No | DD/MM/YYYY |
| I | Blackout End | No | DD/MM/YYYY |
| J | Price per Person | Yes | In supplier currency |
One row creates: SupplierActivity → SupplierService (type=Activity, pricing=PerPerson) → SupplierServiceRate (allotment=999) → SupplierServiceRatePrice (room_type=per_person).
Template Features
Section titled “Template Features”- Date validation cells (DD/MM/YYYY format)
- Number validation for allotment and price columns
- Cell comments on headers with format hints and examples
- Instructions sheet in each template
- Empty rows are skipped automatically
- All validation errors reported with row numbers
Source: backend/app/Services/SupplierTemplateService.php (export), backend/app/Services/SupplierTemplateImportService.php (import)
Test Data Generation
Section titled “Test Data Generation”Use these commands to populate your local environment with test data. Run them in order: hotels → activities → services.
suppliers:hotels:generate
Section titled “suppliers:hotels:generate”Generate test supplier hotels with realistic luxury hotel data.
# Interactive mode with prompts./vendor/bin/sail artisan suppliers:hotels:generate
# Non-interactive with options./vendor/bin/sail artisan suppliers:hotels:generate \ --suppliers=1 --suppliers=2 \ --countries=Spain --countries=France \ --hotels-per-city=3 \ --use-aiOptions:
| Option | Default | Description |
|---|---|---|
--suppliers=* | interactive | Supplier IDs (multiple values allowed) |
--countries=* | interactive | Country names from airports table |
--hotels-per-city | 2 | Hotels per city (minimum 2) |
--use-ai | off | Use OpenRouter API for realistic data |
- Cities sourced from airports table (valid destinations only)
- AI mode generates realistic luxury hotel names via OpenRouter
- Fallback mode uses hotel brand names (Four Seasons, Ritz-Carlton, etc.)
- Room types always include required “2 pax” plus random optional types
Source: backend/app/Console/Commands/GenerateTestSupplierHotels.php, backend/app/Services/SupplierHotelGeneratorService.php
suppliers:activities:generate
Section titled “suppliers:activities:generate”Generate test supplier activities (tours, excursions, experiences) by city.
# Interactive mode./vendor/bin/sail artisan suppliers:activities:generate
# Non-interactive./vendor/bin/sail artisan suppliers:activities:generate \ --suppliers=1 \ --countries=Spain \ --activities-per-city=2Options:
| Option | Default | Description |
|---|---|---|
--suppliers=* | interactive | Supplier IDs (multiple values allowed) |
--countries=* | interactive | Country names from airports table |
--activities-per-city | 3 | Activities per city (minimum 1) |
Creates activities for each supplier. Use suppliers:services:generate --type=activity afterward to create linked services with per_person pricing.
Source: backend/app/Console/Commands/GenerateTestSupplierActivities.php
suppliers:services:generate
Section titled “suppliers:services:generate”Generate supplier services with rates and pricing. Links to hotels/activities created in previous steps.
# Interactive mode (prompts for type and supplier)./vendor/bin/sail artisan suppliers:services:generate
# Non-interactive./vendor/bin/sail artisan suppliers:services:generate \ --type=hotel --suppliers=1 --count=10Options:
| Option | Default | Description |
|---|---|---|
--type | interactive | Service type: hotel, activity, or transfer |
--suppliers=* | interactive | Supplier IDs (multiple values allowed) |
--count | 20 | Number of services to create per supplier |
Pricing models are assigned automatically by type: per_night for hotels, per_person for activities, per_trip for transfers.
Source: backend/app/Console/Commands/GenerateTestSupplierServices.php
Database Schema
Section titled “Database Schema”Core Tables
Section titled “Core Tables”| Table | Purpose |
|---|---|
suppliers | Land service provider companies |
supplier_hotels | Hotel inventory per supplier |
supplier_activities | Excursion/tour inventory per supplier |
supplier_transfers | Transfer inventory per supplier |
supplier_services | Purchasable services (hotel, activity, transfer) |
supplier_service_rates | Rate periods with date/weekday constraints |
supplier_service_rate_prices | Room type prices per rate period |
Translation Tables
Section titled “Translation Tables”| Table | Purpose |
|---|---|
supplier_activity_translations | Per-locale content for activities |
supplier_hotel_translations | Per-locale content for hotels |
supplier_transfer_translations | Per-locale content for transfers |
supplier_service_translations | Per-locale content for services |
Tour Tables
Section titled “Tour Tables”| Table | Purpose |
|---|---|
supplier_tours | Tour packages linked to ProductTemplates |
supplier_supplier_tour | Pivot: many-to-many suppliers per tour |
package_service_supplier_tour | Pivot: many-to-many package services per tour |
supplier_tour_itineraries | Hotel assignments per tour day (selection/luxury/grand_luxury) |
supplier_tour_itinerary_activities | Activity pivot (included/extra/substitution per day) |
supplier_tour_itinerary_transfers | Transfer pivot (selection/luxury/grand_luxury per day) |
supplier_tour_rates | Tour rate periods (legacy) |
supplier_tour_rate_room_prices | Tour room prices (legacy) |
Contract Tables
Section titled “Contract Tables”| Table | Purpose |
|---|---|
supplier_contracts | Contract header with status, validity, signing info |
supplier_contract_services | Line items linking contracts to services/rates with price snapshots |
Key Constraints
Section titled “Key Constraints”supplier_services.supplier_hotel_idis unique (one service per hotel)supplier_services.supplier_activity_idis unique (one service per activity)supplier_services.supplier_transfer_idis unique (one service per transfer)supplier_tour_itinerary_activitiesunique on(itinerary_id, activity_id, type)supplier_tour_itinerary_transfersunique on(itinerary_id, transfer_id, type)- Deleting a supplier cascades to services, hotels, activities, and transfers
- Deleting a service rate cascades to its prices
Source: backend/database/migrations/ (search for supplier)
Files:
- Models:
backend/app/Models/Supplier*.php - Translation Models:
backend/app/Models/SupplierActivityTranslation.php,SupplierHotelTranslation.php,SupplierTransferTranslation.php,SupplierServiceTranslation.php - Translation Trait:
backend/app/Traits/HasTranslations.php - Translation Contract:
backend/app/Contracts/HasSupplierTranslations.php - Translation Admin Trait:
backend/app/Filament/Concerns/HandlesSupplierTranslation.php - Enums:
backend/app/Enums/SupplierServiceType.php,backend/app/Enums/ServicePricingModel.php,backend/app/Enums/ActivityTimeSlot.php,backend/app/Enums/ServiceTier.php,backend/app/Enums/ActivityTier.php,backend/app/Enums/SupplierContractStatus.php,backend/app/Enums/TourStatus.php - Resources:
backend/app/Filament/Resources/Suppliers/ - Policies:
backend/app/Policies/Supplier*.php - Transfer Resource:
backend/app/Filament/Resources/Suppliers/SupplierTransfers/ - Activity Resource:
backend/app/Filament/Resources/Suppliers/SupplierActivities/ - Template Export:
backend/app/Services/SupplierTemplateService.php - Template Import:
backend/app/Services/SupplierTemplateImportService.php - Tour Service:
backend/app/Services/SupplierTourService.php - Contract Service:
backend/app/Services/SupplierContractService.php - Contract Resource:
backend/app/Filament/Resources/Suppliers/SupplierContracts/ - Contract CLI:
backend/app/Console/Commands/GenerateSupplierContractCommand.php - Time Slot Validator:
backend/app/Services/Validation/ActivityTimeSlotOverlapValidator.php - Checkout Services:
backend/app/Services/Checkout/CheckoutHotelService.php,CheckoutActivityService.php,CheckoutTransferService.php - Preview DTO:
backend/app/Filament/Resources/Offers/Schemas/OfferPreviewData.php