Skip to content

Send notifications and mail

The app’s queued Listener sends one notification and one Mailable after OrderPlaced commits. Its payload is an order ID and application-owned idempotency key, never an Eloquent Model; the fixture records that key before its fake delivery.

Before you begin

Run composer run reference:test:communications and review Listeners.

Handle the queued reaction

The Domain listener reacts only after the committing workflow succeeds.

examples/reference-app/app/Pulsar/Domain/Orders/Listeners/SendOrderConfirmation.php — Queued, after-commit [Domain](/concepts/domain) communication with scalar payloads.

php
<?php

namespace App\Pulsar\Domain\Orders\Listeners;

use App\Models\User;
use App\Pulsar\Domain\Orders\Events\OrderPlaced;
use App\Pulsar\Domain\Orders\Mail\OrderConfirmationMail;
use App\Pulsar\Domain\Orders\Models\Order;
use App\Pulsar\Domain\Orders\Notifications\OrderConfirmationNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;

final class SendOrderConfirmation implements ShouldQueue
{
    public bool $afterCommit = true;

    // #region queued-order-confirmation
    public function handle(OrderPlaced $event): void
    {
        $order = Order::query()->findOrFail($event->orderId);
        $recipient = User::query()->where('tenant_id', $order->tenant_id)->firstOrFail();
        $key = 'order-confirmation-'.$order->id;
        if (DB::table('communication_deliveries')->insertOrIgnore([
            'idempotency_key' => $key,
            'created_at' => now(),
            'updated_at' => now(),
        ]) === 0) {
            return;
        }
        Notification::send($recipient, new OrderConfirmationNotification($order->id, $key));
        Mail::to($recipient)->send(new OrderConfirmationMail($order->id, $key));
    }
    // #endregion
}

Verify and troubleshoot

The focused test fakes mail and notifications, invokes the listener twice, and records one communication key. A crash after that local marker but before an external provider accepts the message is still a failure window; retain the same key and use provider-side deduplication where it is available.