web: Mail-Abmeldungen landen im Aktivitäts-Verlauf
Neuer ActivityType "Mail-Abmeldung": Der Unsubscribe-Controller protokolliert Abbestellen (Link-Klick und RFC-8058-One-Click) sowie das Wieder-Aktivieren — nur bei echter Zustandsänderung, wiederholte Klicks erzeugen keine Doppel-Einträge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
215cbcbdbf
commit
ab0030d71e
4 changed files with 55 additions and 4 deletions
|
|
@ -21,6 +21,7 @@ enum ActivityType: string
|
|||
case NewsletterSubscription = 'newsletter_subscription';
|
||||
case SubscriptionChange = 'subscription_change';
|
||||
case MailSent = 'mail_sent';
|
||||
case EmailUnsubscribe = 'email_unsubscribe';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
|
|
@ -37,6 +38,7 @@ enum ActivityType: string
|
|||
self::NewsletterSubscription => 'Newsletter',
|
||||
self::SubscriptionChange => 'Abo',
|
||||
self::MailSent => 'E-Mail',
|
||||
self::EmailUnsubscribe => 'Mail-Abmeldung',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -53,6 +55,7 @@ enum ActivityType: string
|
|||
self::ContactRequest, self::NewsletterSubscription => 'cyan',
|
||||
self::LegalRequest, self::WithdrawalRequest, self::TerminationRequest, self::SubscriptionChange => 'red',
|
||||
self::MailSent => 'purple',
|
||||
self::EmailUnsubscribe => 'orange',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\EmailNotificationType;
|
||||
use App\Models\User;
|
||||
use App\Services\ActivityLog;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
|
@ -19,9 +21,15 @@ use Illuminate\View\View;
|
|||
* rückgängig zu machen. Der POST auf dieselbe URL bedient den
|
||||
* List-Unsubscribe-Header (RFC 8058) — Mail-Clients senden ihn ohne
|
||||
* Nutzer-Interaktion auf der Seite.
|
||||
*
|
||||
* Jede tatsächliche Zustandsänderung landet im Aktivitäts-Verlauf
|
||||
* (Admin ▸ Reports ▸ Verlauf) — wiederholte Klicks erzeugen keine
|
||||
* Doppel-Einträge.
|
||||
*/
|
||||
class EmailUnsubscribeController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ActivityLog $activityLog) {}
|
||||
|
||||
public function show(Request $request, User $user, string $type): View
|
||||
{
|
||||
$notificationType = $this->resolveType($type);
|
||||
|
|
@ -33,7 +41,7 @@ class EmailUnsubscribeController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
$user->optOutOfEmailNotification($notificationType);
|
||||
$this->optOut($user, $notificationType, 'link');
|
||||
|
||||
return view('web.email-unsubscribe', [
|
||||
'state' => 'unsubscribed',
|
||||
|
|
@ -53,7 +61,7 @@ class EmailUnsubscribeController extends Controller
|
|||
{
|
||||
abort_unless($request->hasValidSignature(), 403);
|
||||
|
||||
$user->optOutOfEmailNotification($this->resolveType($type));
|
||||
$this->optOut($user, $this->resolveType($type), 'one-click');
|
||||
|
||||
return response('Sie wurden erfolgreich abgemeldet.');
|
||||
}
|
||||
|
|
@ -64,7 +72,16 @@ class EmailUnsubscribeController extends Controller
|
|||
|
||||
$notificationType = $this->resolveType($type);
|
||||
|
||||
$user->optIntoEmailNotification($notificationType);
|
||||
if (! $user->wantsEmailNotification($notificationType)) {
|
||||
$user->optIntoEmailNotification($notificationType);
|
||||
|
||||
$this->activityLog->record(
|
||||
ActivityType::EmailUnsubscribe,
|
||||
sprintf('%s (%s) hat %s wieder aktiviert.', $user->name, $user->email, $notificationType->unsubscribeLabel()),
|
||||
user: $user,
|
||||
meta: ['type' => $notificationType->value, 'action' => 'resubscribe'],
|
||||
);
|
||||
}
|
||||
|
||||
return view('web.email-unsubscribe', [
|
||||
'state' => 'resubscribed',
|
||||
|
|
@ -72,6 +89,22 @@ class EmailUnsubscribeController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
private function optOut(User $user, EmailNotificationType $notificationType, string $method): void
|
||||
{
|
||||
if (! $user->wantsEmailNotification($notificationType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->optOutOfEmailNotification($notificationType);
|
||||
|
||||
$this->activityLog->record(
|
||||
ActivityType::EmailUnsubscribe,
|
||||
sprintf('%s (%s) hat %s abbestellt.', $user->name, $user->email, $notificationType->unsubscribeLabel()),
|
||||
user: $user,
|
||||
meta: ['type' => $notificationType->value, 'action' => 'unsubscribe', 'method' => $method],
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveType(string $type): EmailNotificationType
|
||||
{
|
||||
$notificationType = EmailNotificationType::tryFrom($type);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ new #[Layout('components.layouts.app'), Title('Verlauf')] class extends Componen
|
|||
:badge="__('Admin Backend')"
|
||||
:eyebrow="__('Administration · Reports')"
|
||||
:title="__('Verlauf')"
|
||||
:description="__('Chronologisches Protokoll der Systemereignisse: Registrierungen, Anmeldungen, Pressemitteilungen, Zahlungen, Formular-Eingänge und versendete E-Mails.')"
|
||||
:description="__('Chronologisches Protokoll der Systemereignisse: Registrierungen, Anmeldungen, Pressemitteilungen, Zahlungen, Formular-Eingänge, versendete E-Mails und Mail-Abmeldungen.')"
|
||||
/>
|
||||
|
||||
{{-- ============== FILTER-PANEL ============== --}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\EmailNotificationType;
|
||||
use App\Enums\NewsletterCampaignStatus;
|
||||
use App\Enums\NewsletterRecipientStatus;
|
||||
|
|
@ -7,6 +8,7 @@ use App\Enums\Portal;
|
|||
use App\Enums\UserPaymentOptionStatus;
|
||||
use App\Jobs\SendNewsletterCampaignMail;
|
||||
use App\Mail\NewsletterCampaignMail;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\NewsletterCampaign;
|
||||
use App\Models\PaymentOption;
|
||||
use App\Models\User;
|
||||
|
|
@ -172,6 +174,19 @@ test('the newsletter unsubscribe link records the opt-out immediately', function
|
|||
|
||||
// Abgemeldete User fallen sofort aus jeder Empfänger-Auswahl.
|
||||
expect(app(CampaignRecipients::class)->query([])->pluck('id'))->not->toContain($user->id);
|
||||
|
||||
// Die Abmeldung steht im Aktivitäts-Verlauf — ein zweiter Klick
|
||||
// erzeugt keinen Doppel-Eintrag.
|
||||
$this->get($url)->assertOk();
|
||||
|
||||
$event = ActivityEvent::query()
|
||||
->where('type', ActivityType::EmailUnsubscribe->value)
|
||||
->sole();
|
||||
|
||||
expect($event->user_id)->toBe($user->id)
|
||||
->and($event->description)->toContain('abbestellt')
|
||||
->and($event->meta['method'])->toBe('link')
|
||||
->and($event->meta['type'])->toBe('newsletter');
|
||||
});
|
||||
|
||||
test('the campaign editor saves drafts and sends via the queue', function () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue