Recht: Erloeschensklausel-Checkbox im Publikationsflow (Par. 356 BGB)

Vierte Bestaetigung im Einreichungs-Modal (sofortige Ausfuehrung,
Erloeschen des Widerrufsrechts mit Vertragserfuellung); Zeitpunkt wird
beim Submit als withdrawal_waiver_consented_at an der PM protokolliert.
Index-Schnell-Submit (wire:confirm) durch das gemeinsame Modal ersetzt -
der Pfad umging bisher alle rechtlichen Bestaetigungen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-03 11:41:31 +00:00
parent 52bc534e7d
commit 20d53988bd
6 changed files with 117 additions and 9 deletions

View file

@ -83,6 +83,7 @@ class PressRelease extends Model
'published_at',
'scheduled_at',
'embargo_at',
'withdrawal_waiver_consented_at',
'legacy_portal',
'legacy_id',
];
@ -105,6 +106,7 @@ class PressRelease extends Model
'published_at' => 'datetime',
'scheduled_at' => 'datetime',
'embargo_at' => 'datetime',
'withdrawal_waiver_consented_at' => 'datetime',
'deleted_at' => 'datetime',
];
}

View file

@ -64,7 +64,13 @@ class PressReleaseService
throw new BlacklistViolationException($reason, $word);
}
$pressRelease->update(['status' => PressReleaseStatus::Review->value]);
// Erlöschensklausel (§ 356 Abs. 4/5 BGB): Das Einreichungs-Modal
// verlangt die Bestätigung der sofortigen Ausführung — der Zeitpunkt
// wird hier zur Nachweisbarkeit protokolliert.
$pressRelease->update([
'status' => PressReleaseStatus::Review->value,
'withdrawal_waiver_consented_at' => now(),
]);
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Review, null, 'customer');
// KI-Klassifikation asynchron anstoßen (Red Flag). Das Routing anhand

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Erlöschensklausel im Publikationsflow (Rechtstexte-Architektur E2):
* Beim Einreichen bestätigt der Kunde per Checkbox, dass er die sofortige
* Ausführung verlangt und sein Widerrufsrecht mit vollständiger
* Vertragserfüllung erlischt (§ 356 Abs. 4/5 BGB). Der Zeitpunkt der
* Bestätigung wird zur Nachweisbarkeit am Datensatz protokolliert.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('press_releases', function (Blueprint $table) {
$table->timestamp('withdrawal_waiver_consented_at')->nullable()->after('embargo_at');
});
}
public function down(): void
{
Schema::table('press_releases', function (Blueprint $table) {
$table->dropColumn('withdrawal_waiver_consented_at');
});
}
};

View file

@ -62,7 +62,7 @@
</div>
</div>
@else
<div class="space-y-5" x-data="{ agb: false, images: false, contact: false }">
<div class="space-y-5" x-data="{ agb: false, images: false, contact: false, waiver: false }">
<div>
<flux:text class="eyebrow muted">{{ __('Veröffentlichung') }}</flux:text>
<flux:heading size="lg">{{ __('Pressemitteilung zur Prüfung einreichen') }}</flux:heading>
@ -113,6 +113,13 @@
x-on:change="contact = $event.target.checked"
:label="__('Die Angaben zum Pressekontakt sind korrekt.')"
/>
{{-- Erlöschensklausel (§ 356 Abs. 4/5 BGB): Bestätigungszeitpunkt
wird serverseitig protokolliert (PressReleaseService). --}}
<flux:switch
align="left"
x-on:change="waiver = $event.target.checked"
:label="__('Ich verlange die sofortige Ausführung und weiß, dass mein Widerrufsrecht mit vollständiger Vertragserfüllung erlischt.')"
/>
</div>
<div class="flex justify-end gap-2">
@ -121,7 +128,7 @@
</flux:modal.close>
<flux:button variant="primary" wire:click="{{ $action }}"
wire:loading.attr="disabled"
x-bind:disabled="! (agb && images && contact)">
x-bind:disabled="! (agb && images && contact && waiver)">
{{ $confirmLabel ?? __('Veröffentlichung anfordern') }}
</flux:button>
</div>

View file

@ -454,12 +454,19 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
<div class="flex items-center gap-1 flex-wrap">
<span class="badge {{ $badgeClass }} dot">{{ $pr->status->label() }}</span>
@if ($status === 'draft')
<button type="button" class="inline-action"
wire:click="submitForReview({{ $pr->id }})"
wire:confirm="{{ __('Pressemitteilung zur Prüfung einreichen?') }}"
title="{{ __('Zur redaktionellen Prüfung senden') }}">
{{ __('Zur Prüfung →') }}
</button>
{{-- Öffnet das gemeinsame Einreichungs-Modal (rechtliche
Bestätigungen inkl. Erlöschensklausel) statt eines
Schnell-Submits, der die Bestätigungen umgehen würde. --}}
<flux:modal.trigger :name="'confirm-submit-'.$pr->id">
<button type="button" class="inline-action"
title="{{ __('Zur redaktionellen Prüfung senden') }}">
{{ __('Zur Prüfung →') }}
</button>
</flux:modal.trigger>
<x-press-release-submit-modal
:name="'confirm-submit-'.$pr->id"
:action="'submitForReview('.$pr->id.')'"
/>
@endif
</div>
</flux:table.cell>

View file

@ -0,0 +1,57 @@
<?php
use App\Enums\PressReleaseStatus;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Queue;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
Queue::fake();
});
test('the submit modal requires the withdrawal waiver confirmation', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a();
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
// Erlöschensklausel (§ 356 Abs. 4/5 BGB) als eigene Bestätigung …
->assertSee('Ich verlange die sofortige Ausführung')
->assertSee('Widerrufsrecht mit vollständiger Vertragserfüllung erlischt')
// … die den Bestätigen-Button mit sperrt.
->assertSeeHtml('agb && images && contact && waiver');
});
test('submitting records the waiver consent timestamp on the press release', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a();
$this->actingAs($customer);
expect($pr->withdrawal_waiver_consented_at)->toBeNull();
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('submitForReview')
->assertHasNoErrors();
$pr->refresh();
expect($pr->status)->toBe(PressReleaseStatus::Review);
expect($pr->withdrawal_waiver_consented_at)->not->toBeNull();
});
test('the index draft action opens the shared submit modal instead of a quick submit', function () {
/** @var TestCase $this */
['customer' => $customer] = makeCustomerForShowPhase8a();
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.index')
->assertSee('Zur Prüfung')
// Gemeinsames Modal inkl. Erlöschensklausel statt wire:confirm,
// das die rechtlichen Bestätigungen umgehen würde.
->assertSee('Ich verlange die sofortige Ausführung')
->assertDontSeeHtml('wire:confirm');
});