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:
150
app/Filament/Widgets/ChurnAnalysis.php
Normal file
150
app/Filament/Widgets/ChurnAnalysis.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Subscription;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ChurnAnalysis extends ChartWidget
|
||||
{
|
||||
protected static ?int $sort = 3;
|
||||
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Churn Analysis';
|
||||
}
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$monthlyChurn = $this->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
|
||||
{
|
||||
return Subscription::query()
|
||||
->select(
|
||||
'provider',
|
||||
DB::raw('COUNT(CASE WHEN status = "cancelled" THEN 1 END) as cancelled'),
|
||||
DB::raw('COUNT(*) as total'),
|
||||
DB::raw('(CAST(COUNT(CASE WHEN status = "cancelled" THEN 1 END) AS REAL) * 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
|
||||
{
|
||||
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('(CAST(COUNT(CASE WHEN subscriptions.status = "cancelled" THEN 1 END) AS REAL) * 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user