presseportale/docs/Decision-Update/Entwicklungsplan Meldeverfahren.md
Kevin Adametz b4b2e013d6 recht: Meldeverfahren v2 Phasen 1-4 — Re-Sync-Sperre, Anonymisierung, Magic-Link-Intake, Self-Service-Korrektur
Phase 1: sync_locked_at auf press_releases (Importer überspringt gesperrte
PMs auch bei --force), LegalRequest um Verifikation/Ausführung erweitert,
verschlüsselte Vorher-Snapshots mit Löschfrist (snapshots:purge, täglich).

Phase 2: AnonymizationService (Unicode-Wortgrenzen-Schwärzung, Pflicht-Diff,
transaktionale Ausführung mit Snapshot, Kontakt-Detach, Bild-Löschung,
Sitemap-Invalidierung, Verlauf ohne Klartext-Altwerte); Admin-Queue kann
DSGVO-Anfragen anonymisieren oder depublizieren, inkl. Snapshot-Restore.

Phase 3: Deflektions-Trichter „PM melden/ändern" (melden/pm) mit Kosten-
Tabelle vorab; Magic-Link geht nur an die hinterlegte Pressekontaktadresse
(neuer purpose press_release_report), atomare Verifikation, Anliegen-Auswahl
erzeugt verifizierte Queue-Anfragen; toter-Kanal-Fallback; Owner-Einstieg.

Phase 4: Panel-Abschnitt „Änderung & Korrektur" — Levenshtein-Gate (5 %,
Mini-Edit-Ausnahme ≤ 2 Zeichen) stuft klein/groß ein; kleine Korrektur
kostenlos sofort live (Snapshot, Sperre, Verlauf, Re-Score), größere als
Change-Anfrage (Interim bis 10-€-Checkout, Phase 7).

Entwicklungsplan mit Entscheidungen E1-E8 und offenen Punkten in
docs/Decision-Update; 41 Pest-Tests (tests/Feature/Legal) grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:56:34 +00:00

30 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)
Phase 6  Spur C — Depublizierung (Gast-Checkout + Reversibilitätsfenster)
Phase 7  Spur D — Änderung (Gast-Checkout + KI-Freigabelauf)
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: Das DSGVO-Anliegen im verifizierten Web-Bereich ist bisher Freitext; geplant ist das strukturierte Schwärzungsformular (Felder + Varianten) mit Pflicht-Diff aus AnonymizationService::preview(), das den fertigen Schwärzungsauftrag in die Admin-Queue legt (Admin-Editor vorbefüllt).
  2. Phase 6 — Spur C Gast-Checkout: 35-€-Checkout für Magic-Link-Verifizierte (ohne Login); Reversibilitätsfenster-Hinweis („umkehrbar bis …", legal.reversal_window_days); lockSync() auch im bestehenden Webhook-Fulfillment fulfillDepublication() nachrüsten (bezahlte Panel-Depublizierung setzt die Sperre bisher nicht!).
  3. Phase 7 — Spur D Checkout: 10 € (billing.change_price_cents anlegen), Gast + Panel; nach Zahlung KI-Freigabelauf auf Pending-Payload (grün/gelb → live, rot → kein Verbrauch, erneut einreichbar). Ersetzt den Interim-Weg „Change-Anfrage in die Queue".
  4. Spur A im Web-Pfad: Korrektur-Formular (CorrectionService-Engine) auch für Magic-Link-Verifizierte statt Queue-Anfrage; Bestätigungsmail an die Kontaktadresse.
  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 (sync_locked_at-ALTER auf press_releases, FULLTEXT-bedingt langsamer Tabellen-Copy) lief am Abend des 22.07. noch; danach php artisan migrate für die restlichen Migrationen (magic_links-Enum) ausführen. Test-DB ist vollständig migriert, alle 41 Legal-Tests grün.
  7. 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

  • Formular: Strukturfelder zum Leeren angeboten + Freitext-Schwärzwerte inkl. Varianten → Pflicht-Diff (Preview aus AnonymizationService::preview()) → Einreichung (nicht Ausführung): LegalRequest type=dsgvo, payload = Schwärzungsauftrag, Status open.
  • Admin-Queue zeigt den vorbereiteten Diff (Phase-2-Editor, vorbefüllt) → Admin bestätigt anonymisieren oder wählt depublizieren → AnonymizationService::execute() bzw. depublish() + Snapshot.
  • Bestätigungsmail an Antragsteller bei Einreichung (Fristbeginn dokumentiert, Vorgangsnummer M-{id}) und bei Ausführung.
  • Wording: In der UI „Schwärzung/Entfernung personenbezogener Daten", nicht „Anonymisierung im DSGVO-Vollsinn" (Guardrail — nicht überversprechen).
  • Bremse: 1 offene Anfrage pro PM/Typ (Logik existiert konzeptionell in LegalRequestStatus::isOpen() — beim Store durchsetzen).
  • Tests: Einreichung erzeugt keinen Datenbank-Write an der PM; Ausführung nur durch Admin; nie Credits.

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-Freigabelauf)

  • Preis: 10 € (E1), neuer Config-Wert billing.change_price_cents = 1000.
  • Formular: neuen Text/Feldwerte eingeben (Voll-Editor light, analog customer/press-releases/edit-Formularregeln aus ProvidesPressReleaseFormRules) → Preis anzeigen → Gast-Checkout (SinglePurchaseType-Case Change, adHocLineItem-Muster).
  • Nach Zahlung: Änderung in Entwurfs-Kopie/Pending-Payload halten (nicht direkt live), dann ClassifyPressRelease auf den neuen Inhalt:
    • grün/gelb → Änderung anwenden, PM bleibt/wird published, lockSync(), Kauf auf Consumed, Mail „live".
    • rot → abgelehnt, Änderung verworfen, kein Verbrauch (E2): Kauf bleibt un-konsumiert, Antragsteller kann mit überarbeitetem Text erneut einreichen ohne erneut zu zahlen; Mail mit Ablehnungsgrund.
  • KiAudit-Trail entsteht automatisch über den bestehenden Job.
  • Tests: rot verwirft ohne PM-Änderung; grün wendet an; Webhook-Idempotenz.

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 — es wird nichts abgezogen Kauf bleibt un-konsumiert (Status nicht Consumed); der Antragsteller kann mit überarbeitetem Text erneut in den Freigabelauf, ohne erneut zu zahlen → Phase 7
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