T1neu: Unternehmer-Pflicht-Checkbox im Checkout, protokolliert mit AGB-Version

B2B-Gate (AGB v0.6 par. 1 Abs. 4): Das Buchungs-Modal verlangt vor jedem
Stripe-Checkout die Bestaetigung 'Ich bestelle in Ausuebung meiner
gewerblichen oder selbstaendigen beruflichen Taetigkeit (par. 14 BGB)'.

- booking_confirmations: user, Buchungstyp/-ref, AGB-Version, IP,
  confirmed_at (Nachweis laut Datenschutz Abschnitt 7)
- Checkout-Link erscheint erst nach der Checkbox; CheckoutController
  verlangt zusaetzlich serverseitig eine zeitnahe Bestaetigung (30 Min.)
  fuer Abo/Einzel-PM/Credit-Topup — Direktaufrufe laufen ins Leere
- config/legal.php als zentrale AGB-Versionsquelle (Seite + Protokoll,
  Uebergangsloesung bis zum legal_documents-System E3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-03 16:17:59 +00:00
parent 99ab47ba1f
commit 8916bcce7c
9 changed files with 311 additions and 7 deletions

View file

@ -6,6 +6,7 @@ use App\Enums\Tier;
use App\Enums\UserPaymentOptionStatus;
use App\Enums\VatTreatment;
use App\Exceptions\InsufficientCreditsException;
use App\Models\BookingConfirmation;
use App\Models\Plan;
use App\Models\UserPaymentOption;
use App\Services\Billing\CreditPricingService;
@ -33,11 +34,41 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
/** Im Bestätigungs-Modal angezeigte Buchung (inkl. USt.-Aufschlüsselung). */
public ?array $booking = null;
/**
* Unternehmer-Pflicht-Checkbox (T1neu, AGB v0.6 § 1 Abs. 4): erst nach
* der Bestätigung wird der Checkout-Link angeboten; die Bestätigung
* selbst wird mit Timestamp + AGB-Version protokolliert.
*/
public bool $entrepreneurConfirmed = false;
public function formatEuro(int $cents): string
{
return number_format($cents / 100, $cents % 100 === 0 ? 0 : 2, ',', '.') . ' €';
}
/**
* Protokolliert die Unternehmer-Bestätigung (T1neu) in dem Moment, in dem
* die Pflicht-Checkbox gesetzt wird — mit Zeitpunkt, IP und der aktuell
* veröffentlichten AGB-Version. Der Checkout-Controller verlangt eine
* zeitnahe Bestätigung, Direktaufrufe ohne Checkbox laufen ins Leere.
*/
public function updatedEntrepreneurConfirmed(bool $value): void
{
if (! $value || ! $this->booking) {
return;
}
BookingConfirmation::query()->create([
'user_id' => auth()->id(),
'booking_type' => $this->booking['type'],
'booking_ref' => $this->booking['ref'],
'interval' => $this->booking['interval'],
'agb_version' => (string) config('legal.agb_version'),
'ip' => request()->ip(),
'confirmed_at' => now(),
]);
}
/**
* Öffnet das Bestätigungs-Modal für eine Buchung (Tarif-Abo, Einzel-PM
* oder Credit-Paket) mit USt.-Aufschlüsselung anhand der Rechnungsadresse.
@ -68,6 +99,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
$vatCents = $resolver->taxCentsFor($netCents, $treatment);
$this->booking = [
'type' => $type,
'ref' => $ref,
'title' => $title,
'subtitle' => $subtitle,
'interval' => $interval,
@ -81,6 +114,9 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
'needs_address' => ! $user->hasCompleteBillingAddress(),
];
// Jede Buchung verlangt eine frische Unternehmer-Bestätigung.
$this->entrepreneurConfirmed = false;
Flux::modal('confirm-booking')->show();
}
@ -942,13 +978,37 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
</flux:button>
</div>
@else
{{-- Unternehmer-Pflicht-Checkbox (T1neu, AGB § 1 Abs. 4):
Bestätigung wird serverseitig protokolliert (Timestamp,
IP, AGB-Version); ohne sie kein Checkout-Link — und der
Checkout-Controller verlangt sie zusätzlich. --}}
<div class="space-y-2">
<flux:switch
align="left"
wire:model.live="entrepreneurConfirmed"
:label="__('Ich bestelle in Ausübung meiner gewerblichen oder selbständigen beruflichen Tätigkeit (§ 14 BGB) und akzeptiere die AGB.')"
/>
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0 ps-0">
{{ __('Das Angebot richtet sich ausschließlich an Unternehmen, Selbstständige und Agenturen.') }}
<a href="{{ route('agb', ['edition' => 'de']) }}" target="_blank" rel="noopener" class="underline underline-offset-2 hover:text-[color:var(--color-ink)]">
{{ __('AGB (Version :version)', ['version' => config('legal.agb_version')]) }}
</a>
</p>
</div>
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" icon="lock-closed" href="{{ $booking['url'] }}">
{{ __('Kostenpflichtig buchen') }}
</flux:button>
@if ($entrepreneurConfirmed)
<flux:button variant="primary" icon="lock-closed" href="{{ $booking['url'] }}">
{{ __('Kostenpflichtig buchen') }}
</flux:button>
@else
<flux:button variant="primary" icon="lock-closed" disabled>
{{ __('Kostenpflichtig buchen') }}
</flux:button>
@endif
</div>
@endif
</div>