Feat: Moving Migrations from boboko to core
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# 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`:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
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).
|
||||
|
||||
```php
|
||||
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.
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
$this->app->booted(function () {
|
||||
NotificationRegistry::get()->register([MyCustomSlackNotification::class]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Removing a notification
|
||||
|
||||
```php
|
||||
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`.
|
||||
Reference in New Issue
Block a user