Notification System
Volāre 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)
- Booking, payment, and lead alerts to Volāre staff and customers
- 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) |
| LeadingPriceAlertNotification | mail (Volāre inbox + configured copies) |
Queued (async, via scheduled command) |
| ProductActivatedNotification | mail (configured recipients) |
Queued (async, via model observer) |
| BookingMainContactCapturedNotification | mail |
Queued (async) |
| BookingPaymentConfirmedNotification | mail |
Queued (async) |
| BalancePaymentRequestNotification | mail |
Queued (async) |
| AllotmentExhaustedNotification | mail |
Queued (async) |
Recipient Determination
Section titled “Recipient Determination”- Admin users — Via
Role::Adminenum (Spatie permissions) - Supplier users — Via
User.supplier_idlinking users to a supplier
Configurable Email Copies (System → Emails)
Section titled “Configurable Email Copies (System → Emails)”Operational emails registered in the TransactionalEmail enum can have extra copy recipients (“watchers”) managed in the System → Emails admin (EmailConfiguration + EmailCopyRecipient rows). At each dispatch site, TransactionalEmailCopier::fanOut() sends every active recipient their own copy through the standard notification mail path. The list page keeps rows in sync with the registry (creates missing keys, prunes stale ones), and the edit page’s “Send preview” renders the email from a recent representative record via TransactionalEmailSampleFactory: booking-submitted emails sample the latest booking with passengers (so previews never show an empty passenger list), checkout continuation samples one with a resumption token, and quotation emails one in a quotation status.
The per-passenger trip access email (trip_access_shared) is the one registered email whose copies are sanitized: watchers receive a single copy per booking without the magic-link CTA, and its preview is sanitized the same way — see Trip Access — Email Template.
Source: backend/app/Enums/TransactionalEmail.php, backend/app/Services/Email/ (TransactionalEmailCopier, EmailCopyService, TransactionalEmailSampleFactory)
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. The Volāre and supplier legs run in separate try/catch blocks so one failing leg cannot silence the other; failures are reported (Sentry/Nightwatch) and logged, but 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
Volāre notification
Section titled “Volāre 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). A supplier with no panel users is not skipped: the email falls back to the supplier’s operational address (suppliers.email), passing the supplier to the notification so the greeting and locale still match the partner. A supplier with neither users nor an email is reported as a RuntimeException so the delivery gap is visible instead of silent; a tour with no linked supplier at all logs a warning. 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 the supplier’s source_locale (via the recipient user’s supplier, or the supplier passed to the notification on operational-email fallback sends): 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 (Volāre)
Section titled “Stop Sale Created (Volāre)”Sent when a StopSale row is created. Dispatched from StopSale::booted() (created hook) to VolareEntity::current()->email so Volāre 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(), with a unique per-send sig signature appended so opens of this email’s link can be attributed in the booking’s Preview Views section (opens of a bare ?resume= URL are labelled as copied/shared instead).
Source: backend/app/Notifications/CheckoutContinuationNotification.php
Booking & Payment Notifications
Section titled “Booking & Payment Notifications”Four queued mail-channel notifications cover the booking lifecycle from lead capture through payment. All are ConfigurableCopyEmail (registered in TransactionalEmail, so extra copy recipients apply — see below) and pin ->locale('es') on the instance so the language survives queueing.
| Notification | Recipient | Fires when |
|---|---|---|
| BookingMainContactCapturedNotification | Reservations inbox + configured copies (internal only) | A lead submits contact details on the checkout Main Contact step. Dispatched once per booking, guarded by bookings.main_contact_notified_at so a contact edit + resubmit does not re-send. |
| BookingPaymentConfirmedNotification | Booking payer (contact_email, client.email fallback) |
A successful payment lands. Renders the designed emails.payment-confirmed template. |
| BalancePaymentRequestNotification | Customer | A Volāre agent generates a balance payment link from the booking admin page; carries the amount, due date, and payment link. |
| AllotmentExhaustedNotification | Volāre entity email + configured copies (internal only) | A booking exhausts allotment for one or more (rate, service date) tuples. One aggregated mail per booking, dispatched from AllotmentService::consumeAllotment via DB::afterCommit. |
Payment-flow details are covered in depth in Payment.
Source: backend/app/Notifications/{BookingMainContactCaptured,BookingPaymentConfirmed,BalancePaymentRequest,AllotmentExhausted}Notification.php
Leading Price Drift Alert
Section titled “Leading Price Drift Alert”Email sent daily at 07:00 Europe/Madrid to the Volare entity email when a product’s leading (“desde”) price rises above a threshold versus the previous day, or its bookable anchor disappears — so the “from” price advertised in paid ads can be re-synced with the product page.
Leading Price Command
Section titled “Leading Price Command”# Normal execution (run by scheduler)./vendor/bin/sail artisan products:check-leading-price
# Preview without persisting snapshots or sending the alert./vendor/bin/sail artisan products:check-leading-price --dry-run
# Custom drift threshold (percent) and/or a single product./vendor/bin/sail artisan products:check-leading-price --threshold=5 --product=44How Drift Is Detected
Section titled “How Drift Is Detected”- The leading price is read via
ProductByMarket::getLeadingPrice()— the same value the product page renders — and stored as oneproduct_leading_price_snapshotsrow per product per day. - Each run compares today’s price against the most recent snapshot from a prior day. The first run for a product establishes a baseline and never alerts.
- Only increases above the threshold or a disappeared anchor (nothing bookable today) are flagged; price drops are benign.
Configuring the Threshold (System → Emails)
Section titled “Configuring the Threshold (System → Emails)”The percent threshold is edited from the System → Emails admin, on the “Leading-price drift alert” row (alert_threshold_percent). When left empty the command falls back to 2%. The row’s Active toggle switches the alert off without a deploy — snapshots keep being recorded (so the baseline stays current), only the email is suppressed. An explicit --threshold= flag on the command overrides the configured value for ad-hoc runs.
Leading Price Recipients
Section titled “Leading Price Recipients”The primary recipient is the Volāre entity email (VolareEntity::current()->email). Like the other transactional emails, extra recipients can be added under Manage recipients in System → Emails — each configured recipient receives their own copy, fanned out via TransactionalEmailCopier. The Send preview action renders a representative sample of the digest (one price-rise row + one anchor-gone row). The alert is a single aggregated digest listing every flagged product with its old → new price and a link to the product edit page in the admin.
Leading Price Schedule
Section titled “Leading Price Schedule”Schedule::command('products:check-leading-price') ->dailyAt('07:00') ->timezone('Europe/Madrid') ->withoutOverlapping() ->onOneServer();Source: backend/app/Console/Commands/CheckLeadingPriceCommand.php, backend/app/Notifications/LeadingPriceAlertNotification.php, backend/app/Models/ProductLeadingPriceSnapshot.php
Product Activated
Section titled “Product Activated”Email sent to the team when a product-by-market transitions to active (becomes visible on the web), so a new itinerary going live is announced automatically instead of by a manual email.
Product Activated Trigger
Section titled “Product Activated Trigger”ProductByMarketObserver::updated() fires the notification when a product’s status changes to active (draft/inactive → active). Creating a product already active — imports, duplication, factories — does not announce; only the transition does, and only once per activation.
Product Activated Recipients
Section titled “Product Activated Recipients”Configured in System → Emails (“Product activated — to team”), seeded with team@byvolare.com by default. Each configured recipient receives their own copy via TransactionalEmailCopier. The Active toggle switches the announcement off; with no recipients (or the config off) nothing is sent.
Product Activated Content
Section titled “Product Activated Content”Spanish email with the product name, market, a link to its public page (ProductByMarket::getPublicUrl()) and an Arkana edit link. Send preview renders a sample from the most recent product.
Source: backend/app/Observers/ProductByMarketObserver.php, backend/app/Notifications/ProductActivatedNotification.php
Email Branding
Section titled “Email Branding”All emails use published vendor mail views with custom Volāre branding:
- Header: Volāre 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=OfferCreatedNotificationTest suites:
backend/tests/Feature/Notifications/OfferCreatedNotificationTest.phpbackend/tests/Feature/Console/CheckLeadingPriceCommandTest.php
Business Rules
Section titled “Business Rules”-
OfferCreatedNotificationis queued — queue worker must be running -
LeadingPriceAlertNotificationis queued even though it fires from a scheduled command — the queue worker must be running for the alert to send -
Admin users receive in-app notifications for every offer
-
SKU must be generated before notification (handled by observer)
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