- Add unified payment provider architecture with contract-based design - Implement 6 payment providers: Stripe, Lemon Squeezy, Polar, Oxapay, Crypto, Activation Keys - Create subscription management with lifecycle handling (create, cancel, pause, resume, update) - Add coupon system with usage tracking and trial extensions - Build Filament admin resources for payment providers, subscriptions, coupons, and trials - Implement payment orchestration service with provider registry and configuration management - Add comprehensive payment logging and webhook handling for all providers - Create customer analytics dashboard with revenue, churn, and lifetime value metrics - Add subscription migration service for provider switching - Include extensive test coverage for all payment functionality
121 lines
4.1 KiB
PHP
121 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Widgets;
|
|
|
|
use App\Models\Subscription;
|
|
use App\Models\User;
|
|
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
|
|
|
class CustomerAnalyticsOverview extends BaseWidget
|
|
{
|
|
protected function getStats(): array
|
|
{
|
|
$totalCustomers = User::count();
|
|
$payingCustomers = Subscription::active()->distinct('user_id')->count('user_id');
|
|
$trialCustomers = Subscription::onTrial()->distinct('user_id')->count('user_id');
|
|
$churnedCustomers = Subscription::where('status', 'cancelled')->distinct('user_id')->count('user_id');
|
|
|
|
$mrr = $this->calculateMRR();
|
|
$arr = $mrr * 12;
|
|
$arpu = $payingCustomers > 0 ? $mrr / $payingCustomers : 0;
|
|
$ltv = $arpu * 12; // Simplified LTV calculation
|
|
|
|
return [
|
|
Stat::make('Total Customers', $totalCustomers)
|
|
->description('All registered users')
|
|
->descriptionIcon('heroicon-o-users')
|
|
->color('primary'),
|
|
|
|
Stat::make('Paying Customers', $payingCustomers)
|
|
->description($this->getCustomerGrowthRate($payingCustomers))
|
|
->descriptionIcon($this->getGrowthIcon($payingCustomers))
|
|
->color($this->getGrowthColor($payingCustomers)),
|
|
|
|
Stat::make('Trial Customers', $trialCustomers)
|
|
->description('Currently on trial')
|
|
->descriptionIcon('heroicon-o-clock')
|
|
->color('warning'),
|
|
|
|
Stat::make('Churned Customers', $churnedCustomers)
|
|
->description($this->getChurnRate($churnedCustomers, $payingCustomers + $churnedCustomers))
|
|
->descriptionIcon('heroicon-o-arrow-trending-down')
|
|
->color('danger'),
|
|
|
|
Stat::make('Monthly Recurring Revenue', '$'.number_format($mrr, 2))
|
|
->description('MRR from active subscriptions')
|
|
->descriptionIcon('heroicon-o-currency-dollar')
|
|
->color('success'),
|
|
|
|
Stat::make('Annual Recurring Revenue', '$'.number_format($arr, 2))
|
|
->description('ARR projection')
|
|
->descriptionIcon('heroicon-o-chart-bar')
|
|
->color('success'),
|
|
|
|
Stat::make('Average Revenue Per User', '$'.number_format($arpu, 2))
|
|
->description('ARPU for paying customers')
|
|
->descriptionIcon('heroicon-o-calculator')
|
|
->color('info'),
|
|
|
|
Stat::make('Lifetime Value', '$'.number_format($ltv, 2))
|
|
->description('Estimated customer LTV')
|
|
->descriptionIcon('heroicon-o-gift')
|
|
->color('purple'),
|
|
];
|
|
}
|
|
|
|
private function calculateMRR(): float
|
|
{
|
|
return Subscription::active()
|
|
->join('plans', 'subscriptions.plan_id', '=', 'plans.id')
|
|
->sum('plans.price');
|
|
}
|
|
|
|
private function getCustomerGrowthRate(int $current): string
|
|
{
|
|
$previous = Subscription::active()
|
|
->where('created_at', '<', now()->subMonth())
|
|
->distinct('user_id')
|
|
->count('user_id');
|
|
|
|
if ($previous == 0) {
|
|
return 'New customers';
|
|
}
|
|
|
|
$growth = (($current - $previous) / $previous) * 100;
|
|
|
|
return $growth >= 0 ? "+{$growth}%" : "{$growth}%";
|
|
}
|
|
|
|
private function getGrowthIcon(int $current): string
|
|
{
|
|
$previous = Subscription::active()
|
|
->where('created_at', '<', now()->subMonth())
|
|
->distinct('user_id')
|
|
->count('user_id');
|
|
|
|
return $current > $previous ? 'heroicon-o-arrow-trending-up' : 'heroicon-o-arrow-trending-down';
|
|
}
|
|
|
|
private function getGrowthColor(int $current): string
|
|
{
|
|
$previous = Subscription::active()
|
|
->where('created_at', '<', now()->subMonth())
|
|
->distinct('user_id')
|
|
->count('user_id');
|
|
|
|
return $current > $previous ? 'success' : 'danger';
|
|
}
|
|
|
|
private function getChurnRate(int $churned, int $total): string
|
|
{
|
|
if ($total == 0) {
|
|
return '0% churn rate';
|
|
}
|
|
|
|
$rate = ($churned / $total) * 100;
|
|
|
|
return "{$rate}% churn rate";
|
|
}
|
|
}
|