feat: implement comprehensive multi-provider payment processing system

- 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
This commit is contained in:
idevakk
2025-11-19 09:37:00 -08:00
parent 0560016f33
commit 27ac13948c
83 changed files with 15613 additions and 103 deletions

View File

@@ -0,0 +1,120 @@
<?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";
}
}