feat(marketing): Audit-Anfrageformular als Livewire-Komponente

Das markemacht.de-Anfrageformular (Audit-Modal) war nach der Laravel-
Integration ohne Endpoint. Es wird jetzt serverseitig über Livewire
verarbeitet statt über das alte statische PHP-Skript.

- AuditAnfrage-Mailable + Mail-View, Reply-To = Absender
- config/marketing.php: Empfänger via MARKETING_ANFRAGE_TO (Default info@markemacht.de)
- Livewire-SFC web.audit-form: Validierung (dt.), Honeypot, Rate-Limit (3/10 Min),
  dispatch('audit-sent') schließt das Modal
- Modal-Markup auf <livewire:web.audit-form /> umgestellt
- Feature-Tests (Validierung, Versand, Honeypot, Throttle)
- Entwicklungsplan unter dev/audit-formular-cookies/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-18 13:20:21 +00:00
parent 9c6b7ed4f3
commit 1f7c0aa430
7 changed files with 527 additions and 0 deletions

47
app/Mail/AuditAnfrage.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class AuditAnfrage extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
public string $anfrageName,
public string $anfrageEmail,
public string $anfrageMessage,
) {}
/**
* Get the message envelope.
*
* Antwort geht direkt an den Absender der Anfrage (Reply-To).
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Neue Anfrage über markemacht.de',
replyTo: [new Address($this->anfrageEmail, $this->anfrageName)],
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'mail.audit-anfrage',
);
}
}

18
config/marketing.php Normal file
View file

@ -0,0 +1,18 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Marketing-Konfiguration (markemacht.de)
|--------------------------------------------------------------------------
|
| Einstellungen rund um die öffentliche Landingpage. Aktuell der Empfänger
| für Anfragen aus dem Audit-/Kontaktformular. Lokal landet alles in Mailpit
| (siehe MAIL_*); der echte Empfänger wird per ENV auf dem Server gesetzt.
|
*/
'anfrage_empfaenger' => env('MARKETING_ANFRAGE_TO', 'info@markemacht.de'),
];

View file

@ -0,0 +1,164 @@
# Umsetzungsplan — Audit-Formular (Laravel) + Cookie-/Datenschutz-Anpassung
**Kontext:** Die statische markemacht.de wurde in die Laravel-App integriert. Zwei offene
Punkte: (1) das Anfrage-/Audit-Formular ist nach der Migration tot und muss als Laravel-Feature
wieder funktionieren, (2) Laravel setzt jetzt System-Cookies — der Hinweis „Keine Cookies" und
die Datenschutzerklärung müssen ehrlich werden.
> Dieses Dokument ist **lebend**: Es wird nach jedem Checkpoint aktualisiert. Wer die Arbeit
> (auch an einem anderen Tag) wieder aufnimmt, liest zuerst **📍 Aktueller Stand** und macht
> beim ersten offenen Checkpoint weiter. Jeder Checkpoint ist so geschnitten, dass der Code
> danach in sich konsistent ist (Tests grün, nichts halb-fertig).
---
## 📍 Aktueller Stand _(immer zuerst lesen)_
- **Phase:** ✅ 1 + 2 abgeschlossen.
- **Zuletzt erledigt:** **Phase 2 komplett & verifiziert** — mm-wirkung-Cookie entfernt (JS+Blade,
aus Build bestätigt), Footer „DSGVO-konform · keine Weitergabe", Datenschutz neu, 61/61 Tests grün,
Build neu (`markemacht-NNJhNDv3.js`).
- **Als Nächstes:** Phase 3 (Klarheits-Check) — eigener Plan/PR, separat. ODER offene Commit-Grenze klären.
- **Offen (Entscheidung User):** Commit-Grenze. Branch ist komplett WIP/uncommittet; die Phase-1/2-
Dateien sind NICHT separat committet, um die laufende Integration nicht zu fragmentieren.
- **Branch:** `feat/markemacht-laravel-migration`
- **Wiederaufnahme:** Diesen Block lesen → ersten offenen `[ ]`-Checkpoint suchen → dort
„Verifikation" am Ende ausführen, um den Ist-Zustand zu bestätigen, dann weiterarbeiten.
---
## Gesperrte Entscheidungen (nicht mehr offen)
- Audit-Formular wird **Livewire-SFC** `web.audit-form` (Projekt-Konvention: Single-File-Component
mit ⚡-Präfix unter `resources/views/components/web/⚡audit-form.blade.php`, **nicht** class-based).
Tag im Markup: `<livewire:web.audit-form />`. Modal-Chrome (öffnen/schließen/Fokus in
`markemacht.js`) bleibt Vanilla-JS unverändert.
- Mailversand über **Mailable** + `config/mail.php`. **Lokal: Mailpit** (Mutterschiff). Echte
SMTP-Creds kommen später in die Server-`.env` — Code liest nur aus `config('mail')`.
- Klarheits-Check ist **nicht** Teil dieses Plans (eigenes Feature/PR, Controller-basiert).
- Cookie-Strategie: nur technisch notwendige Cookies (`*_session`, `XSRF-TOKEN`).
`mm-wirkung`-Cookie wird **entfernt** → localStorage-only. **Kein Cookie-Banner** (kein Rechtsrat).
- Footer-Text statt „Keine Cookies": **„DSGVO-konform · keine Weitergabe"**.
---
## Fortschritt (Überblick)
### Phase 1 — Audit-Formular funktionsfähig (Livewire)
- [x] **CP 1.1** Mail-Fundament: `AuditAnfrage`-Mailable + Mail-View, Config/ENV geprüft
- [x] **CP 1.2** Livewire-Komponente `AuditForm` (Validierung, Honeypot, Throttle, Versand)
- [x] **CP 1.3** Modal-Markup auf `<livewire:web.audit-form />` umstellen; Livewire-Assets sicherstellen
- [x] **CP 1.4** Tests (`Mail::fake`, Validierung, Honeypot, Throttle) + Pint — 58/58 grün
- [x] **CP 1.5** Manuelle Verifikation in Mailpit (Commit: an User-Commit-Grenze gekoppelt)
### Phase 2 — Cookies & Datenschutz ehrlich
- [x] **CP 2.1** `mm-wirkung`-Cookie entfernen (JS + Blade-Fallback) → localStorage-only
- [x] **CP 2.2** Footer-Text „Keine Cookies" → „DSGVO-konform · keine Weitergabe"
- [x] **CP 2.3** Datenschutz: Cookie-Absatz neu (technisch notwendige Cookies) + Aussage Datenweitergabe
- [x] **CP 2.4** Tests (Footer-Text, Datenschutz-Inhalt) + Pint — 61/61 grün, Build neu
### Phase 3 — (separat, nur notiert)
- Klarheits-Check: Blade + JS + `POST /klarheits-check/auswerten` (Anthropic-Proxy),
inkl. KI-Datenschutz-Absatz. Eigener Plan/PR.
---
## Phase 1 — Audit-Formular (Detail)
**Altes Verhalten (Referenz):** `_markemacht.de/assets/php/send-anfrage.php` — POST, Felder
`name`, `email`, `message`, `privacy` (Pflicht-Checkbox), Honeypot `company`/`url`, Versand per
PHPMailer/SMTP, JSON-Antwort `{ok, message}`. Empfänger/Betreff aus `config.php`.
### CP 1.1 — Mail-Fundament
- [ ] `php artisan make:mail AuditAnfrage --markdown=mail.audit-anfrage` (oder plain Blade-View)
- [ ] Mailable: Felder `name`, `email`, `message`; `Reply-To` = Absender; `Subject`
„Neue Anfrage über markemacht.de"; Empfänger aus `config` (z. B. `config('mail.from')`
oder neue `config('marketing.anfrage_empfaenger')`).
- [ ] Empfänger-Adresse: entscheiden, wo sie herkommt (ENV `MARKETING_ANFRAGE_TO`, Default
`info@markemacht.de`). In `config/` ablegen, nicht hardcoden.
- **Verifikation:** `php artisan test` läuft weiter grün; Mailable-Klasse existiert.
- **Wiederaufnahme-Notiz (erledigt 2026-06-18):** Mailable `App\Mail\AuditAnfrage`
(ctor: `anfrageName`, `anfrageEmail`, `anfrageMessage`; `replyTo` = Absender; Subject „Neue
Anfrage über markemacht.de"). View `resources/views/mail/audit-anfrage.blade.php`. Empfänger:
`config('marketing.anfrage_empfaenger')` ← `MARKETING_ANFRAGE_TO`, Default `info@markemacht.de`
(neue Datei `config/marketing.php`). Render + Inhalt verifiziert.
### CP 1.2 — Livewire-Komponente
- [ ] `php artisan make:livewire Web/AuditForm` (Livewire-4-Konvention im Projekt prüfen)
- [ ] Properties + Rules: `name` required, `email` required/email, `message` required,
`privacy` accepted; Honeypot `company`/`url` (müssen leer sein).
- [ ] `submit()`: Honeypot-Check (still „erfolgreich" bei Befüllung), Validierung,
`RateLimiter` (z. B. 3 Anfragen / 10 Min pro IP), `Mail::to(...)->send(new AuditAnfrage(...))`,
Erfolg → `$this->dispatch('audit-sent')` (das hört `markemacht.js:139` bereits),
Felder leeren, Status-Property setzen.
- [ ] View `resources/views/livewire/web/audit-form.blade.php`: 1:1 die bestehenden Felder/Klassen
aus `footer.blade.php` übernehmen (Designsystem unverändert), `wire:model`, `wire:submit`,
`@error`, `wire:loading`, `aria-live`-Status.
- **Verifikation:** Komponente rendert isoliert (`Livewire::test`).
- **Wiederaufnahme-Notiz:** _(Throttle-Limits, Event-Name, Validierungsregeln dokumentieren)_
### CP 1.3 — Einbau ins Modal
- [ ] In `resources/views/web/partials/footer.blade.php` den `<form data-audit-form …>`-Block
(Zeilen ~45–77) durch `<livewire:web.audit-form />` ersetzen. Hülle `[data-audit-form-panel]`
behalten oder in die Komponente ziehen — Modal-Chrome bleibt.
- [ ] Sicherstellen, dass Livewire-Assets auf Marketing-Seiten geladen werden (Default-Auto-Inject
prüfen; ggf. `@livewireScripts`/`@livewireStyles` ins Marketing-Layout). **Achtung:** vorher
lud die Marketing-Seite KEIN Livewire — hier genau testen (Konsole auf Fehler prüfen).
- **Verifikation:** Seite `/audit` und Footer-Modal öffnen, Formular sichtbar, keine JS-Fehler.
- **Wiederaufnahme-Notiz:** _(ob Layout angepasst werden musste)_
### CP 1.4 — Tests
- [ ] Feature-/Livewire-Test in `tests/Feature/Web/`: erfolgreicher Versand (`Mail::fake`,
`assertSent`), Validierungsfehler, Honeypot (kein Versand, aber „ok"), Throttle.
- [ ] `php artisan test --compact --filter=Audit`
- [ ] `vendor/bin/pint --dirty`
- **Verifikation:** alle neuen Tests grün, Gesamt-Suite grün.
### CP 1.5 — Manuell + Commit
- [ ] Lokal echtes Absenden → Mail erscheint in **Mailpit**.
- [ ] Commit: `feat(marketing): Audit-Anfrageformular als Livewire-Komponente`.
- **Verifikation:** `git log` zeigt Commit; Arbeitsbaum sauber bzgl. Phase 1.
---
## Phase 2 — Cookies & Datenschutz (Detail)
### CP 2.1 — mm-wirkung-Cookie entfernen
- [ ] `resources/js/web/markemacht.js:28` (`document.cookie = …mm-wirkung…`) entfernen.
- [ ] `resources/views/web/layouts/marketing.blade.php:53` Cookie-Fallback im Inline-Script
entfernen (localStorage genügt, läuft vor erstem Paint).
- [ ] `npm run build` nötig (JS/Blade-Asset) — User bitten oder ausführen.
- **Verifikation:** Theme-Umschalter funktioniert weiter; im DevTools-Application-Tab kein
`mm-wirkung`-Cookie mehr, nur localStorage.
### CP 2.2 — Footer-Text
- [ ] `resources/views/web/partials/footer.blade.php:6` „Keine Cookies" →
„DSGVO-konform · keine Weitergabe".
- **Verifikation:** Footer zeigt neuen Text.
### CP 2.3 — Datenschutzerklärung
- [ ] `resources/views/web/datenschutz.blade.php:225-226`: „verwendet keine Cookies" ersetzen durch
Absatz **„Technisch notwendige Cookies"**: `markemacht_session` + `XSRF-TOKEN`, Zweck
(Bereitstellung/Absicherung der Formulare), Art. 6 Abs. 1 lit. f DSGVO / § 25 Abs. 2 TTDSG,
keine Einwilligung nötig, kein Tracking.
- [ ] localStorage-Absatz (228–233) bleibt korrekt → behalten.
- [ ] Aussage zur **Datenweitergabe** ergänzen/prüfen (Umami self-hosted, keine Dritten) — passt
zur Footer-Aussage und zum bestehenden Umami-Absatz (285+).
- **Verifikation:** Datenschutzseite konsistent, keine Aussage „keine Cookies" mehr.
### CP 2.4 — Tests + Commit
- [ ] Test in `tests/Feature/Web/`: Footer enthält neuen Text / nicht mehr „Keine Cookies";
Datenschutz enthält „Technisch notwendige Cookies".
- [ ] `php artisan test --compact` + `vendor/bin/pint --dirty`
- [ ] Commit: `feat(marketing): Cookie-Hinweis & Datenschutz an Laravel-Integration angepasst`.
- **Verifikation:** Suite grün, Commit gesetzt.
---
## Changelog (Arbeitsprotokoll)
| Datum | Checkpoint | Ergebnis / Notiz |
|---|---|---|
| 2026-06-18 | Setup | Plan angelegt, Entscheidungen gesperrt, Footer-Text = „DSGVO-konform · keine Weitergabe". |
| 2026-06-18 | Phase 1 | Mailable `AuditAnfrage` + Config `marketing.anfrage_empfaenger`, Livewire-SFC `web.audit-form`, Modal-Einbau, Tests, Mailpit-E2E. |
| 2026-06-18 | Phase 2 | mm-wirkung-Cookie raus (localStorage-only), Footer-Hinweis, Datenschutz „Technisch notwendige Cookies", Tests, Build. |

View file

@ -0,0 +1,140 @@
<?php
use App\Mail\AuditAnfrage;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
new class extends Component
{
public string $name = '';
public string $email = '';
public string $message = '';
public bool $privacy = false;
/** Honeypot-Felder — für Menschen unsichtbar, von Bots befüllt. */
public string $company = '';
public string $url = '';
/** Statusmeldung für aria-live, sichtbar nach Absenden. */
public string $status = '';
public bool $sent = false;
/**
* @return array<string, string>
*/
protected function rules(): array
{
return [
'name' => 'required|string|max:120',
'email' => 'required|email|max:190',
'message' => 'required|string|max:5000',
'privacy' => 'accepted',
];
}
/**
* @return array<string, string>
*/
protected function messages(): array
{
return [
'name.required' => 'Bitte geben Sie Ihren Namen an.',
'email.required' => 'Bitte geben Sie Ihre E-Mail-Adresse an.',
'email.email' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.',
'message.required' => 'Bitte beschreiben Sie kurz Ihr Anliegen.',
'privacy.accepted' => 'Bitte akzeptieren Sie die Datenschutzerklärung.',
];
}
public function submit(): void
{
// Spam-Schutz: Bots befüllen die versteckten Felder. Wir tun so, als sei alles gut.
if ($this->company !== '' || $this->url !== '') {
$this->markeAlsGesendet('Danke. Ihre Anfrage wurde aufgenommen.');
return;
}
$this->validate();
$key = 'audit-anfrage:'.request()->ip();
if (RateLimiter::tooManyAttempts($key, maxAttempts: 3)) {
$this->status = 'Zu viele Anfragen in kurzer Zeit. Bitte versuchen Sie es später erneut oder schreiben Sie an info@markemacht.de.';
return;
}
RateLimiter::hit($key, decaySeconds: 600);
Mail::to(config('marketing.anfrage_empfaenger'))->send(
new AuditAnfrage($this->name, $this->email, $this->message),
);
$this->markeAlsGesendet('Danke. Ihre Anfrage wurde versendet.');
}
private function markeAlsGesendet(string $meldung): void
{
$this->reset(['name', 'email', 'message', 'privacy', 'company', 'url']);
$this->status = $meldung;
$this->sent = true;
// markemacht.js lauscht auf dieses Event und schließt das Modal nach kurzer Zeit.
$this->dispatch('audit-sent');
}
};
?>
<div>
@if ($sent)
<p class="form-status body-text" aria-live="polite">{{ $status }}</p>
@else
<form class="modal-form stack-16" wire:submit="submit" novalidate>
{{-- Honeypot: außerhalb des sichtbaren Flusses, für Bots --}}
<div aria-hidden="true" tabindex="-1" style="position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden;">
<label>Firma<input type="text" tabindex="-1" autocomplete="off" wire:model="company" /></label>
<label>URL<input type="text" tabindex="-1" autocomplete="off" wire:model="url" /></label>
</div>
<div class="form-field">
<label class="meta" for="audit-name">Name</label>
<input id="audit-name" type="text" autocomplete="name" wire:model="name" />
@error('name') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
</div>
<div class="form-field">
<label class="meta" for="audit-email">E-Mail</label>
<input id="audit-email" type="email" autocomplete="email" wire:model="email" />
@error('email') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
</div>
<div class="form-field">
<label class="meta" for="audit-message">Worum geht es?</label>
<textarea id="audit-message" rows="5" wire:model="message"></textarea>
@error('message') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
</div>
<label class="form-check">
<input type="checkbox" wire:model="privacy" />
<span class="form-switch" aria-hidden="true"></span>
<span>Ich akzeptiere die <a href="{{ route('datenschutz') }}">Datenschutzerklärung</a>.</span>
</label>
@error('privacy') <span class="form-error meta" role="alert">{{ $message }}</span> @enderror
<p class="form-status body-text" aria-live="polite">{{ $status }}</p>
<div>
<button class="btn btn-nav-cta" type="submit" wire:loading.attr="disabled" wire:target="submit">
<span wire:loading.remove wire:target="submit">Absenden</span>
<span wire:loading wire:target="submit">Senden …</span>
<svg class="icon" aria-hidden="true"><use href="/web/markemacht/icons.svg#arrow-forward"></use></svg>
</button>
</div>
</form>
@endif
</div>

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Neue Anfrage über markemacht.de</title>
</head>
<body style="font-family: -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; color: #1a1a1a; line-height: 1.5;">
<h2 style="margin-bottom: 1rem;">Neue Anfrage über markemacht.de</h2>
<p>
<strong>Name:</strong> {{ $anfrageName }}<br>
<strong>E-Mail:</strong> {{ $anfrageEmail }}
</p>
<p><strong>Nachricht:</strong></p>
<p style="white-space: pre-wrap;">{{ $anfrageMessage }}</p>
</body>
</html>

View file

@ -0,0 +1,48 @@
@php $sprite = '/web/markemacht/icons.svg'; @endphp
<footer class="site-footer">
<div class="footer-inner">
<div class="footer-meta">
<p class="footer-copy">© <strong>Marke macht<span class="accent">.</span></strong> · Denkraum für Unternehmer</p>
<p class="footer-note">Keine Cookies</p>
</div>
<div class="footer-links">
<a class="footer-link" href="{{ route('impressum') }}">Impressum</a>
<a class="footer-link" href="{{ route('datenschutz') }}">Datenschutz</a>
<button class="footer-link" type="button" data-audit-modal-open>Anfrage</button>
</div>
</div>
</footer>
<div class="modal-backdrop" data-audit-modal aria-hidden="true">
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="audit-modal-title" aria-describedby="audit-modal-desc">
<button class="modal-close" type="button" data-audit-modal-close aria-label="Anfragefenster schließen">
<svg class="icon" aria-hidden="true"><use href="{{ $sprite }}#close"></use></svg>
</button>
<div class="stack-32">
<div class="stack-16">
<span class="meta accent">Anfrage</span>
<h2 class="section-headline" id="audit-modal-title" style="max-width: 16ch;">Kurzer Erstkontakt.</h2>
<p class="body-text max-prose" id="audit-modal-desc">
Wählen Sie den direkten E-Mail-Weg oder senden Sie eine knappe Anfrage über das Formular.
</p>
</div>
<div class="card stack-16">
<span class="meta">Direkt per E-Mail</span>
<p class="body-text">
Öffnet Ihr E-Mail-Programm mit vorausgefülltem Betreff.
</p>
<div>
<a class="btn btn-nav-cta" href="mailto:info@markemacht.de?subject=Anfrage%20Marken-Klarheits-Audit">
E-Mail öffnen
<svg class="icon" aria-hidden="true"><use href="{{ $sprite }}#arrow-forward"></use></svg>
</a>
</div>
</div>
{{-- Anfrageformular: Livewire-Komponente (Validierung + Versand serverseitig). --}}
<livewire:web.audit-form />
</div>
</div>
</div>

View file

@ -0,0 +1,92 @@
<?php
use App\Mail\AuditAnfrage;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Livewire;
use function Pest\Laravel\get;
test('das anfrageformular ist im audit-modal eingebunden', function () {
get(route('audit'))
->assertOk()
->assertSee('wire:submit', false)
->assertSee('livewire', false);
});
test('leeres formular zeigt validierungsfehler und versendet nichts', function () {
Mail::fake();
Livewire::test('web.audit-form')
->call('submit')
->assertHasErrors(['name', 'email', 'message', 'privacy']);
Mail::assertNothingSent();
});
test('ungültige e-mail wird abgewiesen', function () {
Livewire::test('web.audit-form')
->set('name', 'Max Muster')
->set('email', 'keine-email')
->set('message', 'Text')
->set('privacy', true)
->call('submit')
->assertHasErrors(['email']);
});
test('gültige anfrage wird versendet und löst das schließen-event aus', function () {
Mail::fake();
Livewire::test('web.audit-form')
->set('name', 'Max Muster')
->set('email', 'max@example.com')
->set('message', 'Wir brauchen Klarheit.')
->set('privacy', true)
->call('submit')
->assertHasNoErrors()
->assertSet('sent', true)
->assertDispatched('audit-sent');
Mail::assertSent(AuditAnfrage::class, function (AuditAnfrage $mail) {
return $mail->hasTo(config('marketing.anfrage_empfaenger'))
&& $mail->hasReplyTo('max@example.com');
});
});
test('honeypot-feld unterdrückt den versand, meldet aber erfolg', function () {
Mail::fake();
Livewire::test('web.audit-form')
->set('name', 'Bot')
->set('email', 'bot@example.com')
->set('message', 'spam')
->set('privacy', true)
->set('company', 'ausgefuellt')
->call('submit')
->assertHasNoErrors()
->assertSet('sent', true);
Mail::assertNothingSent();
});
test('zu viele anfragen werden gedrosselt', function () {
Mail::fake();
RateLimiter::clear('audit-anfrage:127.0.0.1');
$sende = fn () => Livewire::test('web.audit-form')
->set('name', 'Max Muster')
->set('email', 'max@example.com')
->set('message', 'Anfrage')
->set('privacy', true)
->call('submit');
// Drei erlaubte Versuche …
$sende();
$sende();
$sende();
// … der vierte wird gedrosselt: kein vierter Versand.
$sende()->assertSet('sent', false);
Mail::assertSent(AuditAnfrage::class, 3);
});