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:
176
app/Filament/Widgets/TrialPerformance.php
Normal file
176
app/Filament/Widgets/TrialPerformance.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Subscription;
|
||||
use App\Models\TrialExtension;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TrialPerformance extends ChartWidget
|
||||
{
|
||||
protected static ?int $sort = 4;
|
||||
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Trial Performance';
|
||||
}
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$trialConversion = $this->getTrialConversionRates();
|
||||
$trialExtensions = $this->getTrialExtensionsTrend();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Trial Conversion Rate (%)',
|
||||
'data' => array_values($trialConversion),
|
||||
'borderColor' => 'rgba(168, 85, 247, 1)',
|
||||
'backgroundColor' => 'rgba(168, 85, 247, 0.1)',
|
||||
'fill' => true,
|
||||
'tension' => 0.4,
|
||||
],
|
||||
[
|
||||
'label' => 'Trial Extensions',
|
||||
'data' => array_values($trialExtensions),
|
||||
'borderColor' => 'rgba(251, 146, 60, 1)',
|
||||
'backgroundColor' => 'rgba(251, 146, 60, 0.1)',
|
||||
'fill' => true,
|
||||
'tension' => 0.4,
|
||||
],
|
||||
],
|
||||
'labels' => array_keys($trialConversion),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'responsive' => true,
|
||||
'interaction' => [
|
||||
'intersect' => false,
|
||||
'mode' => 'index',
|
||||
],
|
||||
'plugins' => [
|
||||
'legend' => [
|
||||
'position' => 'top',
|
||||
],
|
||||
'tooltip' => [
|
||||
'callbacks' => [
|
||||
'label' => 'function(context) {
|
||||
let label = context.dataset.label || "";
|
||||
if (label) {
|
||||
label += ": ";
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
if (label.includes("Rate")) {
|
||||
label += context.parsed.y.toFixed(1) + "%";
|
||||
} else {
|
||||
label += context.parsed.y;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}',
|
||||
],
|
||||
],
|
||||
],
|
||||
'scales' => [
|
||||
'y' => [
|
||||
'beginAtZero' => true,
|
||||
'ticks' => [
|
||||
'callback' => 'function(value, index, values) {
|
||||
if (index === 0 || index === values.length - 1) {
|
||||
return value + (this.chart.data.datasets[0].label.includes("Rate") ? "%" : "");
|
||||
}
|
||||
return "";
|
||||
}',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getTrialConversionRates(): array
|
||||
{
|
||||
$conversionData = [];
|
||||
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$month = now()->subMonths($i);
|
||||
$monthStart = $month->copy()->startOfMonth();
|
||||
$monthEnd = $month->copy()->endOfMonth();
|
||||
|
||||
// Trials that ended during this month
|
||||
$endedTrials = Subscription::query()
|
||||
->where('status', 'trialing')
|
||||
->whereBetween('trial_ends_at', [$monthStart, $monthEnd])
|
||||
->get();
|
||||
|
||||
$totalTrials = $endedTrials->count();
|
||||
$convertedTrials = $endedTrials->filter(function ($trial) use ($monthEnd) {
|
||||
// Check if trial converted to paid subscription
|
||||
return Subscription::query()
|
||||
->where('user_id', $trial->user_id)
|
||||
->where('status', 'active')
|
||||
->where('created_at', '>', $trial->trial_ends_at)
|
||||
->where('created_at', '<=', $monthEnd)
|
||||
->exists();
|
||||
})->count();
|
||||
|
||||
$conversionRate = $totalTrials > 0 ? ($convertedTrials / $totalTrials) * 100 : 0;
|
||||
|
||||
$conversionData[$month->format('M Y')] = round($conversionRate, 2);
|
||||
}
|
||||
|
||||
return $conversionData;
|
||||
}
|
||||
|
||||
private function getTrialExtensionsTrend(): array
|
||||
{
|
||||
return TrialExtension::query()
|
||||
->select(
|
||||
DB::raw("strftime('%Y-%m', trial_extensions.granted_at) as month"),
|
||||
DB::raw('COUNT(*) as extensions'),
|
||||
DB::raw('SUM(extension_days) as total_days')
|
||||
)
|
||||
->where('trial_extensions.granted_at', '>=', now()->subMonths(6))
|
||||
->groupBy('month')
|
||||
->orderBy('month')
|
||||
->get()
|
||||
->mapWithKeys(function ($item) {
|
||||
$date = \Carbon\Carbon::createFromFormat('Y-m', $item->month);
|
||||
|
||||
return [$date->format('M Y') => $item->extensions];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function getAverageTrialLength(): float
|
||||
{
|
||||
return Subscription::query()
|
||||
->where('status', 'trialing')
|
||||
->orWhere('status', 'active')
|
||||
->whereNotNull('trial_ends_at')
|
||||
->selectRaw('AVG(DATEDIFF(trial_ends_at, created_at)) as avg_trial_days')
|
||||
->value('avg_trial_days') ?? 0;
|
||||
}
|
||||
|
||||
private function getMostCommonExtensionReasons(): array
|
||||
{
|
||||
return TrialExtension::query()
|
||||
->select('reason', DB::raw('COUNT(*) as count'))
|
||||
->whereNotNull('reason')
|
||||
->groupBy('reason')
|
||||
->orderBy('count', 'desc')
|
||||
->limit(5)
|
||||
->pluck('count', 'reason')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user