From b91bad7e6fb727698008a4d37bac6d04de555e00 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 10:41:53 -0300 Subject: [PATCH] refactor: PostHog review polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New BillingEvent enum replaces 'subscription.{created,updated,cancelled}' strings across StripeEventListener, TrackBilling and tests. - SendEvent now takes (method, payload) directly instead of an array of single-call shapes — overhead with no batching benefit. - PostHogService consolidates the 3 api-key short-circuits into shouldSend(). - SyncUser eager-loads currentWorkspace.withCount('socialAccounts') and drops the redundant posts_count from the workspace group identify. - Frontend Usage interface centralised in resources/js/types — was duplicated in posthog.ts and useFeatureAccess.ts. - posthog.init moved out of module-import side-effect into initializePostHog() called explicitly from app.ts. - SyncUserTest cleans up the convoluted assertion that merged $job->calls with Queue::pushed(). - Drop tests/Feature/StripeEventListenerTest.php (orphan, fully covered by tests/Feature/Listeners/StripeEventListenerTest.php). - Revert .github/FUNDING.yml to match origin/main. --- .github/FUNDING.yml | 2 +- app/Enums/PostHog/BillingEvent.php | 12 ++++ app/Jobs/PostHog/SendEvent.php | 28 ++++---- app/Jobs/PostHog/SyncUser.php | 8 ++- app/Jobs/PostHog/TrackBilling.php | 5 +- app/Listeners/StripeEventListener.php | 19 +++-- app/Services/PostHogService.php | 15 ++-- resources/js/app.ts | 3 +- resources/js/composables/useFeatureAccess.ts | 11 +-- resources/js/posthog.ts | 19 ++--- resources/js/types/index.d.ts | 9 +++ tests/Feature/Jobs/PostHog/SendEventTest.php | 10 +-- tests/Feature/Jobs/PostHog/SyncUserTest.php | 47 +++++-------- .../Feature/Jobs/PostHog/TrackBillingTest.php | 31 ++++---- .../Listeners/StripeEventListenerTest.php | 13 ++-- tests/Feature/StripeEventListenerTest.php | 70 ------------------- tests/Unit/PostHogServiceTest.php | 20 +++--- 17 files changed, 124 insertions(+), 198 deletions(-) create mode 100644 app/Enums/PostHog/BillingEvent.php delete mode 100644 tests/Feature/StripeEventListenerTest.php diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 06984c35..451f220d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [paulocastellano] +github: [trypostit] diff --git a/app/Enums/PostHog/BillingEvent.php b/app/Enums/PostHog/BillingEvent.php new file mode 100644 index 00000000..85c71407 --- /dev/null +++ b/app/Enums/PostHog/BillingEvent.php @@ -0,0 +1,12 @@ +}> $calls + * @param array $payload */ public function __construct( - public array $calls, + public string $method, + public array $payload, ) { $this->onQueue('posthog'); } @@ -45,14 +45,12 @@ public function handle(): void return; } - foreach ($this->calls as $call) { - match (data_get($call, 'method')) { - 'capture' => PostHog::capture(data_get($call, 'payload')), - 'identify' => PostHog::identify(data_get($call, 'payload')), - 'groupIdentify' => PostHog::groupIdentify(data_get($call, 'payload')), - default => Log::warning('PostHog SendEvent: unknown method', ['method' => data_get($call, 'method')]), - }; - } + match ($this->method) { + 'capture' => PostHog::capture($this->payload), + 'identify' => PostHog::identify($this->payload), + 'groupIdentify' => PostHog::groupIdentify($this->payload), + default => Log::warning('PostHog SendEvent: unknown method', ['method' => $this->method]), + }; PostHog::flush(); } diff --git a/app/Jobs/PostHog/SyncUser.php b/app/Jobs/PostHog/SyncUser.php index 34ae719a..7d3290b8 100644 --- a/app/Jobs/PostHog/SyncUser.php +++ b/app/Jobs/PostHog/SyncUser.php @@ -43,7 +43,10 @@ public function __construct(public string $userId) public function handle(PostHogService $postHog): void { - $user = User::with(['account.plan', 'currentWorkspace'])->find($this->userId); + $user = User::with([ + 'account.plan', + 'currentWorkspace' => fn ($query) => $query->withCount('socialAccounts'), + ])->find($this->userId); if (! $user) { return; @@ -78,8 +81,7 @@ public function handle(PostHogService $postHog): void $postHog->groupIdentify('workspace', (string) $workspace->id, [ 'name' => $workspace->name, 'account_id' => (string) $workspace->account_id, - 'social_accounts_count' => $workspace->socialAccounts()->count(), - 'posts_count' => $workspace->posts()->count(), + 'social_accounts_count' => (int) $workspace->social_accounts_count, 'created_at' => $workspace->created_at?->toIso8601String(), ]); } diff --git a/app/Jobs/PostHog/TrackBilling.php b/app/Jobs/PostHog/TrackBilling.php index aa4f9ce2..799539ed 100644 --- a/app/Jobs/PostHog/TrackBilling.php +++ b/app/Jobs/PostHog/TrackBilling.php @@ -4,6 +4,7 @@ namespace App\Jobs\PostHog; +use App\Enums\PostHog\BillingEvent; use App\Models\Account; use App\Services\PostHogService; use Illuminate\Bus\Queueable; @@ -33,7 +34,7 @@ class TrackBilling implements ShouldQueue */ public function __construct( public string $accountId, - public string $event, + public BillingEvent $event, public array $payload, public ?string $previousPlan = null, ) { @@ -50,7 +51,7 @@ public function handle(PostHogService $postHog): void $postHog->capture( (string) $account->owner_id, - $this->event, + $this->event->value, [ 'stripe_status' => data_get($this->payload, 'data.object.status'), 'plan' => $account->plan?->name, diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 6fc0f039..5937f0f4 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -4,6 +4,7 @@ namespace App\Listeners; +use App\Enums\PostHog\BillingEvent; use App\Events\SubscriptionCreated; use App\Jobs\PostHog\TrackBilling; use App\Models\Account; @@ -49,15 +50,14 @@ public function handle(WebhookReceived $event): void protected function handleSubscriptionCreated(Account $account, array $payload): void { $previousPlan = $account->plan?->name; - $plan = $this->resolvePlanFromSubscriptionItems($payload, $account); - if ($plan && $account->plan_id !== $plan->id) { + if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); } SubscriptionCreated::dispatch($account); - $this->trackPlanChange($account, 'subscription.created', $previousPlan, $payload); + $this->trackPlanChange($account, BillingEvent::Created, $previousPlan, $payload); } /** @@ -72,13 +72,12 @@ protected function handleSubscriptionCreated(Account $account, array $payload): protected function handleSubscriptionUpdated(Account $account, array $payload): void { $previousPlan = $account->plan?->name; - $plan = $this->resolvePlanFromSubscriptionItems($payload, $account); - if ($plan && $account->plan_id !== $plan->id) { + if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); } - $this->trackPlanChange($account, 'subscription.updated', $previousPlan, $payload); + $this->trackPlanChange($account, BillingEvent::Updated, $previousPlan, $payload); } /** @@ -93,11 +92,9 @@ protected function handleSubscriptionDeleted(Account $account, array $payload): { $previousPlan = $account->plan?->name; - if ($account->plan_id !== null) { - $account->update(['plan_id' => null]); - } + $account->update(['plan_id' => null]); - $this->trackPlanChange($account, 'subscription.cancelled', $previousPlan, $payload); + $this->trackPlanChange($account, BillingEvent::Cancelled, $previousPlan, $payload); } /** @@ -141,7 +138,7 @@ private function resolvePlanFromSubscriptionItems(array $payload, Account $accou * * @param array $payload */ - private function trackPlanChange(Account $account, string $event, ?string $previousPlan, array $payload): void + private function trackPlanChange(Account $account, BillingEvent $event, ?string $previousPlan, array $payload): void { TrackBilling::dispatch((string) $account->id, $event, $payload, $previousPlan); } diff --git a/app/Services/PostHogService.php b/app/Services/PostHogService.php index 23e3214b..7c46c214 100644 --- a/app/Services/PostHogService.php +++ b/app/Services/PostHogService.php @@ -19,7 +19,7 @@ class PostHogService */ public function capture(string $distinctId, string $event, array $properties = [], ?Account $account = null): void { - if (! config('services.posthog.api_key')) { + if (! $this->shouldSend()) { return; } @@ -43,7 +43,7 @@ public function capture(string $distinctId, string $event, array $properties = [ */ public function identify(string $distinctId, array $properties = []): void { - if (! config('services.posthog.api_key')) { + if (! $this->shouldSend()) { return; } @@ -58,7 +58,7 @@ public function identify(string $distinctId, array $properties = []): void */ public function groupIdentify(string $groupType, string $groupKey, array $properties = []): void { - if (! config('services.posthog.api_key')) { + if (! $this->shouldSend()) { return; } @@ -69,15 +69,18 @@ public function groupIdentify(string $groupType, string $groupKey, array $proper ]); } + private function shouldSend(): bool + { + return (bool) config('services.posthog.api_key'); + } + /** * @param array $payload */ private function dispatch(string $method, array $payload): void { try { - SendEvent::dispatch([ - ['method' => $method, 'payload' => $payload], - ]); + SendEvent::dispatch($method, $payload); } catch (\Throwable $e) { Log::warning('PostHogService: failed to dispatch event', ['method' => $method, 'error' => $e->getMessage()]); } diff --git a/resources/js/app.ts b/resources/js/app.ts index 6b54db35..c63c45ea 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -9,7 +9,7 @@ import { createApp, h } from 'vue'; import { initializeDataLayer } from './datalayer'; import dayjs from './dayjs'; -import { capturePageview, syncPostHogContext } from './posthog'; +import { capturePageview, initializePostHog, syncPostHogContext } from './posthog'; import type { Auth } from './types'; configureEcho({ @@ -48,6 +48,7 @@ createInertiaApp({ // The same hooks fire on every Inertia navigation below so the // account group counts stay reactive and workspace switches // re-attach the right workspace group. + initializePostHog(); syncPostHogContext(props.initialPage); capturePageview(); diff --git a/resources/js/composables/useFeatureAccess.ts b/resources/js/composables/useFeatureAccess.ts index 20b00ffb..b87ef7d9 100644 --- a/resources/js/composables/useFeatureAccess.ts +++ b/resources/js/composables/useFeatureAccess.ts @@ -1,21 +1,14 @@ import { usePage } from '@inertiajs/vue3'; import { computed } from 'vue'; +import type { Usage } from '@/types'; + interface Plan { id: string; slug: string; name: string; } -interface Usage { - workspaceCount: number; - socialAccountCount: number; - memberCount: number; - pendingInviteCount: number; - postCount: number; - creditsUsed: number; -} - interface Features { workspaceLimit: number; socialAccountLimit: number; diff --git a/resources/js/posthog.ts b/resources/js/posthog.ts index 11e61d13..11f818a8 100644 --- a/resources/js/posthog.ts +++ b/resources/js/posthog.ts @@ -1,12 +1,16 @@ import type { Page } from '@inertiajs/core'; import posthog from 'posthog-js'; -import type { Auth } from './types'; +import type { Auth, Usage } from './types'; const apiKey = import.meta.env.VITE_POSTHOG_API_KEY as string | undefined; const host = import.meta.env.VITE_POSTHOG_HOST as string | undefined; -if (apiKey) { +export const initializePostHog = (): void => { + if (!apiKey) { + return; + } + posthog.init(apiKey, { api_host: host || 'https://us.i.posthog.com', ui_host: 'https://us.posthog.com', @@ -19,16 +23,7 @@ if (apiKey) { maskTextSelector: '.ph-no-capture', }, }); -} - -interface Usage { - workspaceCount: number; - socialAccountCount: number; - memberCount: number; - pendingInviteCount: number; - postCount: number; - creditsUsed: number; -} +}; /** * Identify the current person and refresh the dual group context (account + diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 6892b075..483026bc 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -35,6 +35,15 @@ export interface Auth { currentPriceId: string | null; } +export interface Usage { + workspaceCount: number; + socialAccountCount: number; + memberCount: number; + pendingInviteCount: number; + postCount: number; + creditsUsed: number; +} + export interface FlashData { banner?: string; bannerStyle?: 'success' | 'danger' | 'info' | 'warning'; diff --git a/tests/Feature/Jobs/PostHog/SendEventTest.php b/tests/Feature/Jobs/PostHog/SendEventTest.php index 0c93aaa9..ef41f3e8 100644 --- a/tests/Feature/Jobs/PostHog/SendEventTest.php +++ b/tests/Feature/Jobs/PostHog/SendEventTest.php @@ -5,9 +5,7 @@ use App\Jobs\PostHog\SendEvent; test('job is queued on posthog queue', function () { - $job = new SendEvent([ - ['method' => 'capture', 'payload' => ['distinctId' => 'user-1', 'event' => 'test']], - ]); + $job = new SendEvent('capture', ['distinctId' => 'user-1', 'event' => 'test']); expect($job->queue)->toBe('posthog'); }); @@ -15,9 +13,7 @@ test('job skips execution when api key is missing', function () { config(['services.posthog.api_key' => null]); - $job = new SendEvent([ - ['method' => 'capture', 'payload' => ['distinctId' => 'user-1', 'event' => 'test']], - ]); + $job = new SendEvent('capture', ['distinctId' => 'user-1', 'event' => 'test']); // Should not throw - silently skips $job->handle(); @@ -26,7 +22,7 @@ }); test('job has correct retry and timeout settings', function () { - $job = new SendEvent([]); + $job = new SendEvent('capture', []); expect($job->tries)->toBe(3); expect($job->timeout)->toBe(15); diff --git a/tests/Feature/Jobs/PostHog/SyncUserTest.php b/tests/Feature/Jobs/PostHog/SyncUserTest.php index c4c2620e..74d9608f 100644 --- a/tests/Feature/Jobs/PostHog/SyncUserTest.php +++ b/tests/Feature/Jobs/PostHog/SyncUserTest.php @@ -47,13 +47,11 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - $call = $job->calls[0]; - - return $call['method'] === 'identify' - && $call['payload']['distinctId'] === (string) $this->user->id - && $call['payload']['properties']['email'] === $this->user->email - && $call['payload']['properties']['name'] === $this->user->name - && isset($call['payload']['properties']['$set_once']['signed_up_at']); + return $job->method === 'identify' + && $job->payload['distinctId'] === (string) $this->user->id + && $job->payload['properties']['email'] === $this->user->email + && $job->payload['properties']['name'] === $this->user->name + && isset($job->payload['properties']['$set_once']['signed_up_at']); }); }); @@ -73,17 +71,13 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - $accountCall = collect($job->calls) - ->merge(collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls)) - ->first(fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'account'); - - if (! $accountCall) { + if ($job->method !== 'groupIdentify' || $job->payload['groupType'] !== 'account') { return false; } - $props = $accountCall['payload']['properties']; + $props = $job->payload['properties']; - return $accountCall['payload']['groupKey'] === (string) $this->account->id + return $job->payload['groupKey'] === (string) $this->account->id && $props['workspaces_count'] === 1 && $props['social_accounts_count'] === 2 && $props['posts_count'] === 3 @@ -106,15 +100,13 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); - $allCalls = collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls); - $workspaceCall = $allCalls->first( - fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'workspace', - ); - - expect($workspaceCall)->not->toBeNull(); - expect($workspaceCall['payload']['groupKey'])->toBe((string) $workspace->id); - expect($workspaceCall['payload']['properties']['account_id'])->toBe((string) $this->account->id); - expect($workspaceCall['payload']['properties']['social_accounts_count'])->toBe(1); + Queue::assertPushed(SendEvent::class, function ($job) use ($workspace) { + return $job->method === 'groupIdentify' + && $job->payload['groupType'] === 'workspace' + && $job->payload['groupKey'] === (string) $workspace->id + && $job->payload['properties']['account_id'] === (string) $this->account->id + && $job->payload['properties']['social_accounts_count'] === 1; + }); }); test('handle skips workspace group identify when user has no current workspace', function () { @@ -124,12 +116,9 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); - $allCalls = collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls); - $workspaceCall = $allCalls->first( - fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'workspace', - ); - - expect($workspaceCall)->toBeNull(); + Queue::assertNotPushed(SendEvent::class, function ($job) { + return $job->method === 'groupIdentify' && $job->payload['groupType'] === 'workspace'; + }); }); test('job is queued on the posthog connection queue', function () { diff --git a/tests/Feature/Jobs/PostHog/TrackBillingTest.php b/tests/Feature/Jobs/PostHog/TrackBillingTest.php index 974ad67b..faee87ce 100644 --- a/tests/Feature/Jobs/PostHog/TrackBillingTest.php +++ b/tests/Feature/Jobs/PostHog/TrackBillingTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\PostHog\BillingEvent; use App\Jobs\PostHog\SendEvent; use App\Jobs\PostHog\SyncUser; use App\Jobs\PostHog\TrackBilling; @@ -28,7 +29,7 @@ }); test('job is queued on the posthog queue', function () { - $job = new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload); + $job = new TrackBilling((string) $this->account->id, BillingEvent::Updated, $this->payload); expect($job->queue)->toBe('posthog'); }); @@ -36,36 +37,34 @@ test('handle captures event on the owner profile with account group attached', function () { Queue::fake(); - (new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Updated, $this->payload)) ->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - $call = $job->calls[0]; - - return $call['method'] === 'capture' - && $call['payload']['event'] === 'subscription.updated' - && $call['payload']['distinctId'] === (string) $this->user->id - && $call['payload']['properties']['$groups']['account'] === (string) $this->account->id - && $call['payload']['properties']['stripe_status'] === 'active' - && array_key_exists('previous_plan', $call['payload']['properties']); + return $job->method === 'capture' + && $job->payload['event'] === BillingEvent::Updated->value + && $job->payload['distinctId'] === (string) $this->user->id + && $job->payload['properties']['$groups']['account'] === (string) $this->account->id + && $job->payload['properties']['stripe_status'] === 'active' + && array_key_exists('previous_plan', $job->payload['properties']); }); }); test('handle forwards previousPlan as a property when supplied', function () { Queue::fake(); - (new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload, 'Starter')) + (new TrackBilling((string) $this->account->id, BillingEvent::Updated, $this->payload, 'Starter')) ->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['payload']['properties']['previous_plan'] === 'Starter'; + return $job->payload['properties']['previous_plan'] === 'Starter'; }); }); test('handle dispatches SyncUser for the account owner', function () { Bus::fake([SyncUser::class]); - (new TrackBilling((string) $this->account->id, 'subscription.cancelled', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Cancelled, $this->payload)) ->handle(app(PostHogService::class)); Bus::assertDispatched( @@ -78,7 +77,7 @@ Queue::fake(); Bus::fake([SyncUser::class]); - (new TrackBilling('00000000-0000-0000-0000-000000000000', 'subscription.created', $this->payload)) + (new TrackBilling('00000000-0000-0000-0000-000000000000', BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); Queue::assertNothingPushed(); @@ -90,7 +89,7 @@ Queue::fake(); Bus::fake([SyncUser::class]); - (new TrackBilling((string) $this->account->id, 'subscription.created', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); Queue::assertNothingPushed(); @@ -101,7 +100,7 @@ config(['services.posthog.api_key' => null]); Queue::fake(); - (new TrackBilling((string) $this->account->id, 'subscription.created', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); // SyncUser can still be queued but it would itself no-op when handled. diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 67c61705..aab99c10 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\PostHog\BillingEvent; use App\Events\SubscriptionCreated; use App\Jobs\PostHog\TrackBilling; use App\Listeners\StripeEventListener; @@ -160,7 +161,7 @@ Bus::assertDispatched( TrackBilling::class, fn ($job) => $job->accountId === (string) $this->account->id - && $job->event === 'subscription.created', + && $job->event === BillingEvent::Created, ); }); @@ -174,7 +175,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.updated', + fn ($job) => $job->event === BillingEvent::Updated, ); }); @@ -188,7 +189,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.cancelled', + fn ($job) => $job->event === BillingEvent::Cancelled, ); }); @@ -354,7 +355,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.updated' && $job->previousPlan === $starter->name, + fn ($job) => $job->event === BillingEvent::Updated && $job->previousPlan === $starter->name, ); }); @@ -371,7 +372,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.cancelled' && $job->previousPlan === $starter->name, + fn ($job) => $job->event === BillingEvent::Cancelled && $job->previousPlan === $starter->name, ); }); @@ -390,6 +391,6 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.created' && $job->previousPlan === null, + fn ($job) => $job->event === BillingEvent::Created && $job->previousPlan === null, ); }); diff --git a/tests/Feature/StripeEventListenerTest.php b/tests/Feature/StripeEventListenerTest.php deleted file mode 100644 index d61170be..00000000 --- a/tests/Feature/StripeEventListenerTest.php +++ /dev/null @@ -1,70 +0,0 @@ - true]); - - $this->account = Account::factory()->create([ - 'stripe_id' => 'cus_test_'.fake()->uuid(), - ]); - $this->user = User::factory()->create([ - 'account_id' => $this->account->id, - ]); - $this->account->update(['owner_id' => $this->user->id]); - $this->workspace = Workspace::factory()->create([ - 'user_id' => $this->user->id, - 'account_id' => $this->account->id, - ]); - $this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]); - $this->user->update(['current_workspace_id' => $this->workspace->id]); -}); - -test('webhook dispatches subscription created event', function () { - Event::fake([SubscriptionCreated::class]); - - $payload = [ - 'type' => 'customer.subscription.created', - 'data' => [ - 'object' => [ - 'customer' => $this->account->stripe_id, - 'id' => 'sub_test_'.fake()->uuid(), - 'status' => 'active', - ], - ], - ]; - - $listener = new StripeEventListener; - $listener->handle(new WebhookReceived($payload)); - - Event::assertDispatched(SubscriptionCreated::class); -}); - -test('webhook ignores unknown stripe customer', function () { - Event::fake([SubscriptionCreated::class]); - - $payload = [ - 'type' => 'customer.subscription.created', - 'data' => [ - 'object' => [ - 'customer' => 'cus_unknown_'.fake()->uuid(), - 'id' => 'sub_test_'.fake()->uuid(), - 'status' => 'active', - ], - ], - ]; - - $listener = new StripeEventListener; - $listener->handle(new WebhookReceived($payload)); - - Event::assertNotDispatched(SubscriptionCreated::class); -}); diff --git a/tests/Unit/PostHogServiceTest.php b/tests/Unit/PostHogServiceTest.php index 9bf0425a..4db925df 100644 --- a/tests/Unit/PostHogServiceTest.php +++ b/tests/Unit/PostHogServiceTest.php @@ -19,9 +19,9 @@ $service->capture('user-123', 'test_event', ['foo' => 'bar']); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['method'] === 'capture' - && $job->calls[0]['payload']['event'] === 'test_event' - && $job->calls[0]['payload']['distinctId'] === 'user-123'; + return $job->method === 'capture' + && $job->payload['event'] === 'test_event' + && $job->payload['distinctId'] === 'user-123'; }); }); @@ -43,8 +43,8 @@ $service->identify('user-123', ['$email' => 'test@example.com']); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['method'] === 'identify' - && $job->calls[0]['payload']['distinctId'] === 'user-123'; + return $job->method === 'identify' + && $job->payload['distinctId'] === 'user-123'; }); }); @@ -66,9 +66,9 @@ $service->groupIdentify('workspace', 'ws-123', ['name' => 'Test Workspace']); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['method'] === 'groupIdentify' - && $job->calls[0]['payload']['groupType'] === 'workspace' - && $job->calls[0]['payload']['groupKey'] === 'ws-123'; + return $job->method === 'groupIdentify' + && $job->payload['groupType'] === 'workspace' + && $job->payload['groupKey'] === 'ws-123'; }); }); @@ -93,7 +93,7 @@ $service->capture('user-123', 'subscription.created', ['stripe_status' => 'active'], $account); Queue::assertPushed(SendEvent::class, function ($job) use ($account, $plan) { - $payload = $job->calls[0]['payload']; + $payload = $job->payload; return $payload['event'] === 'subscription.created' && $payload['properties']['$groups']['account'] === (string) $account->id @@ -111,7 +111,7 @@ $service->capture('user-123', 'page.viewed', ['url' => '/dashboard']); Queue::assertPushed(SendEvent::class, function ($job) { - $properties = $job->calls[0]['payload']['properties']; + $properties = $job->payload['properties']; return ! array_key_exists('$groups', $properties) && ! array_key_exists('account_id', $properties)