recht: Meldeverfahren v2 Phasen 5+7 — DSGVO-Strukturformular, 10-€-Checkout mit KI-Vorprüfung, Triage & Panel-Modal
Phase 7 (Spur D, E2 revidiert): KI-Vorprüfung VOR der Zahlung im Modal — rot lehnt ohne Zahlung ab, gelb/grün führt zum Stripe-Checkout (SinglePurchaseType::Change, 10 € netto), nach Zahlungseingang übernimmt der Webhook die geprüfte Fassung sofort (applyPaid: Snapshot, Sperre, Verlauf). Nur einmal je PM; Obergrenze legal.change_max_distance_percent (50 %) verhindert die verdeckte Selbst-Depublizierung. Phase 5 (Spur B): strukturiertes Schwärzungsformular mit Pflicht-Vorschau im verifizierten Web-Bereich UND als Modal im Kunden-Panel; Admin-Editor wird aus payload.redaction vorbefüllt. Umsetzungsmail LegalRequestExecuted an Antragsteller, Bestätigungsmail PressReleaseChangeApplied (Spur A/D, neuer abbestellbarer Typ press_release_corrected). Außerdem: - Re-Sync-Sperre + Sitemap-Invalidierung fest in depublish() verankert (Lücke im bezahlten Webhook-Pfad geschlossen) - Öffentliche „Rechtliches"-Seite als zweistufige Triage: primär der Portal-Anmeldelink, Formular nur Fallback mit Identifizierungs-Hinweis; Store auf report/dsgvo/personal_rights gehärtet - Panel-Button „Ändern/Melden" öffnet Anliegen-Modal (4 Spuren) statt des Web-Formulars - Pressekontakt-Fallback für Legacy-Bestände (30k PMs ohne Verknüpfung): Detailseite + pressContactEmail() nutzen PM-Kontakt → Firmen-Kontakt → Firmendaten - Korrektur-Gate entity-fest (Editor-Normalisierung zählt nicht als Änderung), toEditorHtml() in Admin-Edit + Korrektur-Panel, Wochenlimit-Hinweis in Text und Bremsen-Meldung Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b4b2e013d6
commit
9afbedddeb
40 changed files with 2507 additions and 169 deletions
|
|
@ -3,9 +3,11 @@
|
|||
use App\Enums\LegalRequestStatus;
|
||||
use App\Enums\LegalRequestType;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Mail\LegalRequestExecuted;
|
||||
use App\Models\LegalRequest;
|
||||
use App\Services\PressRelease\AnonymizationService;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -38,6 +40,16 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
|||
$this->id = $id;
|
||||
$this->adminNote = (string) ($this->request()->admin_note ?? '');
|
||||
$this->redactionReplacement = AnonymizationService::DEFAULT_REPLACEMENT;
|
||||
|
||||
// Spur B (Phase 5): strukturierter Schwärzungsauftrag aus dem
|
||||
// verifizierten Web-Formular befüllt den Editor vor — der Admin
|
||||
// prüft, ergänzt und führt aus.
|
||||
$redaction = (array) ($this->request()->payload['redaction'] ?? []);
|
||||
|
||||
if ($redaction !== []) {
|
||||
$this->redactionTerms = implode("\n", (array) ($redaction['terms'] ?? []));
|
||||
$this->detachContacts = (bool) ($redaction['detach_contacts'] ?? true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,6 +106,8 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
|||
'executed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->sendExecutedMail($request, 'anonymized');
|
||||
|
||||
$this->redactionPreview = null;
|
||||
$this->notification = __('Anonymisierung ausgeführt — PM geschwärzt, Re-Sync-Sperre gesetzt.');
|
||||
}
|
||||
|
|
@ -117,8 +131,7 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
|||
return;
|
||||
}
|
||||
|
||||
$service->depublish($pressRelease);
|
||||
$pressRelease->lockSync('Depublizierung im Meldeverfahren '.$request->reference());
|
||||
$service->depublish($pressRelease, 'Depublizierung im Meldeverfahren '.$request->reference());
|
||||
|
||||
$request->update([
|
||||
'status' => LegalRequestStatus::Resolved->value,
|
||||
|
|
@ -128,9 +141,26 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
|||
'executed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->sendExecutedMail($request, 'depublished');
|
||||
|
||||
$this->notification = __('PM depubliziert (Tombstone, umkehrbar) — Re-Sync-Sperre gesetzt.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Umsetzungs-Bestätigung an die meldende Person (Phase 5) — nur wenn
|
||||
* eine Antragsteller-Adresse vorliegt (interne Vorgänge haben keine).
|
||||
*
|
||||
* @param 'anonymized'|'depublished' $action
|
||||
*/
|
||||
private function sendExecutedMail(LegalRequest $request, string $action): void
|
||||
{
|
||||
if (! $request->requester_email) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mail::to($request->requester_email)->queue(new LegalRequestExecuted($request->fresh(), $action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rückgängig machen: spielt den verschlüsselten Vorher-Snapshot einer
|
||||
* ausgeführten Korrektur/Anonymisierung zurück (bis zur Löschfrist).
|
||||
|
|
@ -370,6 +400,15 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
|||
{{ __('Schwärzt personenbezogene Daten über Wortgrenzen (inkl. Varianten wie „Herr Teplitsky", „Teplitskys"). Kostenlos, ohne KI-Gate; Alt-Werte werden verschlüsselt gesichert und die Re-Sync-Sperre gesetzt.') }}
|
||||
</p>
|
||||
|
||||
@if ((array) ($request->payload['redaction'] ?? []) !== [])
|
||||
<div class="rounded-[6px] border border-[color:var(--color-warn)]/40 bg-[color:var(--color-warn-soft)] px-3 py-2 text-[12.5px] text-[color:var(--color-ink)]">
|
||||
{{ __('Strukturierter Schwärzungsauftrag aus dem Web-Formular — Begriffe und Optionen sind unten vorbefüllt (:hits Treffer laut Antragsteller-Vorschau).', ['hits' => (int) ($request->payload['redaction']['preview_total_hits'] ?? 0)]) }}
|
||||
@if ($request->payload['redaction']['remove_images_requested'] ?? false)
|
||||
<span class="font-semibold">{{ __('Antragsteller bittet zusätzlich um Entfernung von Bildern mit Personenbezug — unten auswählen.') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:field>
|
||||
<flux:textarea wire:model="redactionTerms" rows="4"
|
||||
:label="__('Schwärzbegriffe (ein Begriff pro Zeile, Varianten einzeln)')"
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
$this->categoryId = $pr->category_id;
|
||||
$this->title = $pr->title;
|
||||
$this->subtitle = $pr->subtitle ?? '';
|
||||
$this->text = $pr->text;
|
||||
$this->text = app(PressReleaseHtmlSanitizer::class)->toEditorHtml($pr->text);
|
||||
$this->keywords = $pr->keywords ?? '';
|
||||
$this->backlinkUrl = $pr->backlink_url ?? '';
|
||||
$this->boilerplateOverride = $pr->boilerplate_override ?? '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue