fix(admin): dedup pending maintenance notifications (updated_at claim + staleness threshold)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
493b81aadf
commit
ab5ba2b169
|
|
@ -20,6 +20,9 @@ use Livewire\Component;
|
||||||
#[Layout('layouts.admin')]
|
#[Layout('layouts.admin')]
|
||||||
class Maintenance extends Component
|
class Maintenance extends Component
|
||||||
{
|
{
|
||||||
|
/** A pending notification is presumed failed (retryable) after this long. */
|
||||||
|
private const RETRY_AFTER_MIN = 60;
|
||||||
|
|
||||||
#[Validate('required|string|max:255')]
|
#[Validate('required|string|max:255')]
|
||||||
public string $title = '';
|
public string $title = '';
|
||||||
|
|
||||||
|
|
@ -188,11 +191,20 @@ class Maintenance extends Component
|
||||||
['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event],
|
['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event],
|
||||||
['email' => $customer->email],
|
['email' => $customer->email],
|
||||||
);
|
);
|
||||||
// Delivered rows (sent_at set by the MessageSent listener) are final; a
|
|
||||||
// row that is still unconfirmed (sent_at null) is retryable — re-queue it.
|
// Delivered rows (sent_at set by the MessageSent listener) are final.
|
||||||
if ($delivery->sent_at !== null) {
|
if ($delivery->sent_at !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Otherwise the row is pending. Re-queue only when this is a fresh claim
|
||||||
|
// or the previous attempt is stale enough to be presumed failed — so a
|
||||||
|
// resend while the job is still in flight does not duplicate the email.
|
||||||
|
// updated_at is the last-attempt marker (touched on every dispatch).
|
||||||
|
if (! $delivery->wasRecentlyCreated && $delivery->updated_at->gt(now()->subMinutes(self::RETRY_AFTER_MIN))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$delivery->forceFill(['updated_at' => now()])->save();
|
||||||
$mail->notificationId = $delivery->id;
|
$mail->notificationId = $delivery->id;
|
||||||
Mail::to($customer->email)
|
Mail::to($customer->email)
|
||||||
->locale($customer->locale ?: config('app.locale'))
|
->locale($customer->locale ?: config('app.locale'))
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ it('publishes a window and emails affected customers once (idempotent)', functio
|
||||||
expect(fn () => MaintenanceNotification::query()->create([
|
expect(fn () => MaintenanceNotification::query()->create([
|
||||||
'maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => 'announcement', 'email' => $customer->email,
|
'maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => 'announcement', 'email' => $customer->email,
|
||||||
]))->toThrow(Illuminate\Database\UniqueConstraintViolationException::class);
|
]))->toThrow(Illuminate\Database\UniqueConstraintViolationException::class);
|
||||||
|
|
||||||
|
// Resending while the first mail is still pending must NOT duplicate.
|
||||||
|
Livewire::actingAs(operator('Owner'))->test(Maintenance::class)->call('resend', $window->uuid);
|
||||||
|
Mail::assertQueued(MaintenanceAnnouncementMail::class, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emails a cancellation to customers who were announced', function () {
|
it('emails a cancellation to customers who were announced', function () {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue