presseportale/docs/Decision-Update/Entwicklungsplan Meldeverfahren.md
Kevin Adametz 9afbedddeb recht: Meldeverfahren v2 Phasen 5+7 — DSGVO-Strukturformular, 10-€-Checkout mit KI-Vorprüfung, Triage & Panel-Modal
Phase 7 (Spur D, E2 revidiert): KI-Vorprüfung VOR der Zahlung im Modal —
rot lehnt ohne Zahlung ab, gelb/grün führt zum Stripe-Checkout
(SinglePurchaseType::Change, 10 € netto), nach Zahlungseingang übernimmt
der Webhook die geprüfte Fassung sofort (applyPaid: Snapshot, Sperre,
Verlauf). Nur einmal je PM; Obergrenze legal.change_max_distance_percent
(50 %) verhindert die verdeckte Selbst-Depublizierung.

Phase 5 (Spur B): strukturiertes Schwärzungsformular mit Pflicht-Vorschau
im verifizierten Web-Bereich UND als Modal im Kunden-Panel; Admin-Editor
wird aus payload.redaction vorbefüllt. Umsetzungsmail LegalRequestExecuted
an Antragsteller, Bestätigungsmail PressReleaseChangeApplied (Spur A/D,
neuer abbestellbarer Typ press_release_corrected).

Außerdem:
- Re-Sync-Sperre + Sitemap-Invalidierung fest in depublish() verankert
  (Lücke im bezahlten Webhook-Pfad geschlossen)
- Öffentliche „Rechtliches"-Seite als zweistufige Triage: primär der
  Portal-Anmeldelink, Formular nur Fallback mit Identifizierungs-Hinweis;
  Store auf report/dsgvo/personal_rights gehärtet
- Panel-Button „Ändern/Melden" öffnet Anliegen-Modal (4 Spuren) statt
  des Web-Formulars
- Pressekontakt-Fallback für Legacy-Bestände (30k PMs ohne Verknüpfung):
  Detailseite + pressContactEmail() nutzen PM-Kontakt → Firmen-Kontakt →
  Firmendaten
- Korrektur-Gate entity-fest (Editor-Normalisierung zählt nicht als
  Änderung), toEditorHtml() in Admin-Edit + Korrektur-Panel,
  Wochenlimit-Hinweis in Text und Bremsen-Meldung

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:02:57 +00:00

37 KiB
Raw Blame History

Entwicklungsplan: Meldeverfahren v2 (Selbstbedienungs-Flow „PM melden")

Stand: 22.07.2026 Grundlage: Lösch- und Änderungsanfragen.md (Entscheidungsachse, Free/Paid-Matrix, §11 Self-Service-Ausbau) und Pressemitteilung melden.md (Vier-Spuren-Flow) Ziel: Portal übernimmt den gesamten Post-Publication-Zustand selbst (connektar = write-once). Vier Spuren: Kleine Korrektur (frei), DSGVO-Anonymisierung (frei, Admin-Gate), Depublizierung (kostenpflichtig), Änderung (kostenpflichtig, KI-Freigabelauf).


0. Ist-Stand: Was bereits existiert (Bauen auf, nicht neu)

Die Codebasis enthält bereits ein Meldeverfahren v0.1 und fast alle benötigten Bausteine. Der Plan ist ein Ausbau, kein Neubau.

Baustein Vorhanden Relevante Dateien
Melde-Queue (DSGVO / Persönlichkeitsrecht / Meldung) ✅ v0.1: Intake → Admin-Queue, manuelle Erledigung app/Models/LegalRequest.php, app/Enums/LegalRequestType.php, app/Enums/LegalRequestStatus.php
Öffentliche Meldeformulare (Honeypot, Rate-Limit, DSA-Eingangsbestätigung M-{id}) ✅ melden/{way} + PM-gebunden /release/{slug}/rechtliches app/Http/Controllers/Web/ReportFormController.php, app/Http/Controllers/LegalRequestController.php, routes/legal.php
Admin-Queue „Recht & Compliance" (can:compliance:manage) ✅ Status-Handling (open/in_progress/resolved/rejected), aber keine Ausführungs-Aktionen (kein Anonymisieren/Depublizieren aus der Queue) resources/views/livewire/admin/legal-requests/{index,show}.blade.php, routes/admin.php
Magic-Link-Infrastruktur (sha256-Token, purpose, payload, TTL, Consume) ✅ purposes login, press_release_access app/Models/MagicLink.php, app/Services/Auth/MagicLinkGenerator.php, app/Http/Controllers/Auth/MagicLinkConsumeController.php, app/Console/Commands/PurgeMagicLinks.php
Depublizierung + Tombstone ✅ depublish() → Archived + depublished_at, Detail-URL liefert HTTP 410 Tombstone app/Services/PressRelease/PressReleaseService.php:394, app/Http/Controllers/Web/ReleaseDetailController.php, resources/views/web/release-tombstone.blade.php
Bezahlte Depublizierung (Stripe-Checkout + Webhook-Fulfillment) ✅ aber nur eingeloggt (routes/customer.php:52) app/Services/Billing/StripeCheckoutService.php:452, app/Listeners/ProcessStripeWebhook.php:475, app/Enums/SinglePurchaseType.php
Credits (Ledger, atomar) ✅ app/Services/Billing/CreditWalletService.php
KI-Prüfung (grün/gelb/rot, Audit) ✅ Klassifikation + Routing bei Erst-Freigabe; Re-Check nach Edit ohne Status-Wirkung app/Jobs/ClassifyPressRelease.php, PressReleaseService::routeByClassification():176, reprocessAfterContentChange():150, app/Models/KiAudit.php
Hinterlegte Pressekontaktadresse ✅ Contact.email (Pivot press_release_contact), Fallback Company.email app/Models/Contact.php, app/Models/Company.php
Audit/Verlauf ✅ ActivityLog::record() + PressReleaseStatusLog (mit source) app/Services/ActivityLog.php, app/Models/PressReleaseStatusLog.php
connektar-Eingang ✅ Legacy-API (store() → Review/KI bzw. Sofort-Publish), Bulk-Import mit LegacyImportMap-Skip-Guard app/Http/Controllers/Api/Legacy/LegacyPressReleaseController.php, app/Services/Import/PressReleaseImporter.php

Fehlende Kernstücke (= dieser Plan): Re-Sync-Sperre als PM-Flag, Anonymisierungs-Engine (Wortgrenzen-Replace + Diff), Magic-Link-Verifikation für das Meldeverfahren, Self-Service-Portal mit den vier Spuren, Admin-Ausführungsaktionen in der Queue, Gast-Checkout für Depublizierung/Änderung, Reversibilitätsfenster, verschlüsselte Alt-Wert-Snapshots mit Löschfrist.


1. Phasenübersicht

Reihenfolge nach Nutzen und Abhängigkeit: Zuerst die Bausteine, die den manuellen Interimsprozess sofort absichern (Re-Sync-Sperre, Anonymisierung im Admin), dann Verifikation, dann die Self-Service-Spuren.

Phase 1  Datenmodell + Re-Sync-Sperre               ✅ umgesetzt 22.07.2026
Phase 2  Anonymisierungs-Engine + Admin-Ausführung   ✅ umgesetzt 22.07.2026
Phase 3  Magic-Link-Verifikation + Intake-Trichter   ✅ umgesetzt 22.07.2026
Phase 4  Spur A — Kleine Korrektur (self-service)   ✅ umgesetzt 22.07.2026 (panel-first)
Phase 5  Spur B — DSGVO-Einreichung (self-service → Admin-Gate)  ✅ umgesetzt 23.07.2026
Phase 6  Spur C — Depublizierung (Gast-Checkout + Reversibilitätsfenster)
Phase 7  Spur D — Änderung (Checkout + KI-Vorprüfung)  ✅ umgesetzt 23.07.2026
Phase 8  Querschnitt-Abschluss: Kommunikation, Auslieferungs-Checks, Doku

Jede Phase ist eigenständig deploybar; nach Phase 2 ist der Interims-Triage-Prozess (§4 des Konzepts) technisch vollständig unterstützt.

Stand 22.07.2026 abends — was noch fehlt (Einstieg für morgen)

Phasen 1–4 sind umgesetzt und getestet (Details in den Phasen-Abschnitten). Offen:

  1. Phase 5 — Spur B strukturiert ✅ umgesetzt 23.07. (Details im Phase-5-Abschnitt).
  2. Phase 6 — Spur C: ✅ Re-Sync-Sperre nachgerüstet (23.07.): PressReleaseService::depublish() setzt die Sperre jetzt IMMER selbst (kein Aufrufer kann sie vergessen) und invalidiert die Sitemap-Caches; der Admin-Pfad gibt seine Vorgangsnummer als Sperrgrund mit. Gast-Checkout zurückgestellt (Entscheidung 23.07.): Magic-Link-Login ins Panel deckt Pressekontakte ab — dort existiert die bezahlte Depublikation bereits. Noch offen aus Phase 6: Reversibilitätsfenster-Hinweis („umkehrbar bis …", legal.reversal_window_days) in Panel/Mails.
  3. Phase 7 — Spur D Checkout ✅ Panel-Pfad am 23.07. umgesetzt — mit geändertem Flow (E2 revidiert, siehe Phase-7-Abschnitt): KI-Vorprüfung vor der Zahlung im Modal, rot → Ablehnung ohne Zahlung, je PM nur einmal möglich; Obergrenze legal.change_max_distance_percent (50 %). Offen: Gast-Pfad für Magic-Link-Verifizierte, Bestätigungsmail nach Ausführung.
  4. Spur A im Web-Pfad ✅ erledigt per Entscheidung 23.07.: Kein eigener Web-Korrektur-Pfad nötig — hinterlegte Pressekontakte loggen sich per bestehendem Magic-Link-Login ins Panel ein und nutzen dort Spur A/D. Bestätigungsmail umgesetzt: PressReleaseChangeApplied (Typ press_release_corrected, abbestellbar) nach Spur-A-Korrektur (sofort) und Spur-D-Änderung (nach Webhook-Ausführung).
  5. Phase 8 — Querschnitt: connektar-Template-Stopp in allen Texten prüfen, noindex im Tombstone verifizieren, zweistufige Eskalations-Mailtexte (Ablehnung mit Aufsichtsbehörden-Hinweis), Doku-Abgleich.
  6. Ops: Dev-DB-Migration ✅ erledigt 23.07.: Das ALTER war serverseitig fertig (Migration manuell nachregistriert), alle restlichen Migrationen gelaufen — der komplette Flow ist auf pressekonto.test browser-testbar.
  7. ✅ Öffentliche Triage-Seite (23.07.): /release/{slug}/rechtliches ist jetzt zweistufig — Stufe 1 wählt das Anliegen (Absender-Selbstservice / DSGVO / Persönlichkeitsrechte / Meldung), Stufe 2 leitet Absender-Anliegen und DSGVO/Persönlichkeitsrechte primär auf den Magic-Link-Weg (Deep-Link zum /melden/pm-Trichter mit vorbefüllter PM-URL); das Kontaktformular ist dort nur der Fallback mit Pflicht-Hinweis auf die persönliche Identifizierung. Meldungen rechtswidriger Inhalte gehen direkt ins Formular (DSA). Der Store-Endpoint akzeptiert nur noch report/dsgvo/personal_rights — Absender-Spuren können nicht mehr als anonyme Queue-Anfrage einlaufen.
  8. Konfig-Feinjustierung (22.07. abends): correction_max_distance_percent auf 5 % und corrections_per_week auf 1 verschärft (statt 15 %/2 aus E5). Dazu im Service die Mini-Edit-Ausnahme: Änderungen ≤ 2 Zeichen gelten immer als klein — sonst würde bei kurzen Titeln ein einzelner Tippfehler die Prozent-Grenze reißen.

Phase 1 — Datenmodell + Re-Sync-Sperre ✅ umgesetzt (22.07.2026)

Warum zuerst: „Jede Mutation setzt die Re-Sync-Sperre" ist strukturell zwingend (§7), sonst überschreibt ein Re-Import jede Anonymisierung — das wäre ein DSGVO-Verstoß nach erfolgter Löschung.

1.1 Migrationen ✅

  • ✅ press_releases: sync_locked_at (nullable timestamp) + sync_lock_reason (string) — 2026_07_22_151506_add_sync_lock_to_press_releases_table.php. Umsetzungshinweis: Wegen des FULLTEXT-Index kann MySQL hier nicht INSTANT arbeiten (voller Tabellen-Copy, ~175k Zeilen); die Migration nutzt deshalb ein kombiniertes ALTER für beide Spalten statt zweier Blueprint-Einzelstatements und verzichtet auf after()-Positionierung. Merkregel für spätere Migrationen an press_releases: Spalten-Adds immer bündeln.
  • ✅ legal_requests erweitert — 2026_07_22_151507_add_meldeverfahren_fields_to_legal_requests_table.php:
    • Neue LegalRequestType-Cases: Correction (Spur A), Depublication (Spur C), Change (Spur D). Dsgvo bleibt Spur B.
    • Neue Spalten: verified_at, verification_method (Enum LegalRequestVerificationMethod: magic_link/manual/logged_in), single_purchase_id (FK, Spur C/D), executed_at.
    • Diff-/Schwärzungsdaten kommen in payload (json, existiert) — Nutzung ab Phase 4/5.
  • ✅ Neue Tabelle press_release_redaction_snapshots (2026_07_22_151507_create_..._table.php) + Modell PressReleaseRedactionSnapshot mit capture() (Cast encrypted:array, purge_after aus config('legal.snapshot_retention_days')), Scope dueForPurge() und Factory. → Reversibilität ohne Klartext-Altwerte im Log; Test sichert ab, dass der Klartext nie unverschlüsselt in der DB liegt.

1.2 Re-Sync-Sperre durchsetzen ✅

  • ✅ PressRelease::lockSync(string $reason) + isSyncLocked() — idempotent (erste Sperre bleibt maßgeblich), schreibt Verlaufs-Eintrag (neuer ActivityType::SyncLock, Badge rot). Wird von jeder Meldeverfahren-Mutation aufgerufen (Phasen 2, 4, 5, 6, 7).
  • ✅ PressReleaseImporter: gesperrte PMs werden auch bei --force übersprungen (Guard isSyncLocked() vor updateOrCreate, zählt als „Übersprungen").
  • ℹ️ LegacyPressReleaseController: kein Guard nötig — die Legacy-API hat heute keinen Update-Pfad (store() legt immer neu an, show() liest nur). Sollte je ein Update-Endpunkt entstehen, gehört der sync_locked_at-Check (→ 409/423) dorthin.
  • ✅ lockSync(): Felder gesetzt, Verlaufs-Eintrag, idempotent (zweiter Aufruf ändert nichts).
  • ✅ Importer überspringt gesperrte PM trotz --force (Schwärzung bleibt erhalten); ungesperrte PM wird weiter aktualisiert.
  • ✅ Snapshot: Klartext liegt nie unverschlüsselt in der DB, purge_after aus Config, dueForPurge()-Scope.
  • Angrenzende Bestandstests (Importer-Datumslogik, LegalRequest, ActivityLog — 23 Tests) weiter grün; Pint sauber.

Phase 2 — Anonymisierungs-Engine + Admin-Ausführung ✅ umgesetzt (22.07.2026)

Ziel: Der Admin kann aus der bestehenden Queue (admin/legal-requests/show) eine DSGVO-Anfrage tatsächlich ausführen statt nur „resolved" zu klicken. Das trägt sofort die Referenzfälle Huraux/Gleue/Rambo.

2.1 Service App\Services\PressRelease\AnonymizationService ✅

  • ✅ preview(PM, Begriffe, Ersetzungswert): Pflicht-Vorschau ohne Mutation — Treffer + Nachher-Wert je Feld (title, subtitle, text, boilerplate_override, keywords; teaser_* sind Integer und entfallen). Genutzt von Admin (Phase 2), wiederverwendbar im Self-Service (Phase 5).
  • ✅ Ersetzung über Wortgrenzen: Unicode-Lookarounds ((?<![\pL\pN])…(?![\pL\pN])) statt \b, damit Umlaute korrekt begrenzen — „Max" zerlegt „maximal" nicht, „Mustermanns" bleibt ohne explizite Variante stehen. Case-insensitive, längste Begriffe zuerst. Ersetzungswert default [entfernt], nie Leerstring.
  • ✅ execute(): Transaktion — verschlüsselter Snapshot (capture(): Felder, Kontakt-IDs, Bild-/Anhang-Metadaten, Begriffe) → Replace → Kontakt-Pivot lösen (optional) → markierte Bilder/Anhänge soft-löschen, auf public-Disk zusätzlich Dateien inkl. Varianten physisch löschen (sonst per Direkt-URL erreichbar) → lockSync() → Sitemap-Cache aller Hosts invalidieren (interne Suche liest live) → ActivityLog (neuer Typ Anonymization) ohne Alt-Werte und ohne Schwärzbegriffe.
  • ✅ Kein KI-Gate, kein Credit-Abzug — der Service berührt weder Klassifikations-Routing noch CreditWalletService.
  • ✅ Panel „Anonymisierung (Schwärzung)" bei type=dsgvo/personal_rights mit PM-Bezug und offenem Status: Schwärzbegriffe (ein Begriff/Variante pro Zeile), Ersetzungswert, Checkbox „Kontakt-Zuordnungen lösen", Bild-Auswahl → Pflicht-Vorschau (Ausführen-Button erscheint erst danach; Ausführen ohne Vorschau wird serverseitig abgewiesen) → „Anonymisieren ausführen" oder „Stattdessen depublizieren" (Matrix-Test §1/§2 menschlich; Vanity-PM → Depublizierung als milderes Restmittel).
  • ✅ Depublizieren-Aktion: PressReleaseService::depublish() + lockSync() + Aktenvermerk über admin_note; setzt executed_at + resolved. Deckt den Fallback „Kanal tot" (Huraux) ab — Admin führt nach manueller Prüfung aus, umkehrbar (Status-Restore, published_at bleibt).
  • ✅ Detail zeigt verification_method/verified_at („nicht verifiziert" bis Phase 3) und executed_at. Queue-Index-Spalte folgt mit Phase 3, wenn Verifikation real befüllt wird.

2.3 Purge-Command ✅

✅ snapshots:purge (PurgeRedactionSnapshots, Scope dueForPurge()), Scheduler täglich 03:10 mit withoutOverlapping + Failure-Alert in routes/console.php.

2.4 Tests ✅ (9 neue Tests grün)

  • ✅ AnonymizationServiceTest: Wortgrenzen/Varianten/Genitiv, Vorschau mutiert nicht, Snapshot verschlüsselt (Klartext weder im DB-Rohwert noch im Verlauf), Kontakte gelöst, Sperre gesetzt, Bilder soft-gelöscht + public-Dateien entfernt, Purge-Command löscht nur fällige Snapshots.
  • ✅ AdminLegalRequestExecutionTest: Vorschau→Ausführen-Happy-Path, Ausführen ohne Vorschau abgewiesen, Depublizieren-Alternative, verboten ohne compliance:manage.
  • Angrenzende Suiten (37 Tests: Legal, Admin-Grenzen, ActivityLog) weiter grün; Pint sauber.

Ziel: Die Meldeseite wird vom Kontaktformular zum Deflektions-Trichter: URL einfügen → System schlägt hinterlegten Kontakt nach → Magic-Link an diesen → verifizierter Zugang zur Anliegen-Auswahl.

3.1 Intake ✅ (PressReleaseReportController, Routen melden/pm* in routes/web.php)

  • ✅ Neue Seite „PM melden/ändern" (web/melden-pm.blade.php): URL-Feld → matchPublishedRelease() → bei PM-Fund + Pressekontakt-Mail (PressRelease::pressContactEmail(): PM-Kontakt via Pivot, Fallback Firmen-Adresse, ohne PortalScope) Magic-Link-Versand. Vier-Anliegen-Tabelle mit Kosten vorab auf der Seite (Anti-Zombie). Callout-Einstieg auf der bestehenden melden-Übersichtsseite.
  • ✅ Adresse nie angezeigt/übernommen — nur maskiert („p***@firma.example").
  • ✅ Kanal tot: Hinweis mit Link auf das bestehende manuelle Formular (melden/kontaktdaten) — dort verifiziert der Admin anhand eines Nachweises (§6-Fallback, Ausführung über Phase 2). (Bewusst kein neues Formular — der DSA-Weg existiert bereits.)
  • ✅ Rate-Limits: pro IP (5/h) und pro PM (legal.report_links_per_day = 3/Tag) + Honeypot + throttle:10,1.
  • ✅ MagicLinkGenerator::createReportLink(): purpose press_release_report (Enum-Migration 2026_07_22_171134_add_report_purpose_to_magic_links_table.php — purpose ist ein DB-Enum!), payload {press_release_id, contact_email}, user_id = PM-Owner, TTL legal.report_link_ttl_hours = 72; ältere unverbrauchte Report-Links derselben PM werden entwertet.
  • ✅ Mailable PressReleaseReportLink (queued, afterCommit) mit Hinweis „kein Anliegen gestartet? → ignorieren".
  • ✅ Consume-Route (melden/pm/verifizieren/{token}, throttled): atomares Claim (Muster MagicLinkConsumeController) → Session-Marker (legal.report_session_minutes = 60) → Anliegen-Auswahl. Kein Login.
  • ✅ Eingeloggter Creator: POST me.press-releases.report (prüft accessibleBy, verification_method=logged_in), Button „Ändern/Melden" auf der Kunden-PM-Detailseite (nur published).

3.3 Anliegen-Auswahl ✅ (web/melden-pm-anliegen.blade.php)

  • ✅ Vier Spuren mit Kosten-Labels (kostenlos / kostenlos / 35 € / 10 €) + Beschreibung; Auswahl erzeugt LegalRequest mit type, verified_at, verification_method, requester_email = Kontaktadresse; Eingangsbestätigung (LegalRequestReceived) + Operator-Mail. Bremse „1 offene Anfrage pro PM/Typ" beim Store durchgesetzt.
  • ℹ️ Interim bis Phase 4–7: Alle vier Spuren landen zunächst als verifizierte Anfragen in der Admin-Queue (DSGVO/Depublizierung dort seit Phase 2 direkt ausführbar). Seiten-Text verspricht ehrlich: kostenpflichtige Anliegen erst nach Zahlungsaufforderung, keine Kosten ohne Bestätigung. Die Self-Service-Ausführung (Spur A sofort, C/D mit Checkout) ersetzt das schrittweise.
  • ✅ Lang-Keys static.pm_report.* in lang/de + lang/en.

3.4 Tests ✅ (PressReleaseReportFlowTest, 10 Tests grün)

Link geht an hinterlegte, nie an Formular-Adresse (maskiert); unbekannte URL abgelehnt; toter Kanal → manueller Fallback; Rate-Limit pro PM; Consume nur einmal; Anliegen landet verifiziert in der Queue; Doppel-Anfrage geblockt; Session-Pflicht; Owner-Einstieg; Fremd-User 403.


Phase 4 — Spur A: Kleine Korrektur (kostenlos, sofort) ✅ umgesetzt (22.07.2026, panel-first)

Bestätigte Entscheidung: Korrekturen gehen sofort live (kein Admin-Gate), landen im Verlauf und sind über den verschlüsselten Snapshot rückgängig machbar.

  • ✅ CorrectionService (app/Services/PressRelease/CorrectionService.php):
    • classify(): relative Änderungsdistanz je Feld (title, subtitle, text, keywords), Gate ≤ legal.correction_max_distance_percent (15 %, E5). Hybrid-Metrik: kurze Felder zeichenbasiert (ein Tippfehler im Titel bleibt „klein"), lange Texte wortbasiertes Levenshtein-DP (PHPs levenshtein() ist auf 255 Bytes begrenzt); Vergleich auf strip_tags-Basis — reine Markup-Änderungen zählen nicht. Der Nutzer wählt die Einstufung nie selbst.
    • apply(): Snapshot (Rückgängig-Basis) → Felder schreiben (Text durch PressReleaseHtmlSanitizer) → lockSync() → LegalRequest (correction, resolved+executed_at, verifiziert) → Verlauf (ActivityType::Correction) → Sitemap-Cache → reprocessAfterContentChange() (Re-Score ohne Status-Wirkung, kein Freigabelauf).
    • Bremse: legal.corrections_per_week = 2 (DB-basiert über ausgeführte Correction-Requests, auditierbar).
    • restoreSnapshot(): spielt Vorher-Stand zurück (Felder + Kontakte; Bilder nur wenn Dateien noch existieren), Sperre bleibt, Verlaufs-Eintrag.
  • ✅ Panel-Abschnitt „Änderung & Korrektur" (Kunden-PM-Detail, direkt über Depublikation): ein Einstieg, Formular vorbefüllt → Pflicht-Vorschau zeigt Einstufung + Distanz je Feld → klein: „Kostenlos speichern — sofort live"; groß: „Als kostenpflichtige Änderung einreichen (10 €)" — Interim bis zum Phase-7-Checkout: erzeugt verifizierte Change-Anfrage in der Admin-Queue (Doppel-Bremse), Zahlungsaufforderung folgt.
  • ✅ Admin-Rückgängig: Button „Rückgängig machen (Snapshot)" in der Queue bei ausgeführten Anfragen mit Snapshot (gilt für Korrektur und Anonymisierung, bis zur Löschfrist) — Anfrage geht zurück auf „In Bearbeitung".
  • ✅ Gemeinsame Cache-Invalidierung nach App\Support\WebCaches::forgetSitemaps() extrahiert (genutzt von Correction + Anonymization).
  • ✅ Tests (11 neue, alle grün): Tippfehler klein / Umschreibung groß / Markup-only = keine Änderung, Sofort-Live mit Snapshot+Sperre+Verlauf, Zu-groß-Abwehr, Wochen-Bremse, Restore (Service + Admin-UI), kein Credit-Bezug, Panel-Happy-Paths, Vorschau-Pflicht.
  • ℹ️ Noch offen aus Phase 4: dieselben Formulare im verifizierten Web-Bereich (Magic-Link-Besucher) — aktuell erzeugt Spur A dort noch eine Queue-Anfrage; die CorrectionService-Engine ist dafür wiederverwendbar. Nutzer-Bestätigungsmail nach Korrektur bewusst weggelassen (Owner sieht das Ergebnis sofort im Panel), für den Web-Pfad nachrüsten.

Phase 5 — Spur B: DSGVO-Einreichung self-service → Admin-Gate ✅ umgesetzt (23.07.2026)

  • ✅ Strukturformular im verifizierten Web-Bereich (melden-pm-anliegen, Alpine-Toggle bei Spur-Auswahl „DSGVO"): Schwärzbegriffe zeilenweise (Hinweis auf Varianten/Genitiv), Checkboxen „Pressekontakt-Daten entfernen" und „Bilder mit Personenbezug entfernen" (Bildauswahl trifft der Admin), Freitext-Begründung bleibt Pflicht.
  • ✅ Pflicht-Diff: Erster POST rendert die Vorschau aus AnonymizationService::preview() (Treffer je Feld, geschwärzte Fassung als Klartext) — erst „Verbindlich einreichen" erzeugt die Anfrage. Begriffe ohne Treffer werden mit Schreibweisen-Hinweis abgewiesen; leeres Formular (keine Begriffe, keine Option) ebenso.
  • ✅ Einreichung ≠ Ausführung: LegalRequest type=dsgvo, Status open, payload.redaction = {terms, detach_contacts, remove_images_requested, preview_total_hits}; an der PM ändert sich nichts, keine Credits. Bremse 1 offene Anfrage pro PM/Typ greift weiter.
  • ✅ Admin-Editor vorbefüllt: admin.legal-requests.show übernimmt Begriffe + Kontakt-Option aus payload.redaction und zeigt einen Hinweis-Kasten (inkl. Bild-Entfernungswunsch). Admin prüft, ergänzt, führt aus oder depubliziert.
  • ✅ Mails: Eingangsbestätigung existierte (Phase 3); neu die Umsetzungsbestätigung LegalRequestExecuted (transaktional, nicht abbestellbar) an die Antragsteller-Adresse nach executeAnonymization („anonymized") und depublishRelease („depublished").
  • ✅ Wording-Guardrail eingehalten: überall „Schwärzung/Entfernung", inkl. Hinweis auf Suchmaschinen-/Drittarchiv-Kopien.
  • ✅ Tests: Pflicht-Vorschau ohne DB-Write, Treffer-Validierung, bestätigte Einreichung mit strukturiertem Payload, Admin-Prefill, Umsetzungs-Mail.

Phase 6 — Spur C: Depublizierung (35 €, Gast-Checkout, umkehrbar)

  • Wiederverwendung: StripeCheckoutService::forDepublication() + ProcessStripeWebhook::fulfillDepublication() existieren. Neu ist der Gast-Pfad: Checkout ohne Login aus dem verifizierten Bereich.
    • Variante (empfohlen): SinglePurchase ohne user_id-Zwang bzw. auf PM-Owner gebucht, legal_request_id als Referenz; Stripe-Session mit customer_email = verifizierte Kontaktadresse. Webhook-Fulfillment bleibt identisch (idempotent, row-locked).
  • Nach Zahlung: depublish() → Tombstone (410 + noindex existiert) → lockSync() → LegalRequest resolved/executed_at → Bestätigungsmail mit Hinweis auf Suchindex-Trägheit (Cache-Aktualisierung bei Google dauert — Textbaustein).
  • Reversibilitätsfenster: depublished_at + config('legal.reversal_window_days') (Standard 14 Tage, E3): innerhalb der Frist Wiederherstellung über Admin (Snapshot nicht nötig — Status-Restore reicht, published_at bleibt). Hinweis im Bestätigungsschritt vor Zahlung („umkehrbar bis …").
  • Preis: 35 € live via config('billing.depublication_price_cents') (existiert). Die systemische 25-Credit-Variante für eingeloggte Kunden bleibt unberührt — aber Test sichert ab, dass der DSGVO-Pfad (Spur B) nie in diesen Checkout mündet.
  • Tests: Gast-Checkout-Fulfillment, Reversibilitätsfenster, Tombstone + Sitemap-Ausschluss.

Phase 7 — Spur D: Änderung (10 €, KI-Vorprüfung) ✅ Panel-Pfad umgesetzt (23.07.2026)

Flow-Entscheidung 23.07. (revidiert E2): Die KI-Prüfung läuft VOR der Zahlung als einmaliger Zwischenschritt im Modal — nicht mehr nach Zahlung auf einem Pending-Payload. Rot → Einreichung wird direkt abgelehnt, es kommt gar nicht erst zur Zahlung (damit entfällt der „kein Verbrauch"-Fall aus E2). Gelb/Grün → Checkbox (§ 14 BGB/AGB, wie Depublikation) + direkter Stripe-Checkout; nach Zahlungseingang geht die Änderung sofort live. Der Vorgang ist je PM nur einmal möglich (auch nach Ablehnung kein zweiter Anlauf).

  • ✅ Obergrenze: legal.change_max_distance_percent = 50 % (env LEGAL_CHANGE_MAX_DISTANCE_PERCENT) — darüber ist auch die kostenpflichtige Änderung nicht buchbar (sonst verdeckte Selbst-Depublizierung); UI verweist auf Depublikation + Neuveröffentlichung. CorrectionService::classify() liefert dafür exceeds_paid_limit.
  • ✅ Preis: billing.change_price_cents = 1000 (E1); 0/leer deaktiviert den Weg (wie Depublikation).
  • ✅ Vorprüfung: CorrectionService::precheckProposedContent() klassifiziert eine NICHT gespeicherte PM-Kopie mit den neuen Werten über den ClassificationManager (KI-Provider, Fallback deterministisch/Blacklist). Ergebnis + Provider landen in payload.ki_check der Change-LegalRequest (rot → Status Rejected, sonst Open).
  • ✅ Checkout: CheckoutController::change() (Route me.checkout.change, checkout/aenderung/{id}) verlangt die offene Change-Anfrage mit gelb/grünem ki_check als Nachweis; SinglePurchaseType::Change (press_release_change, Enum-Migration), Pending-Wiederverwendung wie Depublikation; Anfrage wird über single_purchase_id verknüpft. StripeCheckoutService::forChange() (adHoc-Muster).
  • ✅ Fulfillment: ProcessStripeWebhook::fulfillChange() → CorrectionService::applyPaid(): Snapshot, Felder schreiben (Text durch Sanitizer), lockSync(), Verlauf, Re-Score; Anfrage Resolved/executed_at, Kauf Consumed. Reißt die Obergrenze doch (Payload-Manipulation), bleibt der Kauf unkonsumiert zur Admin-Klärung.
  • ✅ Panel-UI: Modal paid-change mit Zwischenerklärung (einmalige Prüfung, rot/gelb-grün-Folgen), Loading-State, Ablehnungsansicht mit Gründen, Bestätigungs-Switch (BookingConfirmation Typ press_release_change) + Stripe-Button. Abgebrochene Zahlung: Modal springt direkt wieder zum Zahlungs-Schritt der bereits geprüften Fassung.
  • ✅ Tests: Panel (grün → Zahlungs-Schritt, rot → Ablehnung + Einmal-Regel, Obergrenze), Service (applyPaid, precheck, exceeds_paid_limit), Checkout (Nachweis-Pflicht, Pending-Reuse, rot blockiert), Webhook (Ausführung + Replay-Idempotenz).
  • ✅ Bestätigungsmail: PressReleaseChangeApplied an den Auftraggeber nach Spur-A-Korrektur und Spur-D-Ausführung (neuer EmailNotificationType::PressReleaseCorrected, abbestellbar, erscheint automatisch in den Mail-Präferenzen).
  • Entscheidung 23.07.: Kein separater Gast-Pfad — hinterlegte Pressekontakte nutzen den bestehenden Magic-Link-Login ins Panel und bearbeiten dort. Der /melden/pm-Trichter bleibt für Dritte/DSGVO/Depublikations-Anliegen.
  • Kleinigkeit offen: KiAudit-Eintrag für die Vorprüfung (Ergebnis liegt bisher nur in payload.ki_check der Anfrage).

Phase 8 — Querschnitt-Abschluss

  1. Template-/Text-Stopp connektar (Sofortmaßnahme §7): Alle Frontend-/Mail-Texte prüfen (melden*-Views, Mail-Templates, lang/-Strings): kein Verweis mehr auf „kostenfreier Weg über connektar"; Ersatz: Verweis auf die neue Meldeseite. (Antwortvorlagen außerhalb des Systems sind organisatorisch zu ändern — nicht Teil des Codes, im Rollout-Termin erwähnen.)
  2. Auslieferungs-Checkliste (§5) verifizieren: Tombstone 410 ✅, noindex im Tombstone-View prüfen/ergänzen, Sitemap schließt Archived aus (Scope visibleInArchive prüfen) + Cache-Invalidierung nach Mutation, interne Suche (FULLTEXT auf press_releases — liest live, kein Rebuild nötig; Test ergänzen), RSS/Feeds: existieren noch nicht, sind aber geplant (E8) — beim künftigen Feed-Feature zwingend: Archived/anonymisierte PMs ausschließen und Feed-Cache bei Meldeverfahren-Mutationen invalidieren (Anforderung dort einplanen).
  3. Eskalationsstufen-Texte: Mail-Vorlagen zweistufig — Angebots-/Eingangsmail ohne Aufsichtsbehörden-Hinweis, Ablehnungsmail (aus Admin-Queue reject()) mit Art.-12-Pflichthinweis. Textbausteine in lang/.
  4. Doku: kurzer Abschnitt in docs/konzept/ bzw. Update dieses Ordners; CLAUDE.md unberührt.
  5. Gesamt-Testlauf: vendor/bin/sail artisan test --compact + Pint.

2. Getroffene Entscheidungen (bestätigt 22.07.2026)

# Entscheidung Umsetzung im Plan
E1 Zwei getrennte Preise: 35 € = komplette Depublizierung einer PM (Spur C), 10 € = Änderung einer PM (Spur D) Spur C: billing.depublication_price_cents (existiert); Spur D: neuer Config-Wert billing.change_price_cents = 1000 → Phase 7
E2 Bei KI-„rot" kein Verbrauch Revidiert 23.07.: KI-Prüfung läuft vor der Zahlung (einmaliger Zwischenschritt im Modal). Rot → Ablehnung ohne Zahlung; Gelb/Grün → Zahlung, danach sofort live. Je PM nur einmal möglich. Umgesetzt in Phase 7 (precheckProposedContent, Modal paid-change); der „kein Verbrauch"-Fall entfällt, weil vor Zahlung geprüft wird
E3 Reversibilitätsfenster als Config-Variable, Standard 14 Tage legal.reversal_window_days = 14 → Phase 6
E4 Snapshot-Löschfrist als Config-Variable, Standard 30 Tage legal.snapshot_retention_days = 30; bestimmt purge_after → Phasen 1.1 / 2.3
E5 Levenshtein-Schwellwert Spur A als Config-Variable; Start 15 %, am 22.07. abends auf 5 % verschärft (+ Mini-Edit-Ausnahme ≤ 2 Zeichen) legal.correction_max_distance_percent = 5, legal.corrections_per_week = 1 → Phase 4
E6 Spur B bleibt admin-gegated; Automatisierung später bei guter Kalibrierung Wie geplant → Phasen 2 / 5
E7 Magic-Link-TTL/Session: bestehende System-Mechanik wiederverwenden Kein Sondersystem — MagicLinkGenerator-Muster mit purpose-spezifischer TTL wie bei login/press_release_access; TTL des Report-Links als Config-Wert analog der vorhandenen Defaults → Phase 3
E8 RSS/Feeds werden in Zukunft entwickelt Anforderung fürs künftige Feed-Feature vorgemerkt: Archived/anonymisierte PMs ausschließen, Feed-Cache bei Meldeverfahren-Mutationen invalidieren → Phase 8 / Abschnitt 3

3. Nicht-Code-Punkte aus den Konzeptdokumenten (zur Nachverfolgung, außerhalb dieses Plans)

  • Anwaltsagenda §10 (Unentgeltlichkeits-Grenze, UWG connektar, Realnamen-Änderung, Art. 19 Syndication, AI-Act-Co-Betreiber).
  • Syndication-Vertrag mit connektar (Änderungs-/Löschpropagierung, Art.-19-Zuständigkeit einfordern).
  • Externe Antwortvorlagen (Mail-Templates außerhalb des Systems) vom connektar-Verweis befreien.
  • RSS/Feed-Feature (künftige Entwicklung, E8): muss Archived/anonymisierte PMs ausschließen und die Re-Sync-Sperre bzw. Mutationen des Meldeverfahrens respektieren (Cache-Invalidierung).

4. Testing & Qualität (durchgehend)

  • Pest-Feature-Tests je Phase (Konventionen: RefreshDatabase global, RolesAndPermissionsSeeder im beforeEach, Factories PressRelease::factory()->published()->forPortal(...); Vorbilder: tests/Feature/Billing/DepublicationTest.php, tests/Feature/LegalRequestTest.php, tests/Feature/Admin/AdminPressReleaseActionsTest.php).
  • Kritische Invarianten als eigene Tests: (a) DSGVO-Pfad zieht nie Credits/Checkout, (b) jede Mutation setzt sync_locked_at, (c) Importer/API respektieren die Sperre, (d) ActivityLog enthält keine Klartext-Altwerte, (e) Magic-Link geht nie an Formular-Adresse.
  • vendor/bin/sail bin pint --dirty vor jedem Abschluss.

Änderungshistorie

Datum Änderung
22.07.2026 Erstfassung auf Basis Ist-Stand-Analyse der Codebasis (Meldeverfahren v0.1, Magic-Link-, Depublizierungs- und Billing-Infrastruktur)
22.07.2026 (rev. 2) Entscheidungen E1–E8 bestätigt und eingearbeitet: Spur D = 10 €, Depublizierung = 35 €; kein Verbrauch bei KI-rot; Reversibilitätsfenster 14 Tage / Snapshot-Frist 30 Tage / Levenshtein 15 % jeweils per Config; Magic-Link nutzt bestehende Mechanik; RSS/Feeds als künftiges Feature vorgemerkt
22.07.2026 (rev. 3) Phase 1 umgesetzt: 3 Migrationen, lockSync()/isSyncLocked(), Importer-Guard (auch --force), LegalRequestType-Cases + LegalRequestVerificationMethod, Snapshot-Modell mit capture()/Factory, ActivityType::SyncLock, Config E3–E5 in config/legal.php, 5 neue Pest-Tests grün. Erkenntnisse: Legacy-API hat keinen Update-Pfad (API-Guard entfällt vorerst); Spalten-Adds an press_releases wegen FULLTEXT-Index immer als ein kombiniertes ALTER (voller Tabellen-Copy, kein INSTANT)
22.07.2026 (rev. 4) Phase 2 umgesetzt: AnonymizationService (Unicode-Wortgrenzen-Replace, Pflicht-preview(), transaktionales execute() mit Snapshot/Kontakt-Detach/Bild-Löschung/Sperre/Sitemap-Invalidierung), Admin-Queue-Panel Anonymisieren/Depublizieren mit Pflicht-Vorschau, ActivityType::Anonymization, snapshots:purge + Scheduler, 9 neue Pest-Tests grün
22.07.2026 (rev. 5) Admin-Schwärzungs-Editor: kompletter PM-Inhalt als kopierbare Vorschau (vorher/nachher in einem Block), Ersetzungswert vorbefüllt. Phase 3 umgesetzt: Intake melden/pm (Kosten-Tabelle vorab, Honeypot, IP+PM-Rate-Limits), createReportLink + Enum-Migration press_release_report, Mailable, atomare Verifikation mit Session-Marker, Anliegen-Auswahl erzeugt verifizierte Queue-Anfragen (Bremse 1 offene Anfrage/PM/Typ), toter-Kanal-Fallback auf manuelles Formular, Owner-Einstieg aus dem Kunden-Panel, Lang-Keys de/en, 10 neue Pest-Tests grün
22.07.2026 (rev. 6) Phase 4 umgesetzt (panel-first, Entscheidung: sofort live + rückgängig machbar): CorrectionService mit Hybrid-Levenshtein-Gate (Zeichen kurz / Wörter lang, strip_tags-Basis), Panel-Abschnitt „Änderung & Korrektur" über der Depublikation (ein Einstieg, System stuft klein/groß ein; groß = Interim-Queue-Anfrage bis Phase-7-Checkout), Wochen-Bremse per Config, Snapshot-Restore im Admin (Korrektur + Anonymisierung), ActivityType::Correction, WebCaches::forgetSitemaps() extrahiert, 11 neue Pest-Tests grün
22.07.2026 (rev. 7) Konfig verschärft (5 % / 1 Korrektur pro Woche) + Mini-Edit-Ausnahme ≤ 2 Zeichen im Gate; Abschnitt „Stand & was noch fehlt" für den Wiedereinstieg ergänzt; Tagescommit
23.07.2026 (rev. 8) Phase 7 Panel-Pfad umgesetzt mit revidiertem E2 (KI-Vorprüfung VOR Zahlung, rot → Ablehnung ohne Zahlung, nur einmal je PM; Obergrenze 50 %). Außerdem: Vergleichsbasis entity-fest (Editor-Normalisierung ≠ Änderung), toEditorHtml() in Admin-Edit + Korrektur-Panel (Legacy-Plaintext-Umbrüche), Wochenlimit-Hinweis in Panel-Text und Bremsen-Meldung, Dev-DB vollständig migriert
23.07.2026 (rev. 9) Entscheidung: kein separater Web-Korrektur-Pfad — Magic-Link-Login ins Panel deckt Pressekontakte ab. Bestätigungsmail PressReleaseChangeApplied (Spur A sofort, Spur D nach Webhook) mit neuem abbestellbarem Typ press_release_corrected; Phase 7 damit vollständig
23.07.2026 (rev. 10) Phase 5 umgesetzt: strukturiertes DSGVO-Schwärzungsformular mit Pflicht-Diff im verifizierten Web-Bereich, Admin-Editor-Prefill aus payload.redaction, Umsetzungsbestätigung LegalRequestExecuted an Antragsteller
23.07.2026 (rev. 11) Gast-Checkout zurückgestellt (Magic-Link-Login deckt Pressekontakte ab). Re-Sync-Sperre + Sitemap-Invalidierung fest in depublish() verankert — schließt die Lücke im bezahlten Webhook-Pfad; Vollsuite 1418 Tests grün
23.07.2026 (rev. 12) Öffentliche „Rechtliches"-Seite an der PM zur zweistufigen Triage umgebaut: Anliegen-Karten, Magic-Link-Deep-Link mit vorbefüllter URL, Formular nur als Fallback (mit Identifizierungs-Hinweis bei DSGVO/Persönlichkeitsrechten), Meldung direkt; Store-Endpoint auf die drei öffentlichen Typen gehärtet
23.07.2026 (rev. 13) Pressekontakt-Lücke der Legacy-Bestände geschlossen: 30.422 von 30.433 veröffentlichten PMs haben keine PM↔Kontakt-Verknüpfung (im Altsystem hingen Kontakte an der FIRMA). Detailseite zeigt jetzt Fallback Firmen-Kontakt → Firmendaten; pressContactEmail() (Magic-Link-Kanal) nutzt dieselbe Kette PM-Kontakt → Firmen-Kontakt → Firmen-E-Mail
23.07.2026 (rev. 14) Entscheidung: Die Triage-Buttons führen NICHT in den Web-Melde-Trichter (/melden/pm), sondern zum passwortlosen Portal-Anmeldelink (pressekonto /anmeldelink) — dort werden die PMs der Kontaktadresse im Konto zusammengeführt und alle Selbstservice-Funktionen stehen bereit. Der Web-Trichter bleibt als eigenständiger Weg bestehen (u. a. verifizierte DSGVO-Struktureinreichung)
23.07.2026 (rev. 15) Panel: „Ändern/Melden"-Button leitet nicht mehr in den Web-Trichter, sondern öffnet ein Anliegen-Modal (4 Spuren). Korrektur/Änderung und Depublizierung springen zu den Panel-Abschnitten; DSGVO hat jetzt das strukturierte Schwärzungsformular (Pflicht-Vorschau, Einreichung ≠ Ausführung, Payload wie Web-Formular → Admin-Editor-Prefill) direkt als Modal im Panel