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,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();
}
}