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.
Who Can Use It
Section titled “Who Can Use It”- 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.
Data Model
Section titled “Data Model”StopSale belongs to a Supplier and a SupplierTour (supplier_tour_id).
| Column | Notes |
|---|---|
supplier_id | Owning supplier |
supplier_tour_id | Tour the window applies to |
start_date / end_date | Inclusive window (date-cast) |
reason | Free-text reason (logged via activity log) |
created_by_user_id | Auto-stamped from auth()->id() on creating |
Activity is captured via #[ActivityLog(logFillable: true)].
| Method | Purpose |
|---|---|
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
Matching Rule (Web/API vs Admin)
Section titled “Matching Rule (Web/API vs Admin)”Stop sales must answer two slightly different questions:
| Caller | Matching field | Why |
|---|---|---|
Web/API checkout (Offer::scopeBookable()) | offers.departure_date | Portable 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())
Filament Resource
Section titled “Filament Resource”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/
Notification on Create
Section titled “Notification on Create”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