- 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>
266 lines
9.2 KiB
PHP
266 lines
9.2 KiB
PHP
<?php
|
|
|
|
use App\Enums\UserPaymentOptionStatus;
|
|
use App\Models\BillingAddress;
|
|
use App\Models\Plan;
|
|
use App\Models\SinglePurchase;
|
|
use App\Models\User;
|
|
use App\Models\UserPaymentOption;
|
|
use App\Services\Billing\CreditWalletService;
|
|
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']);
|
|
|
|
$user = bookingsTestCustomer();
|
|
BillingAddress::factory()->create(['user_id' => $user->id]);
|
|
$this->actingAs($user);
|
|
|
|
LivewireVolt::test('customer.bookings')
|
|
->assertSee('Business')
|
|
->assertSee('49 €')
|
|
->assertSee('490 €')
|
|
->assertSee('Pressemitteilungen pro Monat')
|
|
->assertSee('max. 2 Veröffentlichungen pro Tag')
|
|
->assertSee('2 Monate gratis')
|
|
->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');
|
|
|
|
$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);
|
|
});
|
|
|
|
test('without any booking the current tariff card is hidden', function () {
|
|
/** @var TestCase $this */
|
|
$this->actingAs(bookingsTestCustomer());
|
|
|
|
LivewireVolt::test('customer.bookings')
|
|
->assertDontSee('Aktueller Tarif')
|
|
->assertSee('Den passenden Tarif wählen')
|
|
->assertSee('Noch kein Buchungsverlauf');
|
|
});
|
|
|
|
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('Pressemitteilungen pro Monat')
|
|
->assertSee(route('me.checkout.billing-portal'), false);
|
|
});
|
|
|
|
test('a grandfathered legacy user sees the bestandsschutz package with portal and 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',
|
|
'legacy_portal' => 'presseecho',
|
|
'net_cents' => 100000,
|
|
'interval' => 'yearly',
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
LivewireVolt::test('customer.bookings')
|
|
->assertSee('Ihr Bestandsschutz')
|
|
->assertSee('Bestandsschutz')
|
|
->assertSee('Presseverteiler Premium')
|
|
// Portal wird jetzt angezeigt.
|
|
->assertSee('Presseecho')
|
|
->assertSee('Unbegrenzte');
|
|
});
|
|
|
|
test('open and consumed single purchases appear in the card 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('Einzel-Pressemitteilung verfügbar')
|
|
->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!');
|
|
});
|
|
|
|
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);
|
|
|
|
LivewireVolt::test('customer.bookings')
|
|
->assertSee('Credit-Wallet & Add-ons')
|
|
->assertSee('30')
|
|
->assertSee('27 Credits') // Bonus-Paket
|
|
->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 () {
|
|
/** @var TestCase $this */
|
|
$user = bookingsTestCustomer();
|
|
app(CreditWalletService::class)->credit($user, 25);
|
|
|
|
$this->actingAs($user);
|
|
|
|
LivewireVolt::test('customer.bookings')
|
|
->call('buyExtraPm')
|
|
->assertSee('Extra-Pressemitteilung gebucht');
|
|
|
|
expect($user->fresh()->creditBalance())->toBe(6); // 25 - 19 (Einzel-Tier)
|
|
expect($user->singlePurchases()->grantingSubmission()->count())->toBe(1);
|
|
});
|
|
|
|
test('buying an extra pm without enough credits shows the mini checkout hint', function () {
|
|
/** @var TestCase $this */
|
|
$user = bookingsTestCustomer();
|
|
app(CreditWalletService::class)->credit($user, 8);
|
|
|
|
$this->actingAs($user);
|
|
|
|
LivewireVolt::test('customer.bookings')
|
|
->call('buyExtraPm')
|
|
->assertSee('Bitte laden Sie mindestens 11 Credits nach');
|
|
|
|
expect($user->singlePurchases()->count())->toBe(0);
|
|
});
|