*/ public function summary(): array { $subscriptions = Subscription::query() ->where('type', $this->plans->subscriptionName()) ->with('items') ->get(); $activeSubscriptions = $subscriptions->filter( fn (Subscription $subscription): bool => $subscription->active() || $subscription->onGracePeriod() ); $subscriberTiers = []; $planBreakdown = []; $gracePeriodSubscribers = []; foreach ($activeSubscriptions as $subscription) { $userId = (int) $subscription->user_id; $tier = $this->tierForSubscription($subscription); if ($subscription->onGracePeriod()) { $gracePeriodSubscribers[$userId] = true; } if ($tier !== null) { $existingTier = $subscriberTiers[$userId] ?? null; if ($existingTier === null || $this->rankForTier($tier) > $this->rankForTier($existingTier)) { $subscriberTiers[$userId] = $tier; } } foreach ($this->planKeysForSubscription($subscription) as $planKey) { $planBreakdown[$planKey] = (int) ($planBreakdown[$planKey] ?? 0) + 1; } } $recentEvents = AcademyBillingEvent::query()->count(); $lastWebhookAt = AcademyBillingEvent::query()->latest('processed_at')->value('processed_at'); return [ 'enabled' => $this->plans->enabled(), 'active_subscribers' => count($subscriberTiers), 'creator_subscribers' => count(array_filter($subscriberTiers, static fn (string $tier): bool => $tier === 'creator')), 'pro_subscribers' => count(array_filter($subscriberTiers, static fn (string $tier): bool => $tier === 'pro')), 'grace_period_subscribers' => count($gracePeriodSubscribers), 'ended_subscriptions' => $subscriptions->filter( fn (Subscription $subscription): bool => ! $subscription->active() && ! $subscription->onGracePeriod() )->count(), 'configured_plan_count' => count(array_keys($this->plans->plans())), 'missing_plan_keys' => $this->plans->missingPriceIds(), 'plan_breakdown' => $this->formatPlanBreakdown($planBreakdown), 'recent_webhook_count' => $recentEvents, 'last_webhook_at' => $lastWebhookAt?->toISOString(), ]; } /** * @return array> */ public function recentEvents(int $limit = 15): array { return AcademyBillingEvent::query() ->latest('processed_at') ->latest('id') ->limit($limit) ->get() ->map(fn (AcademyBillingEvent $event): array => [ 'id' => (int) $event->id, 'event_type' => (string) $event->event_type, 'academy_tier' => $event->academy_tier ? (string) $event->academy_tier : null, 'academy_plan' => $event->academy_plan ? (string) $event->academy_plan : null, 'user_id' => $event->user_id ? (int) $event->user_id : null, 'stripe_customer_id' => $event->stripe_customer_id ? (string) $event->stripe_customer_id : null, 'stripe_subscription_id' => $event->stripe_subscription_id ? (string) $event->stripe_subscription_id : null, 'processed_at' => $event->processed_at?->toISOString(), 'created_at' => $event->created_at?->toISOString(), 'payload_summary' => is_array($event->payload_summary) ? $event->payload_summary : [], ]) ->values() ->all(); } /** * @return array> */ private function formatPlanBreakdown(array $planBreakdown): array { return collect(array_keys($this->plans->plans())) ->map(function (string $planKey) use ($planBreakdown): array { $plan = $this->plans->plan($planKey); return [ 'key' => $planKey, 'label' => (string) ($plan['label'] ?? $planKey), 'tier' => (string) ($plan['tier'] ?? 'free'), 'interval' => (string) ($plan['interval'] ?? 'monthly'), 'configured' => (bool) ($plan['configured'] ?? false), 'subscribers' => (int) ($planBreakdown[$planKey] ?? 0), ]; }) ->values() ->all(); } /** * @return list */ private function planKeysForSubscription(Subscription $subscription): array { $keys = []; foreach ($this->priceIdsForSubscription($subscription) as $priceId) { $plan = $this->plans->planForPriceId($priceId); if ($plan === null) { continue; } $keys[] = (string) ($plan['key'] ?? ''); } return array_values(array_unique(array_filter($keys, static fn (string $key): bool => $key !== ''))); } private function tierForSubscription(Subscription $subscription): ?string { $matchedTier = null; foreach ($this->priceIdsForSubscription($subscription) as $priceId) { $plan = $this->plans->planForPriceId($priceId); if ($plan === null) { continue; } $tier = (string) ($plan['tier'] ?? 'free'); if ($matchedTier === null || $this->rankForTier($tier) > $this->rankForTier($matchedTier)) { $matchedTier = $tier; } } return $matchedTier; } /** * @return Collection */ private function priceIdsForSubscription(Subscription $subscription): Collection { $priceIds = $subscription->items ->pluck('stripe_price') ->filter(fn ($value): bool => is_string($value) && trim($value) !== '') ->map(fn (string $value): string => trim($value)); if ($priceIds->isNotEmpty()) { return $priceIds->values(); } $fallbackPrice = trim((string) $subscription->stripe_price); return $fallbackPrice === '' ? collect() : collect([$fallbackPrice]); } private function rankForTier(string $tier): int { return match ($this->plans->normalizeTier($tier)) { 'pro' => 2, 'creator' => 1, default => 0, }; } }