trypost/app/Models/Traits/HasUsage.php

80 lines
2.5 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Models\Traits;
use App\Models\Account;
use App\Models\Invite;
use App\Models\Post;
use App\Support\BillingCycle;
use Illuminate\Support\Facades\Cache;
/**
* Provides account-level usage counts and plan-resolved feature limits.
*
* `featureLimits()` resolves the account's per-cycle credit allotment directly
* from BillingCycle, computed fresh from the plan, workspace count, and billing
* interval no caching, so there is nothing to invalidate.
*/
trait HasUsage
{
/**
* Cache TTL for the per-account post count. Posts are unbounded by plan
* limits and not used for any quota gating, so a few minutes of staleness
* is acceptable in exchange for skipping a potentially heavy aggregate
* query on every authenticated request.
*/
private const POST_COUNT_CACHE_TTL = 300;
/**
feat: end-to-end PostHog tracking with reactive group metrics Wire PostHog identify + dual-group context across the stack so events land on the right person and account/workspace groups, with counts kept fresh by Inertia navigations rather than per-domain triggers. - New `app/Jobs/SyncUserToPostHog.php` (queue `posthog`): centralised high-level sync — identifies the user and group-identifies their account + current workspace using `$account->usage()` so the metrics reuse the same source of truth Inertia ships in shared props. - `app/Actions/User/CreateUser.php`: dispatches `SyncUserToPostHog` on signup instead of calling `PostHogService` inline. Keeps the action fast and routes everything through the queue. - `app/Listeners/StripeEventListener.php`: webhook now captures `subscription.created`/`updated`/`cancelled` against the account owner profile (with `account` group auto-attached) and re-dispatches `SyncUserToPostHog` so plan/has_active_subscription/is_on_trial refresh after Stripe state changes. - `app/Services/PostHogService.php`: `capture()` accepts an optional `Account` that auto-attaches `$groups.account`, `account_id`, and `plan` properties. Each public method short-circuits when `POSTHOG_API_KEY` is unset so self-hosted installs are unaffected. - `app/Models/Traits/HasUsage.php`: adds `postCount` to the usage shape (combined `withCount(['socialAccounts','posts'])` query) so posts count is part of the same payload Inertia already ships. - `config/horizon.php`: adds `posthog` to `supervisor-1` queues so the queued PostHog jobs actually drain in production. - `resources/js/app.ts`: extracts `syncPostHogContext(page)` and calls it on boot AND on every Inertia navigation, reading the fresh `usage` props. This: - Refreshes account group counts (workspaces, social accounts, posts, members, credits) without per-domain triggers. - Resolves the workspace-switch case where `setup()` does not re-run but `navigate` fires with the new `auth.currentWorkspace`. - Captures the initial `$pageview` so the first page of a session is no longer dropped. - `resources/js/components/UserMenuContent.vue`: `posthog.reset()` on logout so a follow-up login on the same browser doesn't keep events attributed to the previous user. - `resources/js/composables/useFeatureAccess.ts`: TS `Usage` interface gains `postCount`. - `tests/Feature/Models/HasUsageTraitTest.php`: updated for the new usage shape. Full suite: 1407 passed. Hierarchy aligned with the domain model: person = User, group `account` = billing/plan parent, group `workspace` = collaboration child (carries `account_id` for drill-down).
2026-05-07 12:28:02 +00:00
* @return array{workspaceCount: int, socialAccountCount: int, memberCount: int, pendingInviteCount: int, postCount: int, creditsUsed: int}
*/
public function usage(): array
{
feat: end-to-end PostHog tracking with reactive group metrics Wire PostHog identify + dual-group context across the stack so events land on the right person and account/workspace groups, with counts kept fresh by Inertia navigations rather than per-domain triggers. - New `app/Jobs/SyncUserToPostHog.php` (queue `posthog`): centralised high-level sync — identifies the user and group-identifies their account + current workspace using `$account->usage()` so the metrics reuse the same source of truth Inertia ships in shared props. - `app/Actions/User/CreateUser.php`: dispatches `SyncUserToPostHog` on signup instead of calling `PostHogService` inline. Keeps the action fast and routes everything through the queue. - `app/Listeners/StripeEventListener.php`: webhook now captures `subscription.created`/`updated`/`cancelled` against the account owner profile (with `account` group auto-attached) and re-dispatches `SyncUserToPostHog` so plan/has_active_subscription/is_on_trial refresh after Stripe state changes. - `app/Services/PostHogService.php`: `capture()` accepts an optional `Account` that auto-attaches `$groups.account`, `account_id`, and `plan` properties. Each public method short-circuits when `POSTHOG_API_KEY` is unset so self-hosted installs are unaffected. - `app/Models/Traits/HasUsage.php`: adds `postCount` to the usage shape (combined `withCount(['socialAccounts','posts'])` query) so posts count is part of the same payload Inertia already ships. - `config/horizon.php`: adds `posthog` to `supervisor-1` queues so the queued PostHog jobs actually drain in production. - `resources/js/app.ts`: extracts `syncPostHogContext(page)` and calls it on boot AND on every Inertia navigation, reading the fresh `usage` props. This: - Refreshes account group counts (workspaces, social accounts, posts, members, credits) without per-domain triggers. - Resolves the workspace-switch case where `setup()` does not re-run but `navigate` fires with the new `auth.currentWorkspace`. - Captures the initial `$pageview` so the first page of a session is no longer dropped. - `resources/js/components/UserMenuContent.vue`: `posthog.reset()` on logout so a follow-up login on the same browser doesn't keep events attributed to the previous user. - `resources/js/composables/useFeatureAccess.ts`: TS `Usage` interface gains `postCount`. - `tests/Feature/Models/HasUsageTraitTest.php`: updated for the new usage shape. Full suite: 1407 passed. Hierarchy aligned with the domain model: person = User, group `account` = billing/plan parent, group `workspace` = collaboration child (carries `account_id` for drill-down).
2026-05-07 12:28:02 +00:00
$workspaces = $this->workspaces()
->withCount('socialAccounts')
feat: end-to-end PostHog tracking with reactive group metrics Wire PostHog identify + dual-group context across the stack so events land on the right person and account/workspace groups, with counts kept fresh by Inertia navigations rather than per-domain triggers. - New `app/Jobs/SyncUserToPostHog.php` (queue `posthog`): centralised high-level sync — identifies the user and group-identifies their account + current workspace using `$account->usage()` so the metrics reuse the same source of truth Inertia ships in shared props. - `app/Actions/User/CreateUser.php`: dispatches `SyncUserToPostHog` on signup instead of calling `PostHogService` inline. Keeps the action fast and routes everything through the queue. - `app/Listeners/StripeEventListener.php`: webhook now captures `subscription.created`/`updated`/`cancelled` against the account owner profile (with `account` group auto-attached) and re-dispatches `SyncUserToPostHog` so plan/has_active_subscription/is_on_trial refresh after Stripe state changes. - `app/Services/PostHogService.php`: `capture()` accepts an optional `Account` that auto-attaches `$groups.account`, `account_id`, and `plan` properties. Each public method short-circuits when `POSTHOG_API_KEY` is unset so self-hosted installs are unaffected. - `app/Models/Traits/HasUsage.php`: adds `postCount` to the usage shape (combined `withCount(['socialAccounts','posts'])` query) so posts count is part of the same payload Inertia already ships. - `config/horizon.php`: adds `posthog` to `supervisor-1` queues so the queued PostHog jobs actually drain in production. - `resources/js/app.ts`: extracts `syncPostHogContext(page)` and calls it on boot AND on every Inertia navigation, reading the fresh `usage` props. This: - Refreshes account group counts (workspaces, social accounts, posts, members, credits) without per-domain triggers. - Resolves the workspace-switch case where `setup()` does not re-run but `navigate` fires with the new `auth.currentWorkspace`. - Captures the initial `$pageview` so the first page of a session is no longer dropped. - `resources/js/components/UserMenuContent.vue`: `posthog.reset()` on logout so a follow-up login on the same browser doesn't keep events attributed to the previous user. - `resources/js/composables/useFeatureAccess.ts`: TS `Usage` interface gains `postCount`. - `tests/Feature/Models/HasUsageTraitTest.php`: updated for the new usage shape. Full suite: 1407 passed. Hierarchy aligned with the domain model: person = User, group `account` = billing/plan parent, group `workspace` = collaboration child (carries `account_id` for drill-down).
2026-05-07 12:28:02 +00:00
->get();
return [
feat: end-to-end PostHog tracking with reactive group metrics Wire PostHog identify + dual-group context across the stack so events land on the right person and account/workspace groups, with counts kept fresh by Inertia navigations rather than per-domain triggers. - New `app/Jobs/SyncUserToPostHog.php` (queue `posthog`): centralised high-level sync — identifies the user and group-identifies their account + current workspace using `$account->usage()` so the metrics reuse the same source of truth Inertia ships in shared props. - `app/Actions/User/CreateUser.php`: dispatches `SyncUserToPostHog` on signup instead of calling `PostHogService` inline. Keeps the action fast and routes everything through the queue. - `app/Listeners/StripeEventListener.php`: webhook now captures `subscription.created`/`updated`/`cancelled` against the account owner profile (with `account` group auto-attached) and re-dispatches `SyncUserToPostHog` so plan/has_active_subscription/is_on_trial refresh after Stripe state changes. - `app/Services/PostHogService.php`: `capture()` accepts an optional `Account` that auto-attaches `$groups.account`, `account_id`, and `plan` properties. Each public method short-circuits when `POSTHOG_API_KEY` is unset so self-hosted installs are unaffected. - `app/Models/Traits/HasUsage.php`: adds `postCount` to the usage shape (combined `withCount(['socialAccounts','posts'])` query) so posts count is part of the same payload Inertia already ships. - `config/horizon.php`: adds `posthog` to `supervisor-1` queues so the queued PostHog jobs actually drain in production. - `resources/js/app.ts`: extracts `syncPostHogContext(page)` and calls it on boot AND on every Inertia navigation, reading the fresh `usage` props. This: - Refreshes account group counts (workspaces, social accounts, posts, members, credits) without per-domain triggers. - Resolves the workspace-switch case where `setup()` does not re-run but `navigate` fires with the new `auth.currentWorkspace`. - Captures the initial `$pageview` so the first page of a session is no longer dropped. - `resources/js/components/UserMenuContent.vue`: `posthog.reset()` on logout so a follow-up login on the same browser doesn't keep events attributed to the previous user. - `resources/js/composables/useFeatureAccess.ts`: TS `Usage` interface gains `postCount`. - `tests/Feature/Models/HasUsageTraitTest.php`: updated for the new usage shape. Full suite: 1407 passed. Hierarchy aligned with the domain model: person = User, group `account` = billing/plan parent, group `workspace` = collaboration child (carries `account_id` for drill-down).
2026-05-07 12:28:02 +00:00
'workspaceCount' => $workspaces->count(),
'socialAccountCount' => (int) $workspaces->sum('social_accounts_count'),
'memberCount' => $this->users()->count(),
'pendingInviteCount' => Invite::where('account_id', $this->id)
->whereNull('accepted_at')
->count(),
'postCount' => $this->cachedPostCount($workspaces->pluck('id')->all()),
'creditsUsed' => BillingCycle::for($this)->usedCredits(),
];
}
/**
* @return array{monthlyCreditsLimit: int}
*/
public function featureLimits(): array
{
return [
'monthlyCreditsLimit' => BillingCycle::for($this)->creditAllotment(),
];
}
/**
* The `(int)` cast is load-bearing: Laravel's RedisStore stores numeric
* values raw (for atomic INCR) and returns them as strings on read.
*
* @param array<int, string> $workspaceIds
*/
private function cachedPostCount(array $workspaceIds): int
{
if (empty($workspaceIds)) {
return 0;
}
return (int) Cache::remember(
Account::postsCountCacheKey((string) $this->id),
self::POST_COUNT_CACHE_TTL,
fn () => Post::whereIn('workspace_id', $workspaceIds)->count(),
);
}
}