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,11 @@
<?php
namespace App\Filament\Resources\PaymentProviders\Pages;
use App\Filament\Resources\PaymentProviders\PaymentProviderResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePaymentProvider extends CreateRecord
{
protected static string $resource = PaymentProviderResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PaymentProviders\Pages;
use App\Filament\Resources\PaymentProviders\PaymentProviderResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditPaymentProvider extends EditRecord
{
protected static string $resource = PaymentProviderResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PaymentProviders\Pages;
use App\Filament\Resources\PaymentProviders\PaymentProviderResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListPaymentProviders extends ListRecords
{
protected static string $resource = PaymentProviderResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Filament\Resources\PaymentProviders;
use App\Filament\Resources\PaymentProviders\Pages\CreatePaymentProvider;
use App\Filament\Resources\PaymentProviders\Pages\EditPaymentProvider;
use App\Filament\Resources\PaymentProviders\Pages\ListPaymentProviders;
use App\Filament\Resources\PaymentProviders\Schemas\PaymentProviderForm;
use App\Filament\Resources\PaymentProviders\Tables\PaymentProvidersTable;
use App\Models\PaymentProvider;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class PaymentProviderResource extends Resource
{
protected static ?string $model = PaymentProvider::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube;
protected static ?string $navigationLabel = 'Payment Providers';
protected static ?string $modelLabel = 'Payment Provider';
protected static ?string $pluralModelLabel = 'Payment Providers';
protected static string|null|\UnitEnum $navigationGroup = 'Payment Management';
public static function form(Schema $schema): Schema
{
return PaymentProviderForm::configure($schema);
}
public static function table(Table $table): Table
{
return PaymentProvidersTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListPaymentProviders::route('/'),
'create' => CreatePaymentProvider::route('/create'),
'edit' => EditPaymentProvider::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Resources\PaymentProviders\Schemas;
use Filament\Schemas\Components\Grid;
use Filament\Forms\Components\KeyValue;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class PaymentProviderForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('Basic Information')
->schema([
Grid::make(2)
->schema([
TextInput::make('name')
->label('Provider Name')
->required()
->unique(ignoreRecord: true)
->helperText('Internal identifier for the provider'),
TextInput::make('display_name')
->label('Display Name')
->required()
->helperText('Name shown to users'),
]),
Textarea::make('description')
->label('Description')
->rows(3)
->helperText('Brief description of the payment provider')
->columnSpanFull(),
]),
Section::make('Capabilities')
->schema([
Grid::make(3)
->schema([
Toggle::make('is_active')
->label('Active')
->default(true)
->helperText('Enable this provider for use'),
Toggle::make('supports_recurring')
->label('Supports Recurring')
->default(false)
->helperText('Can handle subscription payments'),
Toggle::make('supports_one_time')
->label('Supports One-Time')
->default(true)
->helperText('Can handle single payments'),
]),
Grid::make(2)
->schema([
TextInput::make('priority')
->label('Priority')
->numeric()
->default(0)
->helperText('Higher priority = shown first'),
Toggle::make('is_fallback')
->label('Fallback Provider')
->default(false)
->helperText('Default provider when others fail'),
]),
]),
Section::make('Configuration')
->schema([
KeyValue::make('configuration')
->label('Provider Configuration')
->addActionLabel('Add configuration')
->keyLabel('Key')
->valueLabel('Value')
->helperText('API keys and other provider-specific settings')
->columnSpanFull(),
KeyValue::make('supported_currencies')
->label('Supported Currencies')
->addActionLabel('Add currency')
->keyLabel('Currency Code')
->valueLabel('Display Name')
->default(['USD' => 'US Dollar'])
->helperText('Currencies this provider supports')
->columnSpanFull(),
KeyValue::make('fee_structure')
->label('Fee Structure')
->addActionLabel('Add fee setting')
->keyLabel('Fee Type')
->valueLabel('Value')
->helperText('Example: fixed_fee, percentage_fee')
->columnSpanFull(),
]),
Section::make('Webhook Settings')
->schema([
Grid::make(2)
->schema([
TextInput::make('webhook_url')
->label('Webhook URL')
->url()
->helperText('Endpoint for provider webhooks'),
TextInput::make('webhook_secret')
->label('Webhook Secret')
->password()
->helperText('Secret for webhook validation'),
]),
])
->collapsible(),
]);
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Filament\Resources\PaymentProviders\Tables;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\Action;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class PaymentProvidersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Provider')
->searchable()
->sortable()
->weight('bold'),
TextColumn::make('display_name')
->label('Display Name')
->searchable()
->sortable(),
IconColumn::make('is_active')
->label('Active')
->boolean()
->sortable(),
IconColumn::make('supports_recurring')
->label('Recurring')
->boolean()
->sortable(),
IconColumn::make('supports_one_time')
->label('One-Time')
->boolean()
->sortable(),
IconColumn::make('is_fallback')
->label('Fallback')
->boolean()
->sortable()
->color(fn ($record) => $record->is_fallback ? 'warning' : null),
TextColumn::make('priority')
->label('Priority')
->numeric()
->sortable()
->alignCenter(),
TextColumn::make('supported_currencies')
->label('Currencies')
->formatStateUsing(fn ($state) => is_array($state) ? implode(', ', array_keys($state)) : '')
->limitList(2)
->separator(', ')
->tooltip(fn ($record) => is_array($record->supported_currencies) ? implode(', ', array_keys($record->supported_currencies)) : ''),
TextColumn::make('created_at')
->label('Created')
->dateTime('M j, Y')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->label('Updated')
->dateTime('M j, Y')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
SelectFilter::make('is_active')
->label('Status')
->options([
'1' => 'Active',
'0' => 'Inactive',
]),
SelectFilter::make('supports_recurring')
->label('Recurring Support')
->options([
'1' => 'Supports Recurring',
'0' => 'No Recurring Support',
]),
SelectFilter::make('supports_one_time')
->label('One-Time Support')
->options([
'1' => 'Supports One-Time',
'0' => 'No One-Time Support',
]),
SelectFilter::make('is_fallback')
->label('Fallback Status')
->options([
'1' => 'Is Fallback',
'0' => 'Not Fallback',
]),
])
->recordActions([
EditAction::make(),
Action::make('test_connection')
->label('Test Connection')
->icon('heroicon-o-signal')
->color('info')
->action(function ($record) {
$result = $record->testConnection();
if ($result['success']) {
\Filament\Notifications\Notification::make()
->title('Connection Test Successful')
->body('Provider is configured and responding correctly.')
->success()
->send();
} else {
\Filament\Notifications\Notification::make()
->title('Connection Test Failed')
->body($result['error'])
->danger()
->send();
}
}),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
BulkAction::make('activate')
->label('Activate Selected')
->icon('heroicon-o-check')
->action(function ($records) {
$records->each->update(['is_active' => true]);
\Filament\Notifications\Notification::make()
->title('Providers Activated')
->body('Selected payment providers have been activated.')
->success()
->send();
})
->deselectRecordsAfterCompletion(),
BulkAction::make('deactivate')
->label('Deactivate Selected')
->icon('heroicon-o-x-mark')
->action(function ($records) {
$records->where('is_fallback', false)->each->update(['is_active' => false]);
\Filament\Notifications\Notification::make()
->title('Providers Deactivated')
->body('Selected payment providers have been deactivated (fallback providers were skipped).')
->warning()
->send();
})
->deselectRecordsAfterCompletion(),
])
->emptyStateActions([
CreateAction::make(),
])
->emptyStateDescription('No payment providers configured yet.')
->emptyStateHeading('No Payment Providers')
->emptyStateIcon('heroicon-o-rectangle-stack');
}
}