diff --git a/docs/superpowers/specs/2026-07-21-familyguard-flyer-download-design.md b/docs/superpowers/specs/2026-07-21-familyguard-flyer-download-design.md new file mode 100644 index 0000000..eab4f92 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-familyguard-flyer-download-design.md @@ -0,0 +1,88 @@ +# Design: FamilyGuard Flyer-Download mit E-Mail-Gate + +**Datum:** 2026-07-21 +**Status:** Approved + +## Ziel + +Der FamilyGuard-Flyer (`docs/MBO_FamilyGuard_Flyer_01.pdf`) soll auf der Website herunterladbar sein, aber erst nachdem der Interessent seine E-Mail-Adresse eingegeben und der Datenverarbeitung zugestimmt hat. Jonny (jonny@mbo-tech-it.de) soll bei jedem Download per Mail benachrichtigt werden. Die eingegebenen Adressen werden zusätzlich als Lead in Supabase gespeichert. + +## Architektur + +``` +modules/08-familyguard-flyer/ + migrations/ + MIGRATIONS_FLYER_DOWNLOADS.sql ← neue Tabelle flyer_downloads + +app/api/familyguard-flyer/ + route.ts ← POST: Lead speichern, 2 Mails senden, Download-URL zurückgeben + +components/ + FlyerDownloadForm.tsx ← Client-Komponente: E-Mail + DSGVO-Checkbox + Submit + +public/downloads/ + MBO_FamilyGuard_Flyer_01.pdf ← verschoben aus docs/ +``` + +`FlyerDownloadForm` wird nur einmal eingebunden, auf `app/pakete/familyguard/page.tsx` als neue Sektion (`id="flyer-download"`) vor dem Bottom-Callout. Um das Formular nicht doppelt pflegen zu müssen, bekommt `components/FamilyGuardPromo.tsx` auf der Startseite stattdessen einen Button „Flyer herunterladen", der per Anchor-Link zu `/pakete/familyguard#flyer-download` springt. Das erfüllt „FamilyGuard-Seite + Homepage-Promo-Kachel", ohne die Formular-Logik zweimal zu implementieren. + +## Datenmodell + +Neue Tabelle `flyer_downloads` (Migration nach dem Muster der bestehenden `modules/*/migrations/*.sql`): + +```sql +CREATE TABLE IF NOT EXISTS flyer_downloads ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + email TEXT NOT NULL, + flyer TEXT NOT NULL DEFAULT 'familyguard', + dsgvo_einwilligung BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_flyer_downloads_email ON flyer_downloads(email); +CREATE INDEX IF NOT EXISTS idx_flyer_downloads_created_at ON flyer_downloads(created_at DESC); + +ALTER TABLE flyer_downloads DISABLE ROW LEVEL SECURITY; +``` + +`flyer` als Textfeld (statt hartcodiert) hält die Tabelle offen für künftige weitere Flyer, ohne dass das jetzt genutzt wird (YAGNI: nur ein Default-Wert, keine weitere Logik dafür). + +Da im lokalen `.env.local` nur Platzhalter-Credentials für Supabase liegen, kann die Migration nicht von hier aus ausgeführt werden — Jonny führt das SQL manuell in seiner Supabase-Instanz aus (wie bei den bestehenden Modulen). + +## API-Route `POST /api/familyguard-flyer` + +Request: `{ email: string, dsgvoEinwilligung: boolean }` + +Ablauf (analog zu `app/api/contact/route.ts`): +1. Validierung: `email` vorhanden + Format, `dsgvoEinwilligung === true` → sonst 400 +2. Insert in `flyer_downloads` via `createServiceClient()` (Fehler werden geloggt, blockieren aber nicht den Download) +3. `sendeFlyerBenachrichtigung({ email })` → an `process.env.SMTP_TO`-Äquivalent, konkret hartcodiert an `jonny@mbo-tech-it.de` (kein Konfigurationsfeld nötig, da fest gewünscht) +4. `sendeFlyerLink({ email })` → an den Interessenten, mit Link zum PDF (`${APP_URL}/downloads/MBO_FamilyGuard_Flyer_01.pdf`) +5. Response: `{ ok: true, downloadUrl: "/downloads/MBO_FamilyGuard_Flyer_01.pdf" }` + +Beide Mail-Funktionen kommen als neue Exporte in `lib/mailer.ts`, nutzen den bestehenden `sendWithFallback`-Helper (SMTP mit Queue-Fallback), gleiches HTML-Layout wie die bestehenden Mails. + +## Komponente `FlyerDownloadForm.tsx` + +Client-Komponente nach dem Muster von `Contact.tsx`: +- State: `email`, `dsgvoChecked`, `status` (`idle | loading | success | error`) +- Submit → `fetch("/api/familyguard-flyer", { method: "POST", ... })` +- Bei Erfolg: Formular wird durch einen Download-Button (``) ersetzt +- Checkbox-Label verlinkt auf `/datenschutz` +- Bei Fehler: Fehlermeldung wie im bestehenden Kontaktformular + +## Fehlerbehandlung + +- SMTP nicht erreichbar → automatischer Fallback in die bestehende `email_queue` (kein Nutzer-Impact) +- Supabase-Insert schlägt fehl → wird geloggt, Download funktioniert trotzdem +- Ungültige/leere E-Mail oder fehlende Einwilligung → 400 mit Fehlermeldung, Formular zeigt Inline-Fehler +- PDF-Datei ist eine normale statische Datei unter `/public/downloads` — kein Access-Control-Layer, das Gate ist rein UX-seitig (bewusste Entscheidung, siehe Architektur-Diskussion: für einen Marketing-Flyer ist ein hartes Access-Gating unverhältnismäßig) + +## Testing + +Manueller Test über `npm run dev`: +1. Formular auf `/pakete/familyguard` ausfüllen (mit und ohne Checkbox) → Validierung prüfen +2. Nach Erfolg: Download-Button erscheint, PDF öffnet sich korrekt +3. Eintrag in `flyer_downloads` prüfen (sofern echte Supabase-Zugangsdaten vorhanden sind) +4. Beide Mails prüfen (Empfang bei SMTP_TO/jonny@mbo-tech-it.de und bei der Test-E-Mail-Adresse) bzw. Queue-Eintrag bei fehlendem SMTP +5. Homepage-Kachel: Link führt korrekt zur Formular-Sektion auf der FamilyGuard-Seite