Skip to main content

Overview

Opscale\NotificationCenter\Jobs\SendDelivery is responsible for sending one notification to one profile via one channel. It can be dispatched directly for a specific Delivery record and records the outcome through the TrackEvent service action, which updates the Delivery model’s status accordingly.
ExecuteNotificationStrategy does not dispatch SendDelivery — it calls $profile->notify() directly in its internal sendDelivery() method. SendDelivery is a standalone job you can dispatch independently when you need to re-send a specific delivery record outside the strategy flow.
use Opscale\NotificationCenter\Jobs\SendDelivery;

SendDelivery::dispatch($delivery);

Class signature

class SendDelivery implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        protected Delivery $delivery,
    );

    public function handle(): void;
}

Constructor

delivery
Opscale\NotificationCenter\Models\Delivery
required
The Delivery model representing the specific channel-profile pair for this attempt. The job reads delivery->notification->type to look up the matching queue in config('notification-center.strategies') and places itself on that queue.
Queue assignment mirrors ExecuteNotificationStrategy — the same strategy queue is used:
$type  = strtolower($this->delivery->notification->type->value ?? 'transactional');
$queue = config("notification-center.strategies.{$type}.queue", 'notifications');
$this->onQueue($queue);
If the notification type cannot be resolved, the job falls back to the transactional strategy queue.

How it works

1

Resolve the notification class

handle() reads the channel name from the delivery and looks it up in the notification-center.messages map:
$channel          = $this->delivery->channel;
$messages         = config('notification-center.messages', []);
$notificationClass = $messages[$channel];
If no class is registered for the channel the job returns silently.
2

Resolve the profile

The profile is loaded from the delivery relationship:
$profile = $this->delivery->profile;
If the profile cannot be found the job returns silently without throwing.
3

Send the notification

The notification class is instantiated with the Delivery model and dispatched via Laravel’s notify() method on the profile:
$profile->notify(new $notificationClass($this->delivery));
Laravel’s notification system then fires NotificationSent or NotificationFailed events, which are picked up by the TrackEvent listener to update the delivery status.

Delivery status transitions

The Delivery model progresses through the following statuses. Transitions are enforced by DeliveryStatus::canTransitionTo() — invalid transitions are logged as warnings and silently rejected.
enum DeliveryStatus: string
{
    case PENDING  = 'Pending';   // queued, waiting to be sent
    case FAILED   = 'Failed';    // could not be delivered
    case SENT     = 'Sent';      // handed off to the channel provider
    case RECEIVED = 'Received';  // confirmed received by the recipient
    case OPENED   = 'Opened';    // recipient opened/viewed the notification
    case VERIFIED = 'Verified';  // recipient read or acknowledged the notification
    case EXPIRED  = 'Expired';   // timed out before being verified
}
Allowed transitions:
FromAllowed next statuses
PENDINGSENT, FAILED, EXPIRED
SENTRECEIVED, OPENED, EXPIRED
RECEIVEDOPENED, VERIFIED, EXPIRED
OPENEDVERIFIED, EXPIRED
VERIFIED(terminal)
FAILED(terminal)
EXPIRED(terminal)
A delivery that reaches OPENED or VERIFIED stops the channel escalation cycle in ExecuteNotificationStrategy — no further channels will be attempted for that profile.

Retry behavior

Retry settings are not baked into SendDelivery itself — they are configured per strategy in notification-center.php:
Config keyDescription
max_attemptsMaximum number of delivery attempts per channel before the delivery is marked FAILED
retry_intervalSeconds between retries; provide an array for escalating back-off (e.g. [300, 1800])
// Example — transactional strategy
'transactional' => [
    'max_attempts'   => 3,
    'retry_interval' => [300, 1800], // 5 min, then 30 min
],

Error handling

When a notification fails, Laravel dispatches a NotificationFailed event. The TrackEvent service action listens to this event and:
  1. Records a Failed event on the delivery with the exception message.
  2. Transitions the delivery status to FAILED.
  3. For Alert-type notifications, emits an additional Log::critical entry to alert on-call teams.
// TrackEvent::handleFailed — called automatically by the event listener
protected function handleFailed(NotificationFailed $event): void
{
    $this->failed([
        'error' => $event->data['exception']?->getMessage() ?? 'Unknown error',
    ]);

    if ($type === 'Alert') {
        Logger::critical('Critical notification failed', [...]);
    }
}
Failed deliveries are terminal — their status cannot be changed programmatically. Use the ForceDelivery Nova action to re-send a delivery that has failed.

Registered message channels

The default notification-center.messages map ships with the following channels:
KeyNotification class
novaNovaNotification
cardCardNotification
emailEmailNotification
smsSmsNotification
callCallNotification
whatsappWhatsAppNotification
webpushWebPushNotification
slackSlackNotification
teamsTeamsNotification
Register additional channels by adding entries to config/notification-center.php:
'messages' => [
    'custom-channel' => \App\Notifications\MyCustomNotification::class,
],

Build docs developers (and LLMs) love