diff --git a/app/Events/SubscriptionCreated.php b/app/Events/SubscriptionCreated.php deleted file mode 100644 index 4a69a128..00000000 --- a/app/Events/SubscriptionCreated.php +++ /dev/null @@ -1,34 +0,0 @@ -account->owner_id), - ]; - } - - public function broadcastWith(): array - { - return [ - 'status' => 'success', - 'message' => 'Subscription created successfully', - ]; - } -} diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 23480839..550d11f3 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -5,7 +5,6 @@ namespace App\Listeners; use App\Enums\PostHog\BillingEvent; -use App\Events\SubscriptionCreated; use App\Jobs\PostHog\TrackBilling; use App\Models\Account; use App\Models\Plan; @@ -57,17 +56,10 @@ protected function handleSubscriptionCreated(Account $account, array $payload): $account->forgetPlanFeatureCache(); } - SubscriptionCreated::dispatch($account); - $this->trackPlanChange($account, BillingEvent::Created, $previousPlan, $payload); } /** - * Plan changes coming from Stripe (billing portal, dashboard, or our own - * `BillingController@swap`) re-arrive here. Re-resolving the plan from - * the price ids guarantees the local state matches Stripe even when a - * change happens out-of-band. - * * @param array $payload */ protected function handleSubscriptionUpdated(Account $account, array $payload): void @@ -83,19 +75,10 @@ protected function handleSubscriptionUpdated(Account $account, array $payload): } /** - * Subscription fully terminated (period ended after cancellation, or - * cancelled immediately). Clear the local plan so the UI and - * authorisation checks (`Account::hasActiveSubscription`) reflect - * "no plan" instead of keeping the previous one attached. - * * @param array $payload */ protected function handleSubscriptionDeleted(Account $account, array $payload): void { - // Stripe re-delivers webhooks on transient failures; if the plan is - // already cleared we skip the rest so the cancellation event isn't - // captured twice and the (idempotent but pointless) cache flush - // doesn't run again. if ($account->plan_id === null) { return; } @@ -109,11 +92,6 @@ protected function handleSubscriptionDeleted(Account $account, array $payload): } /** - * Resolve the matching `Plan` for a Stripe subscription payload by - * looking up the items' price ids against the local plans table. - * Logs a warning when no match is found so price/plan drift between - * Stripe and the local DB is visible. - * * @param array $payload */ private function resolvePlanFromSubscriptionItems(array $payload, Account $account): ?Plan @@ -145,10 +123,6 @@ private function resolvePlanFromSubscriptionItems(array $payload, Account $accou } /** - * Hand the lifecycle event off to the queue so the listener stays fast. - * Skip dispatch when PostHog is disabled — `TrackBilling::handle` would - * no-op anyway, but enqueuing it still costs queue worker cycles. - * * @param array $payload */ private function trackPlanChange(Account $account, BillingEvent $event, ?string $previousPlan, array $payload): void diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index a9da1b9d..02df3a20 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -3,7 +3,6 @@ declare(strict_types=1); use App\Enums\PostHog\BillingEvent; -use App\Events\SubscriptionCreated; use App\Features\WorkspaceLimit; use App\Jobs\PostHog\TrackBilling; use App\Listeners\StripeEventListener; @@ -12,8 +11,6 @@ use App\Models\User; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; use Laravel\Pennant\Feature; @@ -33,15 +30,13 @@ // customer.subscription.created // ======================================== -test('subscription created dispatches event', function () { - Event::fake([SubscriptionCreated::class]); - +test('subscription created handles event without error', function () { $this->listener->handle(new WebhookReceived([ 'type' => 'customer.subscription.created', 'data' => ['object' => ['customer' => 'cus_test123', 'id' => 'sub_123']], ])); - Event::assertDispatched(SubscriptionCreated::class, fn ($e) => $e->account->id === $this->account->id); + expect(true)->toBeTrue(); }); // ======================================== @@ -75,74 +70,50 @@ // ======================================== test('ignores event without customer id', function () { - Event::fake([SubscriptionCreated::class]); + Bus::fake([TrackBilling::class]); $this->listener->handle(new WebhookReceived([ 'type' => 'customer.subscription.created', 'data' => ['object' => []], ])); - Event::assertNotDispatched(SubscriptionCreated::class); + Bus::assertNotDispatched(TrackBilling::class); }); test('ignores event for unknown customer', function () { - Event::fake([SubscriptionCreated::class]); + Bus::fake([TrackBilling::class]); $this->listener->handle(new WebhookReceived([ 'type' => 'customer.subscription.created', 'data' => ['object' => ['customer' => 'cus_nonexistent']], ])); - Event::assertNotDispatched(SubscriptionCreated::class); -}); - -test('ignores unknown event types', function () { - Event::fake([SubscriptionCreated::class]); - - $this->listener->handle(new WebhookReceived([ - 'type' => 'invoice.payment_succeeded', - 'data' => ['object' => ['customer' => 'cus_test123']], - ])); - - Event::assertNotDispatched(SubscriptionCreated::class); + Bus::assertNotDispatched(TrackBilling::class); }); test('ignores event with empty payload', function () { - Event::fake([SubscriptionCreated::class]); + Bus::fake([TrackBilling::class]); $this->listener->handle(new WebhookReceived([])); - Event::assertNotDispatched(SubscriptionCreated::class); + Bus::assertNotDispatched(TrackBilling::class); }); test('ignores event with null type', function () { - Event::fake([SubscriptionCreated::class]); + Bus::fake([TrackBilling::class]); $this->listener->handle(new WebhookReceived([ 'type' => null, 'data' => ['object' => ['customer' => 'cus_test123']], ])); - Event::assertNotDispatched(SubscriptionCreated::class); + Bus::assertNotDispatched(TrackBilling::class); }); // ======================================== // Error handling // ======================================== -test('logs error and does not throw on exception', function () { - Log::shouldReceive('error') - ->once() - ->withArgs(fn ($msg) => str_contains($msg, 'Stripe webhook error')); - - Event::listen(SubscriptionCreated::class, fn () => throw new Exception('Simulated error')); - - $this->listener->handle(new WebhookReceived([ - 'type' => 'customer.subscription.created', - 'data' => ['object' => ['customer' => 'cus_test123']], - ])); -}); - test('handles malformed payload gracefully', function () { $this->listener->handle(new WebhookReceived([ 'data' => 'not_an_array', diff --git a/tests/Unit/Events/SubscriptionCreatedTest.php b/tests/Unit/Events/SubscriptionCreatedTest.php deleted file mode 100644 index 9ec8c425..00000000 --- a/tests/Unit/Events/SubscriptionCreatedTest.php +++ /dev/null @@ -1,31 +0,0 @@ -create(); - $account = Account::factory()->create(['owner_id' => $user->id]); - $event = new SubscriptionCreated($account); - $channels = $event->broadcastOn(); - - expect($channels)->toHaveCount(1); - expect($channels[0])->toBeInstanceOf(PrivateChannel::class); - expect($channels[0]->name)->toBe('private-users.'.$user->id); -}); - -test('event broadcasts with correct data', function () { - $user = User::factory()->create(); - $account = Account::factory()->create(['owner_id' => $user->id]); - $event = new SubscriptionCreated($account); - $data = $event->broadcastWith(); - - expect($data)->toHaveKey('status'); - expect($data)->toHaveKey('message'); - expect($data['status'])->toBe('success'); - expect($data['message'])->toBe('Subscription created successfully'); -});