diff --git a/app/Livewire/MobilePricing.php b/app/Livewire/MobilePricing.php index 6fcf7e66..ad5f379b 100644 --- a/app/Livewire/MobilePricing.php +++ b/app/Livewire/MobilePricing.php @@ -71,6 +71,18 @@ public function createCheckoutSession(?string $plan, ?User $user = null) return; } + if ($this->hasActivePaidSubscription($user)) { + Log::warning('Blocked checkout session for a user who already has an active paid subscription.', [ + 'user_id' => $user->id, + ]); + + $message = 'This account already has an active subscription. Manage it from your dashboard.'; + + return Auth::id() === $user->id + ? redirect(route('dashboard'))->with('message', $message) + : redirect(route('login'))->with('message', $message); + } + // Cancel any comped subscription before creating a new paid one if ($user->hasCompedSubscription()) { $user->subscription('default')->cancelNow(); @@ -220,6 +232,16 @@ public function upgradeSubscription(): mixed return redirect(route('dashboard'))->with('success', 'Your subscription has been upgraded to Ultra!'); } + /** + * Comped subscriptions are replaced at checkout, so only paid ones block a new checkout. + */ + private function hasActivePaidSubscription(User $user): bool + { + $subscription = $user->subscription('default'); + + return $subscription && $subscription->active() && ! $user->hasCompedSubscription(); + } + private function findOrCreateUser(string $email): User { Validator::validate(['email' => $email], [ diff --git a/tests/Feature/MobilePricingTest.php b/tests/Feature/MobilePricingTest.php index 5f27d9af..5964c9be 100644 --- a/tests/Feature/MobilePricingTest.php +++ b/tests/Feature/MobilePricingTest.php @@ -629,4 +629,62 @@ public function preview_upgrade_sets_preview_to_null_when_stripe_has_no_upcoming ->assertSet('upgradePreview', null) ->assertOk(); } + + #[Test] + public function ultra_subscriber_cannot_start_a_new_checkout_session() + { + $user = $this->createSubscribedUser(self::MAX_PRICE_ID); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->call('createCheckoutSession', 'max') + ->assertRedirect(route('dashboard')); + + $this->assertSame(1, $user->subscriptions()->count()); + } + + #[Test] + public function guest_cannot_start_a_checkout_session_for_an_email_with_an_active_subscription() + { + $user = $this->createSubscribedUser(self::MAX_PRICE_ID); + + Livewire::test(MobilePricing::class) + ->call('handlePurchaseRequest', [ + 'email' => $user->email, + 'plan' => 'max', + ]) + ->assertRedirect(route('login')); + + $this->assertSame(1, $user->subscriptions()->count()); + } + + #[Test] + public function pro_subscriber_cannot_bypass_the_upgrade_flow_with_a_new_checkout_session() + { + $user = $this->createSubscribedUser(self::PRO_PRICE_ID); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->call('createCheckoutSession', 'max') + ->assertRedirect(route('dashboard')); + } + + private function createSubscribedUser(string $priceId): User + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => $priceId, + 'is_comped' => false, + ]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => $priceId]); + + return $user; + } }