Skip to content

Stop Sales

A Stop Sale is a contiguous date window on a SupplierTour that marks the tour as unavailable. While a window is active, every Offer whose tour matches and whose date falls inside the window is treated as not-bookable on the web/API and surfaces a “Stop sale” badge in the admin UI.

Stop sale is not an OfferStatus value — it is a derived display state computed at read time. Once the window expires or the row is deleted, affected offers revert to whatever their stored status implies. No backfill, no schema migration on the offer.

  • Supplier-managers — full CRUD on stop sales for their own tours (view/create/update/delete_stop_sale)
  • Admins — full CRUD via the wildcard permission

See Roles & Permissions.

StopSale belongs to a Supplier and a SupplierTour (supplier_tour_id).

ColumnNotes
supplier_idOwning supplier
supplier_tour_idTour the window applies to
start_date / end_dateInclusive window (date-cast)
reasonFree-text reason (logged via activity log)
created_by_user_idAuto-stamped from auth()->id() on creating

Activity is captured via #[ActivityLog(logFillable: true)].

MethodPurpose
StopSale::appliesOn(?Carbon $date)True when the row’s window covers the given date (today by default)
StopSale::scopeActive(?Carbon $date)Filters to rows whose window covers the given date
SupplierTour::stopSales()HasMany<StopSale> ordered by start_date
SupplierTour::isStoppedOn(?Carbon $date)True when at least one stop sale on the tour is active on the given date

Source: backend/app/Models/StopSale.php, backend/app/Models/SupplierTour.php, migration backend/database/migrations/2026_05_10_094146_create_stop_sales_table.php

Stop sales must answer two slightly different questions:

CallerMatching fieldWhy
Web/API checkout (Offer::scopeBookable())offers.departure_datePortable SQL filter (no flight-cache join). Stop-sale windows usually span both departure and arrival, so a departure overlap is sufficient in practice.
Admin UI (offer infolist badge, stop-sale form “Affected offers” preview)Offer::getArrivalDate() (last segment of leg_sequence=1 from the bound flight cache)Suppliers care about the arrival date in destination. The admin context can afford the join and reads the precise date.

Offer::isStopped() uses arrival-date matching and is what getDisplayStatusLabel() / getDisplayStatusColor() consult.

The scopeBookable filter is inherited automatically by every web/API call site (ProductByMarketController, CheckoutController) — no per-call site wiring.

Source: backend/app/Models/Offer.php (scopeBookable(), isStopped(), getArrivalDate())

Resource at app/Filament/Resources/StopSales/ with list / create / edit / view / delete pages. Default list filter “Hide expired” hides rows whose end_date < today.

The create form previews “Affected offers” live via a RepeatableEntry whose state closure filters offers by tour + arrival date with a +2 day buffer, so the operator sees what they’re about to stop before saving.

Source: backend/app/Filament/Resources/StopSales/

StopSale::booted() registers a created hook that dispatches StopSaleCreatedToVolareNotification (mail + database/Filament bell) to VolareEntity::current()->email. Failures are logged via Log::warning and never bubble up.

See Notifications — Stop Sale Created (Volare).

Source: backend/app/Notifications/StopSaleCreatedToVolareNotification.php