Files
core/docs/notifications.md
T

4.8 KiB

Notifications

Notifications are driven by NotificationRegistry in Modules\Core\Notification. Each notification class declares what event it listens to and who to notify, and the registry wires up the event listener automatically.


Async delivery

All notification classes extend BaseNotification, which implements ShouldQueue and uses the Queueable trait. This means every notification is delivered asynchronously via the queue — the HTTP request returns immediately and the email is sent by a queue worker.

In local development or environments without a queue worker, set QUEUE_CONNECTION=sync in .env to send emails inline.


Key naming convention

Notification keys follow the pattern module.event.refersTo.via:

Segment Description Example
module The module the notification belongs to appointment
event The event that triggers it booked, cancelled, rescheduled
refersTo Who the notification is about customer, staff
via The delivery channel mail, slack

Example: appointment.cancelled.customer.mail


Built-in notifications

Key Class Event Channel
appointment.booked.customer.mail AppointmentBookedCustomerNotification AppointmentBooked mail
appointment.booked.staff.mail AppointmentBookedStaffNotification AppointmentBooked mail
appointment.cancelled.customer.mail AppointmentCancelledCustomerNotification AppointmentCancelled mail
appointment.rescheduled.customer.mail AppointmentRescheduledCustomerNotification AppointmentRescheduled mail

These are registered in AppointmentsServiceProvider::boot().

When AppointmentBooked fires

AppointmentBooked is not fired directly from book() or reassign(). It is fired by CreateGoogleMeetLinkListener after the Meet link has been created (or attempted). This means notifications always go out with the Meet link already in the event — if the link couldn't be created, meetUrl is null and the email templates handle that gracefully.

Resend behaviour

AppointmentBooked carries an isResend: bool flag (default false). When true:

  • AppointmentBookedStaffNotification returns an empty via() — no staff email is sent
  • N8nWebhookListener short-circuits — no webhook is fired

This flag is set by AppointmentService::resendConfirmation() so only the customer notification goes out on a manual resend.


Creating a notification

Extend BaseNotification:

use Illuminate\Support\Facades\Notification as NotificationFacade;
use Modules\Core\Notification\BaseNotification;

class AppointmentBookedAdminNotification extends BaseNotification
{
    public function __construct(private readonly AppointmentBooked $event) {}

    public static function getKey(): string
    {
        return 'appointment.booked.admin';
    }

    public static function listensTo(): string
    {
        return AppointmentBooked::class;
    }

    public function via(object $notifiable): array
    {
        return ['mail'];
    }

    public function notifiable(): \Illuminate\Notifications\AnonymousNotifiable
    {
        return NotificationFacade::route('mail', 'admin@example.com');
    }
}

Then register it in your service provider:

NotificationRegistry::get()->register([AppointmentBookedAdminNotification::class]);

Notifiable: anonymous vs model

Return an AnonymousNotifiable (via Notification::route()) when there is no Eloquent model involved — e.g. a fixed email address or Slack webhook pulled from the event or config.

Return an Eloquent model when you have one. The model must implement routeNotificationFor for the relevant channel (Laravel does this automatically for mail via the Notifiable trait).

public function notifiable(): User
{
    return User::find($this->event->userId);
}

Replacing a built-in notification

Register a new class with the same key. The registry replaces the previous class under that key, so the existing event listener will dispatch the new class instead.

NotificationRegistry::get()->register([MyCustomMailNotification::class]); // getKey() returns 'appointment.booked.customer.mail'

If overriding a module notification from AppServiceProvider, do it inside booted() to ensure module providers have already run:

public function boot(): void
{
    $this->app->booted(function () {
        NotificationRegistry::get()->register([MyCustomSlackNotification::class]);
    });
}

Removing a notification

NotificationRegistry::get()->unregister('appointment.booked.customer.mail');

The event listener remains registered but short-circuits without dispatching anything.

Again, call this inside booted() when doing it from AppServiceProvider.