Neues Modul unter Admin ▸ Newsletter (nur users:manage): Kampagnen mit Betreff + leicht formatiertem Text (Purifier-Profil newsletter: fett/ kursiv, Listen, Links) im zentralen Mail-Layout. Empfänger-Filter auf der User-Datenbank: Portal, Legacy-Vertrag (Bestandsschutz), Login innerhalb/außerhalb X Monaten, Sortierung nach Login/Erstellung — mit Live-Zähler und Vorschau im Editor. Versand friert die Empfängerliste als Protokoll ein (newsletter_ campaign_recipients) und stellt pro Empfänger einen Queue-Job ein; der Opt-out wird im Worker erneut geprüft. Jede Mail trägt den signierten One-Click-Abmelde-Link (neuer EmailNotificationType:: Newsletter, bestehende Unsubscribe-Infrastruktur inkl. RFC 8058); die Abmeldung greift sofort für alle künftigen Auswahlen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
3.4 KiB
PHP
105 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Enums\EmailNotificationType;
|
|
use App\Enums\NewsletterCampaignStatus;
|
|
use App\Enums\NewsletterRecipientStatus;
|
|
use App\Mail\NewsletterCampaignMail;
|
|
use App\Models\NewsletterCampaign;
|
|
use App\Models\NewsletterCampaignRecipient;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Versendet eine Newsletter-Kampagne an genau einen Empfänger (Protokoll-
|
|
* Zeile). Pro Empfänger ein Job, damit Fehler einzeln nachvollziehbar
|
|
* bleiben und Retries keinen Doppelversand an die übrige Liste auslösen.
|
|
*
|
|
* Der Opt-out wird hier — zum tatsächlichen Versandzeitpunkt — erneut
|
|
* geprüft: Wer sich nach Versandstart abmeldet, wird übersprungen.
|
|
*/
|
|
class SendNewsletterCampaignMail implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public function __construct(public readonly int $recipientId) {}
|
|
|
|
public function handle(): void
|
|
{
|
|
$recipient = NewsletterCampaignRecipient::query()
|
|
->with(['campaign', 'user'])
|
|
->find($this->recipientId);
|
|
|
|
if ($recipient === null || $recipient->status !== NewsletterRecipientStatus::Pending) {
|
|
return;
|
|
}
|
|
|
|
$campaign = $recipient->campaign;
|
|
$user = $recipient->user;
|
|
|
|
if ($user === null || ! $user->is_active) {
|
|
$this->conclude($recipient, NewsletterRecipientStatus::Failed, 'failed_count', 'Benutzer gelöscht oder deaktiviert.');
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $user->wantsEmailNotification(EmailNotificationType::Newsletter)) {
|
|
$this->conclude($recipient, NewsletterRecipientStatus::SkippedOptOut, 'skipped_count');
|
|
|
|
return;
|
|
}
|
|
|
|
Mail::to($recipient->email)->send(new NewsletterCampaignMail($campaign, $user));
|
|
|
|
$this->conclude($recipient, NewsletterRecipientStatus::Sent, 'sent_count');
|
|
}
|
|
|
|
/**
|
|
* Endgültiger Fehlschlag nach allen Versuchen des Workers.
|
|
*/
|
|
public function failed(\Throwable $exception): void
|
|
{
|
|
$recipient = NewsletterCampaignRecipient::query()->find($this->recipientId);
|
|
|
|
if ($recipient === null || $recipient->status !== NewsletterRecipientStatus::Pending) {
|
|
return;
|
|
}
|
|
|
|
$this->conclude($recipient, NewsletterRecipientStatus::Failed, 'failed_count', $exception->getMessage());
|
|
}
|
|
|
|
private function conclude(
|
|
NewsletterCampaignRecipient $recipient,
|
|
NewsletterRecipientStatus $status,
|
|
string $campaignCounter,
|
|
?string $error = null,
|
|
): void {
|
|
$recipient->update([
|
|
'status' => $status->value,
|
|
'error' => $error !== null ? Str::limit($error, 480) : null,
|
|
'sent_at' => $status === NewsletterRecipientStatus::Sent ? now() : null,
|
|
]);
|
|
|
|
$campaign = NewsletterCampaign::query()->find($recipient->newsletter_campaign_id);
|
|
|
|
if ($campaign === null) {
|
|
return;
|
|
}
|
|
|
|
$campaign->increment($campaignCounter);
|
|
|
|
$hasPending = $campaign->recipients()
|
|
->where('status', NewsletterRecipientStatus::Pending->value)
|
|
->exists();
|
|
|
|
if (! $hasPending && $campaign->status === NewsletterCampaignStatus::Sending) {
|
|
$campaign->update([
|
|
'status' => NewsletterCampaignStatus::Sent->value,
|
|
'sent_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
}
|