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>
37 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) ✅ 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:
Phase 5 — Spur B strukturiert✅ umgesetzt 23.07. (Details im Phase-5-Abschnitt).- 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. 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; Obergrenzelegal.change_max_distance_percent(50 %). Offen: Gast-Pfad für Magic-Link-Verifizierte, Bestätigungsmail nach Ausführung.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(Typpress_release_corrected, abbestellbar) nach Spur-A-Korrektur (sofort) und Spur-D-Änderung (nach Webhook-Ausführung).- 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✅ erledigt 23.07.: Das ALTER war serverseitig fertig (Migration manuell nachregistriert), alle restlichen Migrationen gelaufen — der komplette Flow ist auf pressekonto.test browser-testbar.- ✅ Öffentliche Triage-Seite (23.07.):
/release/{slug}/rechtlichesist 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 nochreport/dsgvo/personal_rights— Absender-Spuren können nicht mehr als anonyme Queue-Anfrage einlaufen. - 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 ✅ 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:
LegalRequesttype=dsgvo, Statusopen,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 auspayload.redactionund 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 nachexecuteAnonymization(„anonymized") unddepublishRelease(„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):
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-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 % (envLEGAL_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ürexceeds_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 denClassificationManager(KI-Provider, Fallback deterministisch/Blacklist). Ergebnis + Provider landen inpayload.ki_checkder Change-LegalRequest(rot → StatusRejected, sonstOpen). - ✅ Checkout:
CheckoutController::change()(Routeme.checkout.change,checkout/aenderung/{id}) verlangt die offene Change-Anfrage mit gelb/grünemki_checkals Nachweis;SinglePurchaseType::Change(press_release_change, Enum-Migration), Pending-Wiederverwendung wie Depublikation; Anfrage wird übersingle_purchase_idverknüpft.StripeCheckoutService::forChange()(adHoc-Muster). - ✅ Fulfillment:
ProcessStripeWebhook::fulfillChange()→CorrectionService::applyPaid(): Snapshot, Felder schreiben (Text durch Sanitizer),lockSync(), Verlauf, Re-Score; AnfrageResolved/executed_at, KaufConsumed. Reißt die Obergrenze doch (Payload-Manipulation), bleibt der Kauf unkonsumiert zur Admin-Klärung. - ✅ Panel-UI: Modal
paid-changemit Zwischenerklärung (einmalige Prüfung, rot/gelb-grün-Folgen), Loading-State, Ablehnungsansicht mit Gründen, Bestätigungs-Switch (BookingConfirmationTyppress_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:
PressReleaseChangeAppliedan den Auftraggeber nach Spur-A-Korrektur und Spur-D-Ausführung (neuerEmailNotificationType::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_checkder Anfrage).
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 | 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:
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 |
| 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 |