Skip to content

Suppliers Service

Manages land service providers (DMCs), their hotel inventory, and tour packages with independent per-hotel pricing.

  • 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
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)

Hotels and transfers use a 3-tier luxury categorization system (ServiceTier):

TierEnum ValueLabelDescription
Selectionselection5 Star SelectionBase tier, included in package price
Luxuryluxury5 Star LuxuryUpgrade tier, shown as optional upgrades
Grand Luxurygrand_luxury5 Star Grand LuxuryPremium upgrade tier

Source: backend/app/Enums/ServiceTier.php

Activities use a dedicated ActivityTier enum, separate from the hotel/transfer tiers:

TierEnum ValueLabelDescription
IncludedincludedIncludedBase activities included in package price
ExtraextraExtraOptional add-on activities (shown as “Añadir experiencia”)
SubstitutionsubstitutionSubstitutionUpgrade replacements for included activities (shown as “Mejorar experiencia” with gem badge)

Source: backend/app/Enums/ActivityTier.php

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 transfers

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 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

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.

Modeis_supplement_pricingCalculationExample (2A, 150€/night, 3 nights)
Roomfalserate × nights150 × 3 = 450€
Supplementtruerate × nights × pax150 × 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

TypeDescriptionUse CaseFK Link
HotelAccommodation pricingPer-hotel ratessupplier_hotel_id
ActivityDay tours, excursionsPer-person pricingsupplier_activity_id
TransferAirport/city transfersPer-trip pricingsupplier_transfer_id

Source: backend/app/Enums/SupplierServiceType.php

Activities represent excursions, tours, and experiences that can be assigned to tour itineraries.

Source: backend/app/Models/SupplierActivity.php

FieldDescription
nameActivity name (e.g., “Great Wall Day Tour”)
cityLocation in CitySelect format
time_slotWhen activity occurs (morning, afternoon, full_day, lunch, dinner)
descriptionDetailed description
imagesPhoto gallery
minimum_paxOptional 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.

Activities can specify when they occur during the day:

SlotDescription
morningMorning activity
afternoonAfternoon activity
full_dayAll-day activity
lunchLunch experience
dinnerDinner experience

Source: backend/app/Enums/ActivityTimeSlot.php

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.

Each activity has ONE linked SupplierService for pricing:

// Activity has one service
$activity->service; // SupplierService with pricing
// Service belongs to one activity
$service->activity; // SupplierActivity

When creating a service for an activity, the pricing model auto-switches to PerPerson.

Transfers represent transportation services (airport pickups, city transfers) that can be assigned to tour itineraries.

Source: backend/app/Models/SupplierTransfer.php

FieldDescription
nameTransfer name (e.g., “Airport to Hotel Transfer”)
cityLocation in CitySelect format
vehicle_typeVehicle description (e.g., “Private Car”, “Minivan”)
duration_minutesEstimated duration
descriptionDetailed description
imagesPhoto gallery

Each transfer has ONE linked SupplierService for pricing:

// Transfer has one service
$transfer->service; // SupplierService with pricing
// Service belongs to one transfer
$service->transfer; // SupplierTransfer

Transfers use ServiceTier (see Service Tiers). Activities use ActivityTier (see Activity Tiers):

Transfer tiers:

TierPivot TypeDescriptionIn Offer Price?
SelectionselectionBase package transfersYes
LuxuryluxuryUpgrade transfersNo
Grand Luxurygrand_luxuryPremium transfersNo

Activity tiers:

TierPivot TypeDescriptionIn Offer Price?
IncludedincludedBase package activitiesYes
ExtraextraOptional add-on activitiesNo
SubstitutionsubstitutionUpgrade replacement activitiesNo
// 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

When assigning activities to tour itineraries, time slot conflicts are validated per day and tier:

Conflict Rules:

  • full_day conflicts 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_slot are 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 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 locale
  • translated(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:

EntityTranslated FieldsNot Translated
SupplierActivityname, description, inclusions, warnings, reasons (JSON), amenities (JSON)city, time_slot, images
SupplierHoteldescription, reasons (JSON), amenities (JSON)hotel_name, address (proper nouns)
SupplierTransfername, description, vehicle_typecity, duration_minutes, images
SupplierServicename, descriptionpricing 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

ModelCalculationExample
PerNightprice × nightsHotel accommodation
PackageFixed priceAll-inclusive tour
PerPersonprice × travelersGroup activities
PerTripFixed priceAirport transfer

Source: backend/app/Enums/ServicePricingModel.php

Each service has rate periods defining when prices apply:

  • Date range: start_date to end_date
  • Weekdays: Array of allowed days (e.g., ["mon", "thu", "sat"])
  • Blackout Dates: Date ranges when service is unavailable
  • Allotment: Available inventory slots. AllotmentConsumption records track consumed slots per rate per booking. AllotmentService enforces 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 dates
  • isDateExcluded($date) - Checks blackout ranges only
  • getPriceForRoomType($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:

CodeDescription
1A1 Adult
2A2 Adults
3A3 Adults
4A4 Adults
1A+1CH1 Adult + 1 Child
1A+2CH1 Adult + 2 Children
1A+3CH1 Adult + 3 Children
2A+1CH2 Adults + 1 Child
2A+2CH2 Adults + 2 Children
2A+1B2 Adults + 1 Baby
2A+1CH+1B2 Adults + 1 Child + 1 Baby
3A+1CH3 Adults + 1 Child
per_personPer Person (Activity)
per_tripPer Trip (Transfer)

Special room types:

  • per_person - Used for activity pricing, multiplied by number of travelers
  • per_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

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 returning array<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

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

Tours have an auto-derived status (TourStatus enum) based on 6 completion steps:

StatusValueDescription
DraftdraftOne or more required steps incomplete
CompletecompleteAll 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

Centralized business logic for tour operations:

  • Status derivationdetermineStatusFromFormData() / getCompletionProgressForTour() compute the 6-step completion progress
  • Hotel assignmentsgenerateHotelAssignments(), syncHotelAssignments(), saveHotelAssignments(), loadGroupedHotelAssignments()
  • Activity assignmentssaveActivityAssignments(), loadActivityAssignments() with time slot overlap validation
  • Transfer assignmentssaveTransferAssignments(), loadTransferAssignments()
  • Rate periodssaveRatePeriods(), syncRatePeriods(), loadRatePeriods()

Source: backend/app/Services/SupplierTourService.php

Tour rates have specific validity rules checked by isDateAvailable():

  1. Date within start_date - end_date range
  2. Weekday in allowed weekdays array (e.g., ["mon", "thu"])
  3. Date not in excluded_date_ranges (blackout periods)

Source: backend/app/Models/SupplierTourRate.php

Standardized room type codes used across the system:

CodeDescription
1A1 Adult
2A2 Adults
3A3 Adults
4A4 Adults
1A+1CH1 Adult + 1 Child
1A+2CH1 Adult + 2 Children
1A+3CH1 Adult + 3 Children
2A+1CH2 Adults + 1 Child
2A+2CH2 Adults + 2 Children
2A+1B2 Adults + 1 Baby
2A+1CH+1B2 Adults + 1 Child + 1 Baby
3A+1CH3 Adults + 1 Child

Source: backend/app/Models/SupplierTourRateRoomPrice.php (roomTypeOptions())

FieldDescription
hotel_nameHotel name
cityLocation in CitySelect format (“City, Country”)
addressStreet address
categoryServiceTier enum (selection, luxury, grand_luxury)
meal_planHotelMealPlan enum
room_typesAvailable room configurations (array)
room_typeLegacy single room type field
imagesPhoto gallery
descriptionOptional text description, displayed in PDP hotel detail modal and checkout hotel cards
reasonsArray of selling points (e.g., “Beachfront location”)
amenitiesArray of amenity objects with icon, label, and description

Source: backend/app/Models/SupplierHotel.php

Hotels use a different room type configuration defined as model constants:

CodeDescription
1 pax1 Pax (Single)
2 pax2 Pax (Double) - Required
3 pax3 Pax (Triple)
2 pax 1 baby2 Pax + 1 Baby
2 pax 1 child2 Pax + 1 Child
2 pax 2 children2 Pax + 2 Children
4 pax4 Pax (Quad)
familyFamily Room
suiteSuite

All hotels must include the “2 pax” room type.

Source: backend/app/Models/SupplierHotel.php (ROOM_TYPES, REQUIRED_ROOM_TYPE)

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)
  1. Select ProductByMarket (includes ProductTemplate and linked SupplierTour)
  2. System extracts all hotels from the tour’s itinerary
  3. System extracts all included activities (not upsells)
  4. System finds SupplierService for each hotel and activity
  5. Room type dropdown shows types available at ALL hotels (intersection)
  6. Price displayed is the TOTAL across all services for selected room type
  7. Offer stores the combined land_base_price

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_type
Activity 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_person room 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.

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

PermissionDescription
supplier.view_anyList suppliers
supplier.viewView supplier details
supplier.createCreate suppliers (Admin only)
supplier.updateUpdate supplier
supplier.deleteDelete supplier (Admin only)
supplier_tour.*Tour CRUD operations
supplier_tour_rate.*Rate CRUD operations
supplier_hotel.*Hotel CRUD operations
ResourceNav LabelModelDescription
SuppliersSuppliersSupplierCompany management
HotelsHotelsSupplierHotelHotel inventory
ActivitiesActivitiesSupplierActivityExcursions, day tours
TransfersTransfersSupplierTransferAirport/city transfers
Supplier ServicesSupplier ServicesSupplierServicePer-hotel/activity/transfer pricing
SupplierTourResourceToursSupplierTourTour packages (primary product entry point)
Supplier ContractsSupplier ContractsSupplierContractContract management with status workflow
Release DatesRelease DatesSupplierTourRateTour rate periods (legacy)

Source: backend/app/Filament/Resources/Suppliers/

Creating a SupplierTour uses an embedded wizard that also creates the ProductTemplate:

  1. Product Template Step: AI parser, title, duration, itinerary (cities/nights)
  2. Tour Details Step: Guide info, meals, transport, images
  3. 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

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 Delhi

Generates:

  • 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())

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).

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

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

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 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

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.

  • 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

Contracts follow a strict state machine with guarded transitions:

Draft → Sent → Negotiating → Signed → Active → Expired
↓ ↓ ↓ ↓ ↓
└───────┴─────────┴───────────┴────────┴──→ Terminated
StatusEditableCan Add ServicesFinal
DraftYesYesNo
SentYesYesNo
NegotiatingYesYesNo
SignedNoNoNo
ActiveNoNoNo
ExpiredNoNoYes
TerminatedNoNoYes

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

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())

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

The service class provides:

  • transitionStatus() — validates transitions and sets timestamps
  • getServicesForQuotation() — 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 date
  • collectServicesFromTour(SupplierTour $tour, int $supplierId) — returns the services (hotels, activities, transfers, package services) referenced by the tour and owned by $supplierId. Filters every query by supplier_id so 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. Throws InvalidArgumentException if the supplier isn’t attached to the tour.

Source: backend/app/Services/SupplierContractService.php

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.

Header actions enforce the status lifecycle:

ActionVisible WhenModal
Send for SignatureDraft/Sent, no pending signatureGenerates PDF, sends to Signaturit
Resend EmailPending signature existsTriggers a Signaturit reminder for the same signaturit_signature_id; updates signaturit_sent_at
Cancel SignaturePending signature existsCancels via Signaturit API
Download Contract PDFcontract_pdf_path is setDownloads generated PDF
Download Signed PDFsigned_pdf_path is setDownloads Signaturit-signed PDF
Mark as SentDraftStatus-only flag (no email, no PDF, no Signaturit call) — use when the contract was delivered outside the Signaturit flow
Mark as SignedSent or NegotiatingPrompts for signed_at and signed_by
ActivateSignedConfirmation only
TerminateAny non-final statusPrompts for termination_reason

See Signaturit E-Signature for the full digital signature flow.

Source: backend/app/Filament/Resources/Suppliers/SupplierContracts/

Auto-generate a contract from a tour’s service assignments via Artisan.

Terminal window
# 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-run

Resolves 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

The Supplier Services list page provides Excel template export/import for bulk-creating hotels or activities with their full entity chain.

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

Columns across entity fields, rate period, pricing type, and prices:

ColumnFieldRequiredNotes
AHotel NameYesCreates SupplierHotel
BCityYes
CAddressNo
D-GCategory, Room Config, Room Type, Meal PlanNoSee cell comments for format
HRate Start DateYesDD/MM/YYYY
IRate End DateYesDD/MM/YYYY
JRooms/DayYesAllotment, minimum 1
KRelease DaysNo
LOperating DaysNoComma-separated 3-letter codes (defaults to all days)
MBlackout StartNoDD/MM/YYYY
NBlackout EndNoDD/MM/YYYY
OPricing TypeNoDropdown: “Room” (default, pre-filled) or “Supplement”. Accepts old values (“Room Price”, “Supplement per Person”) on import.
P+Price columnsMin 11A, 2A, 3A, 4A, 6A, 2A+1CH, 2A+2CH, 2A+1B

One row creates: SupplierHotelSupplierService (type=Hotel, pricing=PerNight) → SupplierServiceRateSupplierServiceRatePrice (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.

10 columns:

ColumnFieldRequiredNotes
ANameYesCreates SupplierActivity
BDescriptionNoMax 500 chars
CCityYes
DTime SlotNoDropdown: Morning, Afternoon, Full Day, Lunch, Dinner
ERate Start DateYesDD/MM/YYYY
FRate End DateYesDD/MM/YYYY
GOperating DaysNoComma-separated 3-letter codes (defaults to all days)
HBlackout StartNoDD/MM/YYYY
IBlackout EndNoDD/MM/YYYY
JPrice per PersonYesIn supplier currency

One row creates: SupplierActivitySupplierService (type=Activity, pricing=PerPerson) → SupplierServiceRate (allotment=999) → SupplierServiceRatePrice (room_type=per_person).

  • 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)

Use these commands to populate your local environment with test data. Run them in order: hotels → activities → services.

Generate test supplier hotels with realistic luxury hotel data.

Terminal window
# 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-ai

Options:

OptionDefaultDescription
--suppliers=*interactiveSupplier IDs (multiple values allowed)
--countries=*interactiveCountry names from airports table
--hotels-per-city2Hotels per city (minimum 2)
--use-aioffUse 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

Generate test supplier activities (tours, excursions, experiences) by city.

Terminal window
# 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=2

Options:

OptionDefaultDescription
--suppliers=*interactiveSupplier IDs (multiple values allowed)
--countries=*interactiveCountry names from airports table
--activities-per-city3Activities 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

Generate supplier services with rates and pricing. Links to hotels/activities created in previous steps.

Terminal window
# 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=10

Options:

OptionDefaultDescription
--typeinteractiveService type: hotel, activity, or transfer
--suppliers=*interactiveSupplier IDs (multiple values allowed)
--count20Number 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

TablePurpose
suppliersLand service provider companies
supplier_hotelsHotel inventory per supplier
supplier_activitiesExcursion/tour inventory per supplier
supplier_transfersTransfer inventory per supplier
supplier_servicesPurchasable services (hotel, activity, transfer)
supplier_service_ratesRate periods with date/weekday constraints
supplier_service_rate_pricesRoom type prices per rate period
TablePurpose
supplier_activity_translationsPer-locale content for activities
supplier_hotel_translationsPer-locale content for hotels
supplier_transfer_translationsPer-locale content for transfers
supplier_service_translationsPer-locale content for services
TablePurpose
supplier_toursTour packages linked to ProductTemplates
supplier_supplier_tourPivot: many-to-many suppliers per tour
package_service_supplier_tourPivot: many-to-many package services per tour
supplier_tour_itinerariesHotel assignments per tour day (selection/luxury/grand_luxury)
supplier_tour_itinerary_activitiesActivity pivot (included/extra/substitution per day)
supplier_tour_itinerary_transfersTransfer pivot (selection/luxury/grand_luxury per day)
supplier_tour_ratesTour rate periods (legacy)
supplier_tour_rate_room_pricesTour room prices (legacy)
TablePurpose
supplier_contractsContract header with status, validity, signing info
supplier_contract_servicesLine items linking contracts to services/rates with price snapshots
  • supplier_services.supplier_hotel_id is unique (one service per hotel)
  • supplier_services.supplier_activity_id is unique (one service per activity)
  • supplier_services.supplier_transfer_id is unique (one service per transfer)
  • supplier_tour_itinerary_activities unique on (itinerary_id, activity_id, type)
  • supplier_tour_itinerary_transfers unique 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