CluPilotCloud/app/Services/Maintenance/MaintenanceNotifier.php

75 lines
2.7 KiB
PHP

<?php
namespace App\Services\Maintenance;
use App\Mail\MaintenanceAnnouncementMail;
use App\Mail\MaintenanceCancelledMail;
use App\Models\Customer;
use App\Models\MaintenanceNotification;
use App\Models\MaintenanceWindow;
use Illuminate\Mail\Mailable;
use Illuminate\Support\Facades\Mail;
/**
* Ledger-guarded delivery of maintenance emails. Shared by the admin console and
* the mail-event listeners so idempotency and the announcement/cancellation
* pairing are enforced in exactly one place.
*/
class MaintenanceNotifier
{
/** A pending notification is presumed failed (retryable) after this long. */
public const RETRY_AFTER_MIN = 60;
public function announce(MaintenanceWindow $window): void
{
foreach ($window->affectedCustomers() as $customer) {
$this->deliver($window, $customer, 'announcement', new MaintenanceAnnouncementMail($window, $customer));
}
}
/** Notify customers who actually received the announcement that it's cancelled. */
public function notifyCancellation(MaintenanceWindow $window): void
{
$ids = MaintenanceNotification::query()
->where('maintenance_window_id', $window->id)
->where('event', 'announcement')
->whereNotNull('sent_at')
->pluck('customer_id');
foreach (Customer::query()->whereIn('id', $ids)->get() as $customer) {
$this->deliver($window, $customer, 'cancelled', new MaintenanceCancelledMail($window, $customer));
}
}
/** Queue one mail per (window, customer, event), idempotently and retryably. */
public function deliver(MaintenanceWindow $window, Customer $customer, string $event, Mailable $mail): void
{
$delivery = MaintenanceNotification::query()->firstOrCreate(
['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event],
['email' => $customer->email],
);
if ($delivery->sent_at !== null) {
return; // already delivered — final
}
if (! $delivery->wasRecentlyCreated && $delivery->updated_at->gt(now()->subMinutes(self::RETRY_AFTER_MIN))) {
return; // still pending/in-flight — don't duplicate
}
$wasNew = $delivery->wasRecentlyCreated;
$priorUpdatedAt = $delivery->updated_at;
$delivery->forceFill(['updated_at' => now()])->save();
$mail->notificationId = $delivery->id;
try {
Mail::to($customer->email)
->locale($customer->locale ?: config('app.locale'))
->queue($mail);
} catch (\Throwable $e) {
$wasNew ? $delivery->delete() : $delivery->forceFill(['updated_at' => $priorUpdatedAt])->save();
throw $e;
}
}
}