UI-Feinschliff Buchungen: Tarife oben, Credits darunter, Preisliste, USt.-Modal

- Reihenfolge: Tarife -> Einzel-PM -> Credit-Wallet & Add-ons -> Preisliste
- Buchungs-Bestaetigungs-Modal mit USt.-Aufschluesselung (netto/USt./gesamt,
  Reverse-Charge & steuerbefreit) vor Stripe; greift fuer Tarife, Einzel-PM
  und Credit-Pakete (selectBooking)
- Proaktiver Banner oben, wenn die Rechnungsadresse fehlt (Link ins Profil);
  im Modal Adress-Gate statt Stripe-Link
- Extra-PM-Karte nur bei Guthaben > 0 oder aktivem Abo (sonst Preisliste)
- Pruefkontingent ausgeblendet (Phase 2)
- Preisliste: Extra-PM nach Tarif (eigener Tarif markiert), Boost-Laufzeiten,
  Nachweis-PDF

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-17 15:26:04 +00:00
parent 77a4476fd0
commit 3c6190099f
2 changed files with 463 additions and 167 deletions

View file

@ -1,6 +1,7 @@
<?php
use App\Enums\UserPaymentOptionStatus;
use App\Models\BillingAddress;
use App\Models\Plan;
use App\Models\SinglePurchase;
use App\Models\User;
@ -36,7 +37,9 @@ test('the bookings page renders the active plans with checkout links', function
]);
Plan::factory()->inactive()->create(['name' => 'Versteckt']);
$this->actingAs(bookingsTestCustomer());
$user = bookingsTestCustomer();
BillingAddress::factory()->create(['user_id' => $user->id]);
$this->actingAs($user);
LivewireVolt::test('customer.bookings')
->assertSee('Business')
@ -45,20 +48,26 @@ test('the bookings page renders the active plans with checkout links', function
->assertSee('Pressemitteilungen pro Monat')
->assertSee('max. 2 Veröffentlichungen pro Tag')
->assertSee('2 Monate gratis')
->assertSee(route('me.checkout.subscription', ['planSlug' => 'business', 'interval' => 'monthly']), false)
->assertSee(route('me.checkout.subscription', ['planSlug' => 'business', 'interval' => 'yearly']), false)
->assertDontSee('Versteckt');
->assertDontSee('Versteckt')
// Buchung läuft über das Bestätigungs-Modal (selectBooking → Checkout-URL).
->call('selectBooking', 'subscription', 'business', 'monthly')
->assertSee(route('me.checkout.subscription', ['planSlug' => 'business', 'interval' => 'monthly']), false);
});
test('the single pm block links to its checkout', function () {
/** @var TestCase $this */
config()->set('billing.single_pm_stripe_price_id', 'price_test_single_pm');
$this->actingAs(bookingsTestCustomer());
$user = bookingsTestCustomer();
BillingAddress::factory()->create(['user_id' => $user->id]);
$this->actingAs($user);
LivewireVolt::test('customer.bookings')
->assertSee('Einzel-Pressemitteilung — ohne Abo')
->assertSee('19 €')
->call('selectBooking', 'single_pm')
->assertSee('Buchung bestätigen')
->assertSee('zzgl. 19 % USt.')
->assertSee(route('me.checkout.single-pm'), false);
});
@ -167,6 +176,7 @@ test('the checkout success banner is shown after returning from stripe', functio
test('the wallet block shows the balance and the credit pack topup links', function () {
/** @var TestCase $this */
$user = bookingsTestCustomer();
BillingAddress::factory()->create(['user_id' => $user->id]);
app(CreditWalletService::class)->credit($user, 30);
$this->actingAs($user);
@ -175,9 +185,55 @@ test('the wallet block shows the balance and the credit pack topup links', funct
->assertSee('Credit-Wallet & Add-ons')
->assertSee('30')
->assertSee('27 Credits') // Bonus-Paket
->assertSee(route('me.checkout.credit-topup', ['pack' => 'p25']), false)
->assertSee('Prüfkontingent')
->assertSee('4 / 4'); // Einzel-Tier: 4 Freiprüfungen
->assertSee('Preisliste')
->assertSee('Was kostet wie viel')
->assertDontSee('Prüfkontingent') // Phase 2 — vorerst ausgeblendet
// Paket-Kauf läuft über das Bestätigungs-Modal.
->call('selectBooking', 'credit_pack', 'p25')
->assertSee(route('me.checkout.credit-topup', ['pack' => 'p25']), false);
});
test('the extra pm card is hidden without credits and without a subscription', function () {
/** @var TestCase $this */
$this->actingAs(bookingsTestCustomer());
LivewireVolt::test('customer.bookings')
->assertDontSee('Aus Guthaben buchen') // Extra-PM-Karte ausgeblendet
->assertSee('Was kostet wie viel'); // Preis ergibt sich aus der Preisliste
});
test('the extra pm card appears once the wallet has credits', function () {
/** @var TestCase $this */
$user = bookingsTestCustomer();
app(CreditWalletService::class)->credit($user, 20);
$this->actingAs($user);
LivewireVolt::test('customer.bookings')
->assertSee('Aus Guthaben buchen');
});
test('a missing billing address shows the proactive banner with a profile link', function () {
/** @var TestCase $this */
$this->actingAs(bookingsTestCustomer());
LivewireVolt::test('customer.bookings')
->assertSee('Rechnungsadresse fehlt')
->assertSee(route('me.profile'), false);
});
test('with a billing address the banner is gone and the booking modal offers the stripe link', function () {
/** @var TestCase $this */
$user = bookingsTestCustomer();
BillingAddress::factory()->create(['user_id' => $user->id]);
$this->actingAs($user);
LivewireVolt::test('customer.bookings')
->assertDontSee('Rechnungsadresse fehlt')
->call('selectBooking', 'single_pm')
->assertSee('Kostenpflichtig buchen')
->assertSee(route('me.checkout.single-pm'), false);
});
test('buying an extra pm from the wallet succeeds with enough credits', function () {