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:
parent
99ab47ba1f
commit
8916bcce7c
9 changed files with 311 additions and 7 deletions
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||
|
||||
use App\Enums\SinglePurchaseStatus;
|
||||
use App\Enums\SinglePurchaseType;
|
||||
use App\Models\BookingConfirmation;
|
||||
use App\Models\Plan;
|
||||
use App\Models\SinglePurchase;
|
||||
use App\Services\Billing\CreditTopupService;
|
||||
|
|
@ -33,6 +34,10 @@ class CheckoutController extends Controller
|
|||
return $this->backToProfile();
|
||||
}
|
||||
|
||||
if ($redirect = $this->requireEntrepreneurConfirmation($request)) {
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
if ($user->subscribed()) {
|
||||
return $this->backToBookings(__('Es besteht bereits ein aktives Abo. Ein Tarifwechsel ist aktuell über den Support möglich.'));
|
||||
}
|
||||
|
|
@ -54,6 +59,10 @@ class CheckoutController extends Controller
|
|||
return $this->backToProfile();
|
||||
}
|
||||
|
||||
if ($redirect = $this->requireEntrepreneurConfirmation($request)) {
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
if (! config('billing.single_pm_stripe_price_id')) {
|
||||
return $this->backToBookings(__('Die Einzel-Pressemitteilung ist noch nicht buchbar. Bitte versuchen Sie es später erneut.'));
|
||||
}
|
||||
|
|
@ -80,6 +89,10 @@ class CheckoutController extends Controller
|
|||
return $this->backToProfile();
|
||||
}
|
||||
|
||||
if ($redirect = $this->requireEntrepreneurConfirmation($request)) {
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
if (! $topups->findPack($pack)) {
|
||||
return $this->backToBookings(__('Dieses Credit-Paket ist nicht verfügbar.'));
|
||||
}
|
||||
|
|
@ -111,6 +124,23 @@ class CheckoutController extends Controller
|
|||
->with('checkout-notice', $notice);
|
||||
}
|
||||
|
||||
/**
|
||||
* B2B-Gate (T1neu, AGB v0.6 § 1 Abs. 4): Jeder Stripe-Checkout setzt eine
|
||||
* zeitnah protokollierte Unternehmer-Bestätigung voraus (Pflicht-Checkbox
|
||||
* im Buchungs-Modal). Direktaufrufe der Checkout-Routen ohne Bestätigung
|
||||
* werden auf die Buchungsseite zurückgeleitet.
|
||||
*/
|
||||
private function requireEntrepreneurConfirmation(Request $request): ?RedirectResponse
|
||||
{
|
||||
if (BookingConfirmation::query()->recentFor($request->user())->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->backToBookings(
|
||||
__('Bitte bestätigen Sie im Buchungsdialog, dass Sie als Unternehmer bestellen — das Angebot richtet sich ausschließlich an Unternehmen, Selbstständige und Agenturen.')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buchungs-Voraussetzung (12.06.2026): ohne vollständige
|
||||
* Rechnungsadresse kein Checkout — der Hinweis erscheint direkt auf
|
||||
|
|
|
|||
51
app/Models/BookingConfirmation.php
Normal file
51
app/Models/BookingConfirmation.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Protokollierte Unternehmer-Bestätigung vor einem Stripe-Checkout
|
||||
* (T1neu, AGB v0.6 § 1 Abs. 4): Nachweis, dass der Kunde bei der Buchung
|
||||
* bestätigt hat, als Unternehmer zu handeln — samt akzeptierter AGB-Version.
|
||||
*/
|
||||
class BookingConfirmation extends Model
|
||||
{
|
||||
/**
|
||||
* Gültigkeitsfenster: Der Checkout muss zeitnah nach der Bestätigung
|
||||
* starten, sonst wird sie erneut verlangt (Direktaufrufe der
|
||||
* Checkout-Routen ohne Modal-Bestätigung laufen so ins Leere).
|
||||
*/
|
||||
public const VALID_MINUTES = 30;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'booking_type',
|
||||
'booking_ref',
|
||||
'interval',
|
||||
'agb_version',
|
||||
'ip',
|
||||
'confirmed_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'confirmed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function scopeRecentFor(Builder $query, User $user): Builder
|
||||
{
|
||||
return $query
|
||||
->where('user_id', $user->id)
|
||||
->where('confirmed_at', '>=', now()->subMinutes(self::VALID_MINUTES));
|
||||
}
|
||||
}
|
||||
20
config/legal.php
Normal file
20
config/legal.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rechtstext-Versionen (Übergangslösung bis zum legal_documents-System, E3)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Zentrale Quelle für die aktuell veröffentlichte AGB-Fassung. Wird auf
|
||||
| der AGB-Seite angezeigt und bei jeder Unternehmer-Bestätigung im
|
||||
| Checkout protokolliert (booking_confirmations.agb_version, T1neu).
|
||||
| Bei einer neuen AGB-Fassung hier Version + Stand aktualisieren.
|
||||
|
|
||||
*/
|
||||
|
||||
'agb_version' => '0.6',
|
||||
'agb_effective_from' => '2026-07-03',
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Unternehmer-Bestätigung je Buchungsvorgang (T1neu, AGB v0.6 § 1 Abs. 4):
|
||||
* Der Kunde bestätigt vor dem Stripe-Checkout, in Ausübung seiner
|
||||
* gewerblichen oder selbständigen beruflichen Tätigkeit zu handeln.
|
||||
* Protokolliert werden Zeitpunkt, IP und die akzeptierte AGB-Version —
|
||||
* das B2B-Gate stützt sich auf diesen Nachweis.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('booking_confirmations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
// subscription | single_pm | credit_pack
|
||||
$table->string('booking_type', 40);
|
||||
// Plan-Slug bzw. Pack-Key; null bei Einzel-PM.
|
||||
$table->string('booking_ref', 80)->nullable();
|
||||
$table->string('interval', 20)->nullable();
|
||||
$table->string('agb_version', 40);
|
||||
$table->string('ip', 45)->nullable();
|
||||
$table->timestamp('confirmed_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'confirmed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('booking_confirmations');
|
||||
}
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@
|
|||
$contactEmail = $brand['contact_email'] ?? 'info@businessportal24.com';
|
||||
$hubUrl = rtrim((string) config('domains.domain_portal_url', config('app.url')), '/');
|
||||
$hubLabel = str_replace(['https://', 'http://'], '', $hubUrl);
|
||||
|
||||
// Zentrale Versionsangabe (config/legal.php) — dieselbe Version wird bei
|
||||
// der Unternehmer-Bestätigung im Checkout protokolliert (T1neu).
|
||||
$agbVersion = (string) config('legal.agb_version');
|
||||
$agbStand = \Illuminate\Support\Carbon::parse((string) config('legal.agb_effective_from'))->locale('de')->isoFormat('D. MMMM YYYY');
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
|
@ -21,7 +26,7 @@
|
|||
eyebrow="Rechtliches"
|
||||
title="Allgemeine Geschäftsbedingungen"
|
||||
breadcrumb="AGB"
|
||||
updated="Stand: 3. Juli 2026 · Version 0.6"
|
||||
updated="Stand: {{ $agbStand }} · Version {{ $agbVersion }}"
|
||||
>
|
||||
<div class="pm-body max-w-[760px]">
|
||||
<h2>§ 1 Geltungsbereich und Vertragspartner</h2>
|
||||
|
|
@ -443,7 +448,7 @@
|
|||
|
||||
<hr>
|
||||
<p>
|
||||
<strong>Stand:</strong> 3. Juli 2026 · <strong>Version:</strong> 0.6<br>
|
||||
<strong>Stand:</strong> {{ $agbStand }} · <strong>Version:</strong> {{ $agbVersion }}<br>
|
||||
Diese AGB gelten für Buchungen über pressekonto ({{ $hubLabel }}) mit Veröffentlichung auf
|
||||
presseecho.de und businessportal24.com.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use App\Enums\UserPaymentOptionStatus;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\BookingConfirmation;
|
||||
use App\Models\Plan;
|
||||
use App\Models\SinglePurchase;
|
||||
use App\Models\User;
|
||||
|
|
@ -49,8 +50,11 @@ test('the bookings page renders the active plans with checkout links', function
|
|||
->assertSee('max. 2 Veröffentlichungen pro Tag')
|
||||
->assertSee('2 Monate gratis')
|
||||
->assertDontSee('Versteckt')
|
||||
// Buchung läuft über das Bestätigungs-Modal (selectBooking → Checkout-URL).
|
||||
// Buchung läuft über das Bestätigungs-Modal; der Checkout-Link
|
||||
// erscheint erst nach der Unternehmer-Bestätigung (T1neu).
|
||||
->call('selectBooking', 'subscription', 'business', 'monthly')
|
||||
->assertDontSee(route('me.checkout.subscription', ['planSlug' => 'business', 'interval' => 'monthly']), false)
|
||||
->set('entrepreneurConfirmed', true)
|
||||
->assertSee(route('me.checkout.subscription', ['planSlug' => 'business', 'interval' => 'monthly']), false);
|
||||
});
|
||||
|
||||
|
|
@ -86,6 +90,7 @@ test('the single pm block links to its checkout', function () {
|
|||
->call('selectBooking', 'single_pm')
|
||||
->assertSee('Buchung bestätigen')
|
||||
->assertSee('zzgl. 19 % USt.')
|
||||
->set('entrepreneurConfirmed', true)
|
||||
->assertSee(route('me.checkout.single-pm'), false);
|
||||
});
|
||||
|
||||
|
|
@ -207,8 +212,9 @@ test('the wallet block shows the balance and the credit pack topup links', funct
|
|||
->assertSee('Preisliste')
|
||||
->assertSee('Was kostet wie viel')
|
||||
->assertDontSee('Prüfkontingent') // Phase 2 — vorerst ausgeblendet
|
||||
// Paket-Kauf läuft über das Bestätigungs-Modal.
|
||||
// Paket-Kauf läuft über das Bestätigungs-Modal (inkl. T1neu-Checkbox).
|
||||
->call('selectBooking', 'credit_pack', 'p25')
|
||||
->set('entrepreneurConfirmed', true)
|
||||
->assertSee(route('me.checkout.credit-topup', ['pack' => 'p25']), false);
|
||||
});
|
||||
|
||||
|
|
@ -252,6 +258,8 @@ test('with a billing address the banner is gone and the booking modal offers the
|
|||
->assertDontSee('Rechnungsadresse fehlt')
|
||||
->call('selectBooking', 'single_pm')
|
||||
->assertSee('Kostenpflichtig buchen')
|
||||
->assertSee('gewerblichen oder selbständigen beruflichen Tätigkeit')
|
||||
->set('entrepreneurConfirmed', true)
|
||||
->assertSee(route('me.checkout.single-pm'), false);
|
||||
});
|
||||
|
||||
|
|
@ -283,3 +291,40 @@ test('buying an extra pm without enough credits shows the mini checkout hint', f
|
|||
|
||||
expect($user->singlePurchases()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('checking the entrepreneur box records a booking confirmation with the agb version', function () {
|
||||
/** @var TestCase $this */
|
||||
// T1neu (AGB v0.6 § 1 Abs. 4): Die Pflicht-Checkbox protokolliert
|
||||
// Zeitpunkt + AGB-Version — das B2B-Gate stützt sich auf diesen Nachweis.
|
||||
$user = bookingsTestCustomer();
|
||||
BillingAddress::factory()->create(['user_id' => $user->id]);
|
||||
config()->set('billing.single_pm_stripe_price_id', 'price_test_single_pm');
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->call('selectBooking', 'single_pm')
|
||||
->set('entrepreneurConfirmed', true);
|
||||
|
||||
$confirmation = BookingConfirmation::query()->sole();
|
||||
|
||||
expect($confirmation->user_id)->toBe($user->id);
|
||||
expect($confirmation->booking_type)->toBe('single_pm');
|
||||
expect($confirmation->agb_version)->toBe((string) config('legal.agb_version'));
|
||||
expect($confirmation->confirmed_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('selecting a new booking resets the entrepreneur confirmation checkbox', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = bookingsTestCustomer();
|
||||
BillingAddress::factory()->create(['user_id' => $user->id]);
|
||||
config()->set('billing.single_pm_stripe_price_id', 'price_test_single_pm');
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->call('selectBooking', 'single_pm')
|
||||
->set('entrepreneurConfirmed', true)
|
||||
->call('selectBooking', 'single_pm')
|
||||
->assertSet('entrepreneurConfirmed', false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Enums\SinglePurchaseStatus;
|
||||
use App\Enums\SinglePurchaseType;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\BookingConfirmation;
|
||||
use App\Models\Plan;
|
||||
use App\Models\SinglePurchase;
|
||||
use App\Models\User;
|
||||
|
|
@ -25,9 +26,26 @@ function checkoutTestCustomer(): User
|
|||
// Buchungs-Voraussetzung (12.06.2026): vollständige Rechnungsadresse.
|
||||
BillingAddress::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
// B2B-Gate (T1neu): Jeder Checkout verlangt eine zeitnah protokollierte
|
||||
// Unternehmer-Bestätigung (Pflicht-Checkbox im Buchungs-Modal).
|
||||
confirmEntrepreneur($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function confirmEntrepreneur(User $user): BookingConfirmation
|
||||
{
|
||||
return BookingConfirmation::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'booking_type' => 'subscription',
|
||||
'booking_ref' => null,
|
||||
'interval' => null,
|
||||
'agb_version' => (string) config('legal.agb_version'),
|
||||
'ip' => '127.0.0.1',
|
||||
'confirmed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stripe-Checkout-Stub: Der Controller gibt das Checkout-Objekt zurück,
|
||||
* der Router ruft toResponse() — wir leiten auf eine Fake-Stripe-URL um.
|
||||
|
|
@ -79,6 +97,32 @@ test('a checkout without a complete billing address redirects to the profile', f
|
|||
expect(SinglePurchase::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('a checkout without a recent entrepreneur confirmation bounces back to the bookings page', function () {
|
||||
/** @var TestCase $this */
|
||||
// B2B-Gate (T1neu): Rechnungsadresse vorhanden, aber keine (bzw. nur eine
|
||||
// abgelaufene) Unternehmer-Bestätigung → kein Stripe-Checkout.
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('customer');
|
||||
BillingAddress::factory()->create(['user_id' => $user->id]);
|
||||
$plan = Plan::factory()->create(['stripe_price_id_monthly' => 'price_test_m_b2b']);
|
||||
config()->set('billing.single_pm_stripe_price_id', 'price_test_single_pm');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'monthly']))
|
||||
->assertRedirect(route('me.bookings.index'))
|
||||
->assertSessionHas('checkout-notice');
|
||||
|
||||
// Abgelaufene Bestätigung zählt nicht.
|
||||
confirmEntrepreneur($user)->update(['confirmed_at' => now()->subMinutes(BookingConfirmation::VALID_MINUTES + 1)]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('me.checkout.single-pm'))
|
||||
->assertRedirect(route('me.bookings.index'))
|
||||
->assertSessionHas('checkout-notice');
|
||||
|
||||
expect(SinglePurchase::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('a plan without a synced stripe price redirects back with a notice', function () {
|
||||
/** @var TestCase $this */
|
||||
$plan = Plan::factory()->create(['stripe_price_id_monthly' => null]);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Enums\SinglePurchaseStatus;
|
||||
use App\Listeners\ProcessStripeWebhook;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\BookingConfirmation;
|
||||
use App\Models\User;
|
||||
use App\Services\Billing\CreditTopupService;
|
||||
use App\Services\Billing\CreditWalletService;
|
||||
|
|
@ -91,6 +92,15 @@ test('the topup checkout route starts a stripe checkout with a complete billing
|
|||
$user->assignRole('customer');
|
||||
BillingAddress::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
// B2B-Gate (T1neu): Checkout setzt eine zeitnahe Unternehmer-Bestätigung voraus.
|
||||
BookingConfirmation::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'booking_type' => 'credit_pack',
|
||||
'booking_ref' => 'p25',
|
||||
'agb_version' => (string) config('legal.agb_version'),
|
||||
'confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
$checkout = Mockery::mock(Checkout::class);
|
||||
$checkout->shouldReceive('toResponse')->andReturn(new RedirectResponse('https://checkout.stripe.com/c/pay/test'));
|
||||
$mock = Mockery::mock(StripeCheckoutService::class);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue