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>
30 KiB
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:
- 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). - 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-FulfillmentfulfillDepublication()nachrüsten (bezahlte Panel-Depublizierung setzt die Sperre bisher nicht!). - Phase 7 — Spur D Checkout: 10 € (
billing.change_price_centsanlegen), 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". - Spur A im Web-Pfad: Korrektur-Formular (CorrectionService-Engine) auch für Magic-Link-Verifizierte statt Queue-Anfrage; Bestätigungsmail an die Kontaktadresse.
- Phase 8 — Querschnitt: connektar-Template-Stopp in allen Texten prüfen,
noindexim Tombstone verifizieren, zweistufige Eskalations-Mailtexte (Ablehnung mit Aufsichtsbehörden-Hinweis), Doku-Abgleich. - Ops: Dev-DB-Migration (
sync_locked_at-ALTER aufpress_releases, FULLTEXT-bedingt langsamer Tabellen-Copy) lief am Abend des 22.07. noch; danachphp artisan migratefür die restlichen Migrationen (magic_links-Enum) ausführen. Test-DB ist vollständig migriert, alle 41 Legal-Tests grün. - Konfig-Feinjustierung (22.07. abends):
correction_max_distance_percentauf 5 % undcorrections_per_weekauf 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 aufafter()-Positionierung. Merkregel für spätere Migrationen anpress_releases: Spalten-Adds immer bündeln. - ✅
legal_requestserweitert —2026_07_22_151507_add_meldeverfahren_fields_to_legal_requests_table.php:- Neue
LegalRequestType-Cases:Correction(Spur A),Depublication(Spur C),Change(Spur D).Dsgvobleibt Spur B. - Neue Spalten:
verified_at,verification_method(EnumLegalRequestVerificationMethod: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
- ✅ Neue Tabelle
press_release_redaction_snapshots(2026_07_22_151507_create_..._table.php) + ModellPressReleaseRedactionSnapshotmitcapture()(Castencrypted:array,purge_afterausconfig('legal.snapshot_retention_days')), ScopedueForPurge()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 (neuerActivityType::SyncLock, Badge rot). Wird von jeder Meldeverfahren-Mutation aufgerufen (Phasen 2, 4, 5, 6, 7). - ✅
PressReleaseImporter: gesperrte PMs werden auch bei--forceübersprungen (GuardisSyncLocked()vorupdateOrCreate, 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 dersync_locked_at-Check (→409/423) dorthin.
1.3 Tests ✅ (tests/Feature/Legal/SyncLockTest.php, 5 Tests grün)
- ✅
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_afteraus 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 TypAnonymization) ohne Alt-Werte und ohne Schwärzbegriffe. - ✅ Kein KI-Gate, kein Credit-Abzug — der Service berührt weder Klassifikations-Routing noch
CreditWalletService.
2.2 Admin-Queue ausgebaut (admin/legal-requests/show) ✅
- ✅ Panel „Anonymisierung (Schwärzung)" bei
type=dsgvo/personal_rightsmit 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 überadmin_note; setztexecuted_at+resolved. Deckt den Fallback „Kanal tot" (Huraux) ab — Admin führt nach manueller Prüfung aus, umkehrbar (Status-Restore,published_atbleibt). - ✅ Detail zeigt
verification_method/verified_at(„nicht verifiziert" bis Phase 3) undexecuted_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 ohnecompliance:manage. - Angrenzende Suiten (37 Tests: Legal, Admin-Grenzen, ActivityLog) weiter grün; Pint sauber.
Phase 3 — Magic-Link-Verifikation + Intake-Trichter ✅ umgesetzt (22.07.2026)
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 bestehendenmelden-Ü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.
3.2 Magic-Link ✅
- ✅
MagicLinkGenerator::createReportLink(): purposepress_release_report(Enum-Migration2026_07_22_171134_add_report_purpose_to_magic_links_table.php—purposeist ein DB-Enum!), payload{press_release_id, contact_email},user_id= PM-Owner, TTLlegal.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 (MusterMagicLinkConsumeController) → Session-Marker (legal.report_session_minutes= 60) → Anliegen-Auswahl. Kein Login. - ✅ Eingeloggter Creator: POST
me.press-releases.report(prüftaccessibleBy,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
LegalRequestmittype,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.*inlang/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 (PHPslevenshtein()ist auf 255 Bytes begrenzt); Vergleich aufstrip_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 durchPressReleaseHtmlSanitizer) →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):LegalRequesttype=dsgvo, payload = Schwärzungsauftrag, Statusopen. - 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):
SinglePurchaseohneuser_id-Zwang bzw. auf PM-Owner gebucht,legal_request_idals Referenz; Stripe-Session mitcustomer_email= verifizierte Kontaktadresse. Webhook-Fulfillment bleibt identisch (idempotent, row-locked).
- Variante (empfohlen):
- Nach Zahlung:
depublish()→ Tombstone (410 + noindex existiert) →lockSync()→LegalRequestresolved/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_atbleibt). 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 ausProvidesPressReleaseFormRules) → Preis anzeigen → Gast-Checkout (SinglePurchaseType-CaseChange,adHocLineItem-Muster). - Nach Zahlung: Änderung in Entwurfs-Kopie/Pending-Payload halten (nicht direkt live), dann
ClassifyPressReleaseauf den neuen Inhalt:- grün/gelb → Änderung anwenden, PM bleibt/wird published,
lockSync(), Kauf aufConsumed, 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.
- grün/gelb → Änderung anwenden, PM bleibt/wird published,
- KiAudit-Trail entsteht automatisch über den bestehenden Job.
- Tests: rot verwirft ohne PM-Änderung; grün wendet an; Webhook-Idempotenz.
Phase 8 — Querschnitt-Abschluss
- 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.) - Auslieferungs-Checkliste (§5) verifizieren: Tombstone 410 ✅,
noindexim Tombstone-View prüfen/ergänzen, Sitemap schließt Archived aus (ScopevisibleInArchiveprüfen) + Cache-Invalidierung nach Mutation, interne Suche (FULLTEXT aufpress_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). - Eskalationsstufen-Texte: Mail-Vorlagen zweistufig — Angebots-/Eingangsmail ohne Aufsichtsbehörden-Hinweis, Ablehnungsmail (aus Admin-Queue
reject()) mit Art.-12-Pflichthinweis. Textbausteine inlang/. - Doku: kurzer Abschnitt in
docs/konzept/bzw. Update dieses Ordners;CLAUDE.mdunberührt. - 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:
RefreshDatabaseglobal,RolesAndPermissionsSeederimbeforeEach, FactoriesPressRelease::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 --dirtyvor 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 |