getMonthlyChurnRate(); $churnByProvider = $this->getChurnByProvider(); $churnByPlan = $this->getChurnByPlan(); return [ 'datasets' => [ [ 'label' => 'Monthly Churn Rate (%)', 'data' => array_values($monthlyChurn), 'borderColor' => 'rgba(239, 68, 68, 1)', 'backgroundColor' => 'rgba(239, 68, 68, 0.1)', 'fill' => true, 'tension' => 0.4, ], ], 'labels' => array_keys($monthlyChurn), ]; } protected function getType(): string { return 'line'; } 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(1) + "%"; } return label; }', ], ], ], 'scales' => [ 'y' => [ 'beginAtZero' => true, 'max' => 20, 'ticks' => [ 'callback' => 'function(value) { return value + "%"; }', ], ], ], ]; } private function getMonthlyChurnRate(): array { $churnData = []; for ($i = 5; $i >= 0; $i--) { $month = now()->subMonths($i); $monthStart = $month->copy()->startOfMonth(); $monthEnd = $month->copy()->endOfMonth(); // Active subscriptions at the beginning of the month $activeAtStart = Subscription::query() ->where('status', 'active') ->where('created_at', '<', $monthStart) ->count(); // Subscriptions cancelled during the month $cancelledDuringMonth = Subscription::query() ->where('status', 'cancelled') ->whereBetween('cancelled_at', [$monthStart, $monthEnd]) ->count(); // Calculate churn rate $churnRate = $activeAtStart > 0 ? ($cancelledDuringMonth / $activeAtStart) * 100 : 0; $churnData[$month->format('M Y')] = round($churnRate, 2); } return $churnData; } private function getChurnByProvider(): array { $castExpression = $this->getCastExpression(); return Subscription::query() ->select( 'provider', DB::raw('COUNT(CASE WHEN status = "cancelled" THEN 1 END) as cancelled'), DB::raw('COUNT(*) as total'), DB::raw("({$castExpression} * 100.0 / COUNT(*)) as churn_rate") ) ->groupBy('provider') ->orderBy('churn_rate', 'desc') ->get() ->mapWithKeys(function ($item) { return [$item->provider => round($item->churn_rate, 2)]; }) ->toArray(); } private function getChurnByPlan(): array { $castExpression = $this->getCastExpression(); return Subscription::query() ->select( 'plans.name as plan_name', DB::raw('COUNT(CASE WHEN subscriptions.status = "cancelled" THEN 1 END) as cancelled'), DB::raw('COUNT(*) as total'), DB::raw("({$castExpression} * 100.0 / COUNT(*)) as churn_rate") ) ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->groupBy('plans.id', 'plans.name') ->orderBy('churn_rate', 'desc') ->limit(10) ->get() ->mapWithKeys(function ($item) { return [$item->plan_name => round($item->churn_rate, 2)]; }) ->toArray(); } private function getCastExpression(): string { $connection = DB::connection()->getDriverName(); return match ($connection) { 'sqlite' => 'CAST(COUNT(CASE WHEN status = "cancelled" THEN 1 END) AS REAL)', 'mysql', 'mariadb' => 'CAST(COUNT(CASE WHEN status = "cancelled" THEN 1 END) AS DECIMAL(10,2))', 'pgsql' => 'CAST(COUNT(CASE WHEN status = "cancelled" THEN 1 END) AS NUMERIC)', 'sqlsrv' => 'CAST(COUNT(CASE WHEN status = "cancelled" THEN 1 END) AS FLOAT)', default => 'CAST(COUNT(CASE WHEN status = "cancelled" THEN 1 END) AS DECIMAL(10,2))', // fallback to MySQL format }; } }