From b863b67979d5a0b935a105b08330881f3ac0134b Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 20 Apr 2026 19:05:41 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Widerrufsrecht-Best=C3=A4tigung=20beim?= =?UTF-8?q?=20Upgrade=20(Free=20=E2=86=92=20Pro)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migration: withdrawal_waivers (user, plan, billing, amount, IP, user_agent, confirmed_at, pdf_path für spätere PDF-Generierung) - Model: WithdrawalWaiver mit User/Plan-Relation - Checkout/Index: rightAcknowledged + waiverConfirmed Properties; Validierung vor Checkout; Waiver-Record wird vor Zahlung gespeichert - View: Amber-Box mit Hinweistext + 2 Checkboxen; CTA-Button disabled solange nicht beide bestätigt; nur bei paid Plänen sichtbar - Übersetzungen: waiver_info, waiver_right_acknowledged, waiver_confirmed, waiver_required (DE + EN) Co-Authored-By: Claude Sonnet 4.6 --- src/app/Livewire/Checkout/Index.php | 27 +++++++++++- src/app/Models/WithdrawalWaiver.php | 43 +++++++++++++++++++ ...000001_create_withdrawal_waivers_table.php | 33 ++++++++++++++ src/database/seeders/TranslationSeeder.php | 16 +++++++ .../views/livewire/checkout/index.blade.php | 34 +++++++++++++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/app/Models/WithdrawalWaiver.php create mode 100644 src/database/migrations/2026_04_20_000001_create_withdrawal_waivers_table.php diff --git a/src/app/Livewire/Checkout/Index.php b/src/app/Livewire/Checkout/Index.php index 9e67755..298644e 100644 --- a/src/app/Livewire/Checkout/Index.php +++ b/src/app/Livewire/Checkout/Index.php @@ -6,6 +6,7 @@ use App\Enums\SubscriptionStatus; use App\Models\Feature; use App\Models\Plan; use App\Models\Subscription; +use App\Models\WithdrawalWaiver; use App\Services\StripeService; use Livewire\Attributes\Computed; use Livewire\Component; @@ -15,6 +16,9 @@ class Index extends Component public Plan $plan; public string $billing = 'monthly'; + public bool $rightAcknowledged = false; + public bool $waiverConfirmed = false; + public function mount(string $planId, string $billing = 'monthly'): void { $this->plan = Plan::public()->where('active', true)->findOrFail($planId); @@ -53,13 +57,34 @@ class Index extends Component $stripe = app(StripeService::class); try { - // ── Free Plan: Stripe-Abo kündigen ──────────────────────────── + // ── Free Plan: Stripe-Abo kündigen (kein Widerrufsrecht nötig) ─ if ($this->plan->isFree()) { $stripe->cancelUserSubscription($user); $this->redirect(route('subscription.index'), navigate: false); return; } + // ── Widerrufsrecht-Bestätigung prüfen ───────────────────────── + if (!$this->rightAcknowledged || !$this->waiverConfirmed) { + $this->dispatch('notify', ['type' => 'error', 'message' => t('checkout.waiver_required')]); + return; + } + + // ── Widerrufsverzicht speichern ──────────────────────────────── + WithdrawalWaiver::create([ + 'user_id' => $user->id, + 'plan_id' => $this->plan->id, + 'plan_name' => $this->plan->name, + 'billing' => $this->billing, + 'amount_cents' => $this->activePrice, + 'checkout_type' => $this->checkoutType, + 'right_acknowledged' => true, + 'waiver_confirmed' => true, + 'confirmed_at' => now(), + 'ip_address' => request()->ip(), + 'user_agent' => request()->userAgent(), + ]); + // ── Upgrade / Downgrade: bestehendes Abo in-place updaten ───── $existingSub = $this->activeStripeSub; if ($existingSub) { diff --git a/src/app/Models/WithdrawalWaiver.php b/src/app/Models/WithdrawalWaiver.php new file mode 100644 index 0000000..13be754 --- /dev/null +++ b/src/app/Models/WithdrawalWaiver.php @@ -0,0 +1,43 @@ + 'boolean', + 'waiver_confirmed' => 'boolean', + 'confirmed_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function plan(): BelongsTo + { + return $this->belongsTo(Plan::class); + } +} diff --git a/src/database/migrations/2026_04_20_000001_create_withdrawal_waivers_table.php b/src/database/migrations/2026_04_20_000001_create_withdrawal_waivers_table.php new file mode 100644 index 0000000..b18f11f --- /dev/null +++ b/src/database/migrations/2026_04_20_000001_create_withdrawal_waivers_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + $table->foreignUuid('plan_id')->nullable()->constrained()->nullOnDelete(); + $table->string('plan_name'); + $table->string('billing', 20); + $table->unsignedInteger('amount_cents'); + $table->string('checkout_type', 20); + $table->boolean('right_acknowledged')->default(false); + $table->boolean('waiver_confirmed')->default(false); + $table->timestamp('confirmed_at'); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->string('pdf_path')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('withdrawal_waivers'); + } +}; diff --git a/src/database/seeders/TranslationSeeder.php b/src/database/seeders/TranslationSeeder.php index 5431bfd..4280199 100644 --- a/src/database/seeders/TranslationSeeder.php +++ b/src/database/seeders/TranslationSeeder.php @@ -758,6 +758,22 @@ class TranslationSeeder extends Seeder 'checkout.redirect_stripe' => ['de' => 'Du wirst sicher zu Stripe weitergeleitet.', 'en' => 'You will be securely redirected to Stripe.'], 'checkout.no_checkout' => ['de' => 'Kein Checkout nötig – Stripe verarbeitet die Änderung direkt.', 'en' => 'No checkout needed – Stripe processes the change directly.'], 'checkout.cancel_effective' => ['de' => 'Kündigung wird sofort wirksam.', 'en' => 'Cancellation takes effect immediately.'], + 'checkout.waiver_info' => [ + 'de' => 'Hinweis zum Widerrufsrecht: Du hast das Recht, diesen Vertrag innerhalb von 14 Tagen ohne Angabe von Gründen zu widerrufen. Da wir die Dienstleistung sofort bereitstellen, erlischt dein Widerrufsrecht mit Beginn der Leistungserbringung – sofern du ausdrücklich zustimmst.', + 'en' => 'Right of withdrawal notice: You have the right to withdraw from this contract within 14 days without giving any reason. Since we provide the service immediately, your right of withdrawal expires upon commencement of the service – provided you expressly agree.', + ], + 'checkout.waiver_right_acknowledged' => [ + 'de' => 'Ich bestätige, dass ich über mein 14-tägiges Widerrufsrecht informiert wurde.', + 'en' => 'I confirm that I have been informed about my 14-day right of withdrawal.', + ], + 'checkout.waiver_confirmed' => [ + 'de' => 'Ich verlange ausdrücklich die sofortige Ausführung des Vertrags und bestätige, dass ich mit Beginn der Leistungserbringung mein Widerrufsrecht verliere.', + 'en' => 'I expressly request the immediate execution of the contract and acknowledge that I will lose my right of withdrawal upon commencement of the service.', + ], + 'checkout.waiver_required' => [ + 'de' => 'Bitte bestätige beide Checkboxen zum Widerrufsrecht.', + 'en' => 'Please confirm both checkboxes regarding the right of withdrawal.', + ], // ── Checkout Success ───────────────────── 'checkout.success.processing' => ['de' => 'Zahlung wird verarbeitet…', 'en' => 'Processing payment…'], diff --git a/src/resources/views/livewire/checkout/index.blade.php b/src/resources/views/livewire/checkout/index.blade.php index aed0c9a..0fc0bd4 100644 --- a/src/resources/views/livewire/checkout/index.blade.php +++ b/src/resources/views/livewire/checkout/index.blade.php @@ -256,6 +256,38 @@ @endif + {{-- Widerrufsrecht-Checkboxen (nur bei paid plans) --}} + @if($this->checkoutType !== 'cancel') +
+
+ +

+ {{ t('checkout.waiver_info') }} +

+
+ + +
+ @endif + {{-- CTA --}} @if($this->checkoutType === 'cancel')