Phase 9F: Tarif-Seite mit Stripe-Checkout und Billing Portal
- Buchungs-Seite zeigt das echte 4-Tier-Raster aus plans (Monat/Jahr-
Toggle, Jahrespreis als "2 Monate gratis") mit Checkout-Buttons,
Einzel-PM als separaten No-Abo-Block und Enterprise-Hinweis;
Credit-Konzept-Mock entfernt (Credits folgen mit 9I bzw. Phase 2)
- Aktueller-Tarif-Panel real: Abo (Preis, Kontingent, Kündigungsstatus),
Bestandstarif (unbegrenzt, nächste MAN-Rechnung), offene Einzelkäufe;
Kontingent-Kachel zeigt "Unbegrenzt" bei Bestandsschutz
- "Abo verwalten" über das Stripe Billing Portal
(me.checkout.billing-portal; Zahlungsmethode, Rechnungen, Kündigung)
- Aktive Buchungen + Verlauf aus echten Daten (Abo, Legacy-Vereinbarung,
offene/eingelöste Einzelkäufe mit PM-Verknüpfung)
- Tests: BookingsPageTest (9 Tests), PanelConsolidationTest angepasst;
Suite 519 passed / 4 skipped
- Doku: PHASE-9-Plan 9F ✅, Billing-Doku (Routen, Stripe Tax aktiviert),
STATUS-ABGLEICH, Checkliste, PROGRESS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c8dc99c3c8
commit
23ac8bc7f1
11 changed files with 581 additions and 316 deletions
156
tests/Feature/Billing/BookingsPageTest.php
Normal file
156
tests/Feature/Billing/BookingsPageTest.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\UserPaymentOptionStatus;
|
||||
use App\Models\Plan;
|
||||
use App\Models\SinglePurchase;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPaymentOption;
|
||||
use App\Services\Billing\StripeCheckoutService;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
use Tests\TestCase;
|
||||
|
||||
beforeEach(function (): void {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
});
|
||||
|
||||
function bookingsTestCustomer(): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('customer');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
test('the bookings page renders the active plans with checkout links', function () {
|
||||
/** @var TestCase $this */
|
||||
Plan::factory()->create([
|
||||
'name' => 'Business',
|
||||
'slug' => 'business',
|
||||
'monthly_price_cents' => 4900,
|
||||
'yearly_price_cents' => 49000,
|
||||
'press_release_quota' => 10,
|
||||
'daily_limit' => 2,
|
||||
]);
|
||||
Plan::factory()->inactive()->create(['name' => 'Versteckt']);
|
||||
|
||||
$this->actingAs(bookingsTestCustomer());
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->assertSee('Business')
|
||||
->assertSee('49 €')
|
||||
->assertSee('490 €')
|
||||
->assertSee('10 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');
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->assertSee('Einzel-Pressemitteilung — ohne Abo')
|
||||
->assertSee('19 €')
|
||||
->assertSee(route('me.checkout.single-pm'), false);
|
||||
});
|
||||
|
||||
test('without any booking the page shows the empty state', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->actingAs(bookingsTestCustomer());
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->assertSee('Noch kein aktiver Tarif')
|
||||
->assertSee('Noch keine aktiven Buchungen');
|
||||
});
|
||||
|
||||
test('a subscriber sees the current plan and the manage button', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = bookingsTestCustomer();
|
||||
$plan = Plan::factory()->create([
|
||||
'name' => 'Pro',
|
||||
'press_release_quota' => 25,
|
||||
'stripe_price_id_monthly' => 'price_test_m_pro',
|
||||
]);
|
||||
subscribeUserToPlan($user, $plan);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->assertSee('Ihr aktueller Tarif')
|
||||
->assertSee('Abo verwalten')
|
||||
->assertSee('Abo: Pro')
|
||||
->assertSee(route('me.checkout.billing-portal'), false);
|
||||
});
|
||||
|
||||
test('a grandfathered legacy user sees the bestandstarif with unlimited quota', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = bookingsTestCustomer();
|
||||
UserPaymentOption::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'status' => UserPaymentOptionStatus::Grandfathered->value,
|
||||
'current_period_end' => now()->addMonths(3),
|
||||
'legacy_conditions' => ['name' => 'Presseverteiler Premium'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->assertSee('Bestandstarif')
|
||||
->assertSee('Presseverteiler Premium')
|
||||
->assertSee('Unbegrenzte Pressemitteilungen (Bestandsschutz).')
|
||||
->assertSee('Unbegrenzt');
|
||||
});
|
||||
|
||||
test('open and consumed single purchases appear in bookings and history', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = bookingsTestCustomer();
|
||||
SinglePurchase::factory()->paid()->create(['user_id' => $user->id]);
|
||||
SinglePurchase::factory()->consumed()->create(['user_id' => $user->id]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
LivewireVolt::test('customer.bookings')
|
||||
->assertSee('einlösbar')
|
||||
->assertSee('eingelöst am');
|
||||
});
|
||||
|
||||
test('the billing portal redirects without an active subscription', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->actingAs(bookingsTestCustomer())
|
||||
->get(route('me.checkout.billing-portal'))
|
||||
->assertRedirect(route('me.bookings.index'))
|
||||
->assertSessionHas('checkout-notice');
|
||||
});
|
||||
|
||||
test('the billing portal forwards a subscriber to stripe', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = bookingsTestCustomer();
|
||||
$user->forceFill(['stripe_id' => 'cus_test_portal'])->save();
|
||||
$plan = Plan::factory()->create(['stripe_price_id_monthly' => 'price_test_m_portal']);
|
||||
subscribeUserToPlan($user, $plan);
|
||||
|
||||
$this->mock(StripeCheckoutService::class, function ($mock) {
|
||||
$mock->shouldReceive('billingPortalUrl')
|
||||
->once()
|
||||
->andReturn('https://billing.stripe.com/p/session/test');
|
||||
});
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('me.checkout.billing-portal'))
|
||||
->assertRedirect('https://billing.stripe.com/p/session/test');
|
||||
});
|
||||
|
||||
test('the checkout success banner is shown after returning from stripe', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->actingAs(bookingsTestCustomer())
|
||||
->get(route('me.bookings.index', ['checkout' => 'erfolg']))
|
||||
->assertOk()
|
||||
->assertSee('Vielen Dank für Ihre Buchung!');
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Tests\TestCase;
|
||||
|
|
@ -58,21 +59,27 @@ test('customer can access me dashboard but not admin dashboard', function () {
|
|||
$this->get(route('dashboard'))->assertForbidden();
|
||||
});
|
||||
|
||||
test('customer bookings page shows credit packages and add ons from pricing concept', function () {
|
||||
test('customer bookings page shows the tariff grid and single pm block', function () {
|
||||
// Seit Phase 9F zeigt die Seite das echte Tarif-Raster mit
|
||||
// Stripe-Checkout statt des Credit-Konzept-Mocks (Credits → Phase 9I/2).
|
||||
/** @var TestCase $this */
|
||||
$customer = User::factory()->create(['is_active' => true]);
|
||||
$customer->assignRole('customer');
|
||||
|
||||
Plan::factory()->create([
|
||||
'name' => 'Starter',
|
||||
'monthly_price_cents' => 2900,
|
||||
'press_release_quota' => 3,
|
||||
]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get(route('me.bookings.index'))
|
||||
->assertSuccessful()
|
||||
->assertSee('Credit-Pakete')
|
||||
->assertSee('Standard')
|
||||
->assertSee('50')
|
||||
->assertSee('45 €')
|
||||
->assertSee('Pressetext-Optimierung')
|
||||
->assertSee('Top-Slot Startseite')
|
||||
->assertSee('Score 80+')
|
||||
->assertSee('Den passenden Tarif wählen')
|
||||
->assertSee('Starter')
|
||||
->assertSee('29 €')
|
||||
->assertSee('2 Monate gratis')
|
||||
->assertSee('Einzel-Pressemitteilung — ohne Abo')
|
||||
->assertSee('Noch keine aktiven Buchungen');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue