- 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
180 lines
8.2 KiB
PHP
180 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources\TrialExtensions\Schemas;
|
|
|
|
use App\Models\Subscription;
|
|
use Filament\Forms\Components\DateTimePicker;
|
|
use Filament\Forms\Components\KeyValue;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Schemas\Components\Grid;
|
|
use Filament\Schemas\Components\Section;
|
|
use Filament\Schemas\Schema;
|
|
|
|
class TrialExtensionForm
|
|
{
|
|
public static function configure(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->components([
|
|
Section::make('Trial Extension Details')
|
|
->schema([
|
|
Grid::make(2)
|
|
->schema([
|
|
Select::make('subscription_id')
|
|
->label('Subscription')
|
|
->options(function () {
|
|
return Subscription::with(['user', 'plan'])
|
|
->where('status', 'trialing')
|
|
->orWhere('status', 'active')
|
|
->get()
|
|
->mapWithKeys(function ($subscription) {
|
|
$label = "{$subscription->user->name} - {$subscription->plan->name}";
|
|
if ($subscription->trial_ends_at) {
|
|
$label .= " ({$subscription->trial_ends_at->format('M j, Y')})";
|
|
}
|
|
return [$subscription->id => $label];
|
|
})
|
|
->toArray();
|
|
})
|
|
->searchable()
|
|
->preload()
|
|
->required()
|
|
->live()
|
|
->afterStateUpdated(function ($state, callable $set, callable $get) {
|
|
if ($state) {
|
|
$subscription = Subscription::find($state);
|
|
if ($subscription) {
|
|
$set('user_id', $subscription->user_id);
|
|
$set('original_trial_ends_at', $subscription->trial_ends_at);
|
|
|
|
// Set user display name
|
|
if ($subscription->relationLoaded('user')) {
|
|
$set('user_display', $subscription->user->name);
|
|
} else {
|
|
$subscription->load('user');
|
|
$set('user_display', $subscription->user->name);
|
|
}
|
|
|
|
self::calculateNewTrialEndDate($set, $get);
|
|
}
|
|
}
|
|
}),
|
|
|
|
TextInput::make('user_display')
|
|
->label('User')
|
|
->disabled()
|
|
->dehydrated(false),
|
|
]),
|
|
|
|
// Hidden field for user_id to ensure it gets submitted
|
|
TextInput::make('user_id')
|
|
->hidden(),
|
|
|
|
Grid::make(2)
|
|
->schema([
|
|
TextInput::make('extension_days')
|
|
->label('Extension Days')
|
|
->numeric()
|
|
->required()
|
|
->default(7)
|
|
->live(onBlur: true)
|
|
->helperText('Number of days to extend the trial')
|
|
->afterStateUpdated(fn ($state, callable $set, callable $get) => self::calculateNewTrialEndDate($set, $get)),
|
|
|
|
Select::make('extension_type')
|
|
->label('Extension Type')
|
|
->options([
|
|
'manual' => 'Manual Grant',
|
|
'automatic' => 'Automatic Extension',
|
|
'compensation' => 'Compensation',
|
|
])
|
|
->default('manual')
|
|
->required(),
|
|
]),
|
|
|
|
Grid::make(2)
|
|
->schema([
|
|
DateTimePicker::make('original_trial_ends_at')
|
|
->label('Original Trial End Date')
|
|
->disabled()
|
|
->dehydrated(false),
|
|
|
|
DateTimePicker::make('new_trial_ends_at')
|
|
->label('New Trial End Date')
|
|
->required()
|
|
->readOnly()
|
|
->helperText('This will be calculated automatically based on extension days'),
|
|
]),
|
|
]),
|
|
|
|
Section::make('Additional Information')
|
|
->schema([
|
|
Textarea::make('reason')
|
|
->label('Reason')
|
|
->rows(3)
|
|
->helperText('Reason for granting this trial extension'),
|
|
|
|
Select::make('granted_by_admin_id')
|
|
->label('Granted By')
|
|
->relationship('grantedByAdmin', 'name')
|
|
->searchable()
|
|
->preload()
|
|
->default(fn () => auth()->id())
|
|
->helperText('Admin who granted this extension'),
|
|
])
|
|
->collapsible(),
|
|
|
|
Section::make('Timestamps')
|
|
->schema([
|
|
DateTimePicker::make('granted_at')
|
|
->label('Granted At')
|
|
->default(now())
|
|
->required(),
|
|
])
|
|
->collapsible(),
|
|
|
|
Section::make('Metadata')
|
|
->schema([
|
|
KeyValue::make('metadata')
|
|
->label('Custom Metadata')
|
|
->addActionLabel('Add metadata')
|
|
->keyLabel('Key')
|
|
->valueLabel('Value')
|
|
->helperText('Additional key-value data for this trial extension'),
|
|
])
|
|
->collapsible(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Calculate the new trial end date based on original date and extension days
|
|
*/
|
|
private static function calculateNewTrialEndDate(callable $set, callable $get): void
|
|
{
|
|
$subscriptionId = $get('subscription_id');
|
|
$extensionDays = (int) $get('extension_days') ?: 0;
|
|
|
|
if (! $subscriptionId || ! $extensionDays) {
|
|
$set('new_trial_ends_at', null);
|
|
return;
|
|
}
|
|
|
|
$subscription = Subscription::find($subscriptionId);
|
|
if (! $subscription) {
|
|
$set('new_trial_ends_at', null);
|
|
return;
|
|
}
|
|
|
|
// Use the original trial end date if available, otherwise use current date
|
|
$baseDate = $subscription->trial_ends_at ?: now();
|
|
|
|
// Calculate new end date by adding extension days
|
|
$newEndDate = $baseDate->copy()->addDays($extensionDays);
|
|
|
|
// Format for DateTimePicker (Y-m-d H:i:s format)
|
|
$set('new_trial_ends_at', $newEndDate->format('Y-m-d H:i:s'));
|
|
}
|
|
}
|