refactor: PostHog review polish
- 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.
This commit is contained in:
parent
1df59f09b4
commit
b91bad7e6f
17 changed files with 124 additions and 198 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1 +1 @@
|
|||
github: [paulocastellano]
|
||||
github: [trypostit]
|
||||
|
|
|
|||
12
app/Enums/PostHog/BillingEvent.php
Normal file
12
app/Enums/PostHog/BillingEvent.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\PostHog;
|
||||
|
||||
enum BillingEvent: string
|
||||
{
|
||||
case Created = 'subscription.created';
|
||||
case Updated = 'subscription.updated';
|
||||
case Cancelled = 'subscription.cancelled';
|
||||
}
|
||||
|
|
@ -13,11 +13,10 @@
|
|||
use PostHog\PostHog;
|
||||
|
||||
/**
|
||||
* Low-level PostHog dispatch worker. Receives an array of pre-built API
|
||||
* calls (`capture`, `identify`, `groupIdentify`) and forwards them to the
|
||||
* PostHog SDK. Higher-level jobs in this namespace (`SyncUser`,
|
||||
* `TrackBilling`) build the payloads and queue this job to do the network
|
||||
* work.
|
||||
* Low-level PostHog dispatch worker. Forwards a single API call (`capture`,
|
||||
* `identify`, `groupIdentify`) to the PostHog SDK. Higher-level jobs in this
|
||||
* namespace (`SyncUser`, `TrackBilling`) build the payloads and queue this
|
||||
* job to do the network work.
|
||||
*
|
||||
* No-op when `POSTHOG_API_KEY` is unset so self-hosted installs are
|
||||
* unaffected.
|
||||
|
|
@ -31,10 +30,11 @@ class SendEvent implements ShouldQueue
|
|||
public int $timeout = 15;
|
||||
|
||||
/**
|
||||
* @param array<array{method: string, payload: array<string, mixed>}> $calls
|
||||
* @param array<string, mixed> $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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, mixed> $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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, mixed> $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()]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 +
|
||||
|
|
|
|||
9
resources/js/types/index.d.ts
vendored
9
resources/js/types/index.d.ts
vendored
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Events\SubscriptionCreated;
|
||||
use App\Listeners\StripeEventListener;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Cashier\Events\WebhookReceived;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['trypost.self_hosted' => 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);
|
||||
});
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue