Notification System
Volare uses Laravel’s notification system to send alerts via email and database channels.
When to Use
Section titled “When to Use”- In-app notifications when offers are created (database channel)
- Daily email digests summarizing offer activity for admins
- Daily email digests summarizing active offers for supplier users
- System alerts requiring in-app visibility
Configuration
Section titled “Configuration”Required env vars:
MAIL_MAILER=smtpMAIL_HOST=mailpit(local) or SMTP serverMAIL_PORT=1025(local) or SMTP portMAIL_FROM_ADDRESS="noreply@volare.test"MAIL_FROM_NAME="Volare"
Queue worker: Must be running for OfferCreatedNotification (queued)
./vendor/bin/sail artisan queue:workDatabase: Notifications stored in notifications table
Architecture
Section titled “Architecture”Channels
Section titled “Channels”Notifications use different channels depending on their purpose:
| Notification | Channel | Delivery |
|---|---|---|
| OfferCreatedNotification | database | Queued (async) |
| BookingSubmittedToVolareNotification | mail + database | Queued (async) |
| BookingSubmittedToSupplierNotification | mail + database | Queued (async) |
| StopSaleCreatedToVolareNotification | mail + database | Queued (async) |
| QuotationRequestedNotification | mail (AnonymousNotifiable) / database (User) | Queued (async) |
| CheckoutContinuationNotification | mail | Queued (async) |
| DailyOffersDigestNotification | mail | Synchronous (via scheduled command) |
| SupplierOffersDigestNotification | mail | Synchronous (via scheduled command) |
Recipient Determination
Section titled “Recipient Determination”- Admin users — Via
Role::Adminenum (Spatie permissions) - Supplier users — Via
User.supplier_idlinking users to a supplier
Offer Created Notification
Section titled “Offer Created Notification”In-app notification sent when new offers are created. Appears in the Filament admin notification bell.
Trigger
Section titled “Trigger”OfferObserver::created() — After SKU generation completes
Source: backend/app/Observers/OfferObserver.php
Recipients
Section titled “Recipients”- All users with
Role::Admin - Users linked to offer’s supplier via:
Offer → SupplierTourRate → SupplierTour → Supplier → Users - Merged and deduplicated before sending
Database Payload
Section titled “Database Payload”[ 'title' => 'New Offer Created', 'message' => 'Offer {SKU} has been created.', 'offer_id' => $offer->id, 'sku' => $offer->sku, 'final_price' => $offer->final_price,]Source: backend/app/Notifications/OfferCreatedNotification.php
Booking Submitted Notifications
Section titled “Booking Submitted Notifications”Two queued notifications fire once payment has been confirmed and the booking has been finalized (passengers attached, upsells persisted). Both are dispatched from BookingStatusService::notifyBookingSubmitted() inside the routing transition’s transaction; failures are logged and swallowed so notification problems never break the status transition.
Trigger: BookingStatusService::transition() detects from_status === BookingStatus::PaymentProcessing AND to_status ∈ {PendingFlightBooking, PendingLandConfirmation} and calls notifyBookingSubmitted() after the booking row is updated and the transition row is recorded. By this point BookingFinalizationService::finalizeFromCheckoutSession has committed the passenger pivot, so the email renders the real pax count and names. Firing earlier on Checkout → PendingPayment produced Pasajeros: 0 because passengers had not been created yet.
Source: backend/app/Services/Booking/BookingStatusService.php
Volare notification
Section titled “Volare notification”Routed to the email of the active VolareEntity (VolareEntity::current()->email). Email lists booking reference, status, client, passenger count, departure/return dates, tour name, and the suppliers attached to the tour. The Filament bell entry links to the full BookingResource.
Source: backend/app/Notifications/BookingSubmittedToVolareNotification.php
Supplier notification
Section titled “Supplier notification”Sent to all users of every supplier attached to the booked tour (offer.supplierTourRate.tour.suppliers.users). Pricing is intentionally omitted — the email contains the tour name, the outbound departure datetime from origin, the outbound arrival datetime at the destination (prefixed with the destination IATA code, with destino/destination as a locale-aware fallback when the code is missing), and passenger count + names as a bullet list. After the operational block, the email closes with an “Urgente” call-to-action prompting the supplier to review hotels, activities, and special requirements; below the CTA a sign-off and a support contact line (bookings@byvolare.com) are rendered. The CTA links to the supplier-facing Trips to Deliver page, not the full booking. The default mail salutation is suppressed.
Flight datetimes are read from the offer’s dynamic flight cache (offer.offerFlights.flightCache.segments filtered to leg_sequence = 1), not from FlightBooking — by the time the email fires the booking is in PendingFlightBooking / PendingLandConfirmation, and the airline-booking call (FlightBooking::create) hasn’t run yet. When no outbound leg is bound, both datetime values render as em-dashes.
Localization: Email body is translated via lang/{en,es,ca,de}/notifications.php under the booking_submitted_supplier.* key. Locale is resolved from each supplier user’s Supplier.source_locale: the primary segment is taken (e.g. es_CR → es) and only en/es/ca/de are accepted; anything else falls back to es.
The Filament bell entry uses Filament\Notifications\Notification::make()->getDatabaseMessage() and links to TripToDeliverResource::view.
Source: backend/app/Notifications/BookingSubmittedToSupplierNotification.php, backend/lang/{en,es,ca,de}/notifications.php
Stop Sale Created (Volare)
Section titled “Stop Sale Created (Volare)”Sent when a StopSale row is created. Dispatched from StopSale::booted() (created hook) to VolareEntity::current()->email so Volare staff are aware of every newly opened stop-sale window. Failures are logged and never bubble up out of the model event.
Source: backend/app/Notifications/StopSaleCreatedToVolareNotification.php, backend/app/Models/StopSale.php
See Stop Sales for the feature.
Quotation Notifications
Section titled “Quotation Notifications”Two notifications support the non-standard-pax quotation flow.
QuotationRequestedNotification
Section titled “QuotationRequestedNotification”Fired from BookingFunnelService::requestQuotation() when a non-standard-pax booking stops at Main Contact. The channel is chosen by recipient: an AnonymousNotifiable (the reservations inbox mail route) receives mail, while panel User recipients (sales agents + admins, via Role::SalesAgent / Role::Admin) receive a database (Arkana bell) entry. Failures are logged and swallowed so they never break the checkout response.
The reservations inbox address comes from VolareEntity::current()->reservationsEmail(), which falls back to reservas@byvolare.com when the admin-editable reservations_email column is empty.
Source: backend/app/Notifications/QuotationRequestedNotification.php
CheckoutContinuationNotification
Section titled “CheckoutContinuationNotification”Mail to the customer, sent by an agent action once DMC availability is confirmed (booking in QuotationConfirmed). The CTA links to the Travelers step via Booking::getCheckoutContinuationUrl().
Source: backend/app/Notifications/CheckoutContinuationNotification.php
Daily Admin Digest
Section titled “Daily Admin Digest”Email sent daily at 8:00 AM Europe/Madrid to all admin users with a summary of offer activity.
Command
Section titled “Command”# Normal execution (run by scheduler)./vendor/bin/sail artisan offers:send-daily-digest
# Test with a specific "yesterday" date./vendor/bin/sail artisan offers:send-daily-digest --date=2026-02-23Sections
Section titled “Sections”The email contains up to three sections. Empty sections are omitted.
| Section | What it shows | Query logic |
|---|---|---|
| PENDING REVIEW | All current draft offers | status = Draft (not date-bounded) |
| ACTIVATED YESTERDAY | Offers activated the previous day | status = Active AND activated_at within yesterday (Madrid TZ → UTC) |
| CREATED YESTERDAY | Offers created the previous day | created_at within yesterday (Madrid TZ → UTC) |
Display Rules
Section titled “Display Rules”- Offers are grouped by product name (
ProductByMarket → ProductTemplate.title) - Each offer row shows: SKU (linked to admin), Airport, Departure date, Price + currency
- Draft section limited to 5 offers with “…and X more in the admin panel” overflow
- Email is skipped entirely if all three sections are empty
- Email is skipped if no admin users exist
- “View All Offers” button links to the Filament offer list
Schedule
Section titled “Schedule”Registered in routes/console.php:
Schedule::command('offers:send-daily-digest') ->dailyAt('08:00') ->timezone('Europe/Madrid') ->onOneServer();Source: backend/app/Console/Commands/SendDailyOffersDigestCommand.php
Template: backend/resources/views/mail/offers/daily-digest.blade.php
Supplier Active Offers Digest
Section titled “Supplier Active Offers Digest”Email sent daily at 8:00 AM Europe/Madrid to supplier-linked users showing all active offers for their supplier.
Supplier Digest Command
Section titled “Supplier Digest Command”# Normal execution (run by scheduler)./vendor/bin/sail artisan offers:send-supplier-digest
# Test for a specific supplier./vendor/bin/sail artisan offers:send-supplier-digest --supplier=5How Suppliers Are Matched to Offers
Section titled “How Suppliers Are Matched to Offers”Supplier → SupplierTour → ProductTemplate → ProductByMarket → Offers (status=Active)Only suppliers that have at least one active offer (through this chain) receive a digest.
Supplier Recipients
Section titled “Supplier Recipients”Users linked to the supplier via User.supplier_id. Suppliers with no linked user accounts are skipped.
Supplier Display Rules
Section titled “Supplier Display Rules”- Single section showing all active offers for that supplier
- Offers grouped by product name
- Each row shows: SKU, Airport, Departure date, Price + currency
- Limited to 15 offers with “…and X more in the admin panel” overflow
- “View Supplier Tours” button links to the Filament supplier tours list
- Subject line: “Active Offers Summary - {Supplier Name} - {date}“
Supplier Schedule
Section titled “Supplier Schedule”Schedule::command('offers:send-supplier-digest') ->dailyAt('08:00') ->timezone('Europe/Madrid') ->onOneServer();Source: backend/app/Console/Commands/SendSupplierOffersDigestCommand.php
Template: backend/resources/views/mail/offers/supplier-digest.blade.php
Email Branding
Section titled “Email Branding”All emails use published vendor mail views with custom Volare branding:
- Header: Volare SVG logo (replaces default Laravel logo)
- Footer: “VOLĀRE” branding
Source: backend/resources/views/vendor/mail/html/header.blade.php, message.blade.php
Testing Notifications
Section titled “Testing Notifications”Artisan Command
Section titled “Artisan Command”# Send test notification to first user./vendor/bin/sail artisan notification:test
# Send to specific email./vendor/bin/sail artisan notification:test user@example.comSource: backend/app/Console/Commands/SendTestNotification.php
Pest Tests
Section titled “Pest Tests”# Offer created notification tests./vendor/bin/sail artisan test --filter=OfferCreatedNotification
# Admin digest tests./vendor/bin/sail artisan test --filter=DailyOffersDigest
# Supplier digest tests./vendor/bin/sail artisan test --filter=SupplierOffersDigestTest suites:
backend/tests/Feature/Notifications/OfferCreatedNotificationTest.phpbackend/tests/Feature/Notifications/DailyOffersDigestNotificationTest.phpbackend/tests/Feature/Commands/SendDailyOffersDigestCommandTest.phpbackend/tests/Feature/Commands/SendSupplierOffersDigestCommandTest.php
Business Rules
Section titled “Business Rules”OfferCreatedNotificationis queued — queue worker must be running- Digest notifications are not queued — they run synchronously from scheduled commands
- Admin users receive in-app notifications for every offer + daily email digest
- Supplier users receive daily email digest for their supplier’s active offers only
- SKU must be generated before notification (handled by observer)
- Digests are skipped when there is nothing to report
Filament Integration
Section titled “Filament Integration”Database notifications appear in Filament admin panel:
- Notification bell icon in header
- Unread count badge
- Click to view details
- Mark as read functionality
Local Development
Section titled “Local Development”Use Mailpit for email testing:
- Web UI: http://localhost:8025
- SMTP: localhost:1025
- View all sent emails without actual delivery
- Test email templates and content
Related
Section titled “Related”- Queue System
- Offer Observer Logic
- Source:
backend/app/Notifications/ - Source:
backend/app/Observers/OfferObserver.php - Source:
backend/app/Console/Commands/SendDailyOffersDigestCommand.php - Source:
backend/app/Console/Commands/SendSupplierOffersDigestCommand.php