getLTVByCohort(); $ltvByProvider = $this->getLTVByProvider(); $ltvByPlan = $this->getLTVByPlan(); return [ 'datasets' => [ [ 'label' => 'Average LTV by Cohort ($)', 'data' => array_values($ltvByCohort), 'borderColor' => 'rgba(34, 197, 94, 1)', 'backgroundColor' => 'rgba(34, 197, 94, 0.1)', 'fill' => true, 'tension' => 0.4, ], ], 'labels' => array_keys($ltvByCohort), ]; } protected function getType(): string { return 'bar'; } protected function getOptions(): array { return [ 'responsive' => true, 'plugins' => [ 'legend' => [ 'position' => 'top', ], 'tooltip' => [ 'callbacks' => [ 'label' => 'function(context) { let label = context.dataset.label || ""; if (label) { label += ": "; } if (context.parsed.y !== null) { label += "$" + context.parsed.y.toFixed(2); } return label; }', ], ], ], 'scales' => [ 'y' => [ 'beginAtZero' => true, 'ticks' => [ 'callback' => 'function(value) { return "$" + value; }', ], ], ], ]; } private function getLTVByCohort(): array { $cohorts = []; // Get cohorts by signup month (last 6 months) for ($i = 5; $i >= 0; $i--) { $month = now()->subMonths($i); $monthStart = $month->copy()->startOfMonth(); $monthEnd = $month->copy()->endOfMonth(); // Users who signed up in this month $cohortUsers = Subscription::query() ->select('user_id') ->whereBetween('created_at', [$monthStart, $monthEnd]) ->distinct() ->pluck('user_id'); if ($cohortUsers->isEmpty()) { $cohorts[$month->format('M Y')] = 0; continue; } // Calculate LTV for this cohort $totalRevenue = Subscription::query() ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->whereIn('subscriptions.user_id', $cohortUsers) ->where('subscriptions.status', 'active') ->sum('plans.price'); // Estimate LTV based on current MRR and average subscription length $avgSubscriptionLength = $this->getAverageSubscriptionLength(); $cohortSize = $cohortUsers->count(); $avgMRRPerUser = $cohortSize > 0 ? $totalRevenue / $cohortSize : 0; $ltv = $avgMRRPerUser * $avgSubscriptionLength; $cohorts[$month->format('M Y')] = round($ltv, 2); } return $cohorts; } private function getLTVByProvider(): array { return Subscription::query() ->select( 'provider', DB::raw('AVG(plans.price) as avg_monthly_price'), DB::raw('COUNT(*) as total_subscriptions') ) ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->where('subscriptions.status', 'active') ->groupBy('provider') ->orderBy('avg_monthly_price', 'desc') ->get() ->mapWithKeys(function ($item) { $avgLength = $this->getAverageSubscriptionLengthByProvider($item->provider); $ltv = $item->avg_monthly_price * $avgLength; return [$item->provider => round($ltv, 2)]; }) ->toArray(); } private function getLTVByPlan(): array { return Subscription::query() ->select( 'plans.name as plan_name', DB::raw('plans.price'), DB::raw('COUNT(*) as total_subscriptions') ) ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->where('subscriptions.status', 'active') ->groupBy('plans.id', 'plans.name', 'plans.price') ->orderBy('plans.price', 'desc') ->limit(10) ->get() ->mapWithKeys(function ($item) { $avgLength = $this->getAverageSubscriptionLengthByPlan($item->plan_name); $ltv = $item->price * $avgLength; return [$item->plan_name => round($ltv, 2)]; }) ->toArray(); } private function getAverageSubscriptionLength(): float { // Average subscription length in months (simplified calculation) return 12; // Could be calculated based on historical data } private function getAverageSubscriptionLengthByProvider(string $provider): float { // Provider-specific average subscription length $providerLengths = [ 'stripe' => 14, 'lemon_squeezy' => 12, 'polar' => 10, 'oxapay' => 8, 'crypto' => 6, 'activation_key' => 24, ]; return $providerLengths[$provider] ?? 12; } private function getAverageSubscriptionLengthByPlan(string $planName): float { // Plan-specific average subscription length (could be based on plan tier) return 12; // Simplified, could be more sophisticated } private function getTopQuartileLTV(): float { $ltvs = Subscription::query() ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->where('subscriptions.status', 'active') ->selectRaw('plans.price * ? as ltv', [$this->getAverageSubscriptionLength()]) ->pluck('ltv') ->sort() ->values(); $quartileIndex = (int) ($ltvs->count() * 0.75); return $ltvs->get($quartileIndex, 0); } private function getBottomQuartileLTV(): float { $ltvs = Subscription::query() ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->where('subscriptions.status', 'active') ->selectRaw('plans.price * ? as ltv', [$this->getAverageSubscriptionLength()]) ->pluck('ltv') ->sort() ->values(); $quartileIndex = (int) ($ltvs->count() * 0.25); return $ltvs->get($quartileIndex, 0); } }