Files
zemailnator/app/Filament/Resources/Subscriptions/Schemas/SubscriptionForm.php
idevakk d767c6cf59 feat(plans): enhance plan management with dynamic providers, improved UI, and bug fixes
Dynamic Provider Integration:
  - Replace hardcoded provider arrays with database-driven payment_providers lookup
  - Display provider status (Active/Inactive) in selection dropdowns
  - Add provider_variant_id and provider_product_id input fields to plan configuration
  - Update EditPlan and SubscriptionForm with dynamic provider selection
  - Add empty state handling with helpful guidance when no providers exist

  UI/UX Improvements:
  - Format billing_cycle_days to readable text (Daily, Weekly, Monthly, Quarterly, Annually)
  - Add color-coded badges for billing cycle frequency
  - Fix plan_providers and plan_feature_limits count display with eager loading
  - Implement intelligent color coding for count indicators
  - Add visual status indicators for provider availability

  Database Compatibility:
  - Fix SQLite strftime() compatibility across all dashboard widgets
  - Fix CAST AS REAL syntax in ChurnAnalysis widget
  - Add database-agnostic date and cast expression methods
  - Support MySQL, SQLite, PostgreSQL, and SQL Server

  Bug Fixes:
  - Fix null reference error in SubscriptionForm provider_data access
  - Add null safety checks for new subscription creation
  - Optimize queries with withCount() to prevent N+1 issues

  Performance Optimizations:
  - Add eager loading with withCount() for relationship counts
  - Optimize plan provider and feature limit queries
  - Prevent N+1 query issues in resource tables

  BREAKING CHANGE: Plan provider configuration now uses dynamic provider options
  from payment_providers table instead of hardcoded list.
2025-12-02 09:47:51 -08:00

312 lines
16 KiB
PHP

<?php
namespace App\Filament\Resources\Subscriptions\Schemas;
use App\Models\PaymentProvider;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
class SubscriptionForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('Basic Information')
->schema([
Grid::make(2)
->schema([
Select::make('user_id')
->label('User')
->relationship('user', 'name')
->searchable()
->preload()
->required()
->createOptionForm([
TextInput::make('name')
->required(),
TextInput::make('email')
->email()
->required(),
]),
Select::make('plan_id')
->label('Plan')
->relationship('plan', 'name')
->searchable()
->preload()
->required(),
]),
Grid::make(2)
->schema([
Select::make('type')
->label('Subscription Type')
->options([
'default' => 'Default',
'premium' => 'Premium',
'enterprise' => 'Enterprise',
'trial' => 'Trial',
])
->required()
->default('default')
->helperText('Type of subscription'),
Select::make('status')
->label('Status')
->options([
'active' => 'Active',
'trialing' => 'Trial',
'cancelled' => 'Cancelled',
'paused' => 'Paused',
'incomplete' => 'Incomplete',
])
->required()
->default('active'),
]),
Grid::make(2)
->schema([
Select::make('provider')
->label('Provider')
->options(function () {
$providers = PaymentProvider::orderBy('priority', 'desc')
->orderBy('name')
->get();
if ($providers->isEmpty()) {
return [
'no_providers' => '⚠️ No payment providers configured - Please add providers first',
];
}
return $providers->mapWithKeys(function ($provider) {
$status = $provider->is_active ? 'Active' : 'Inactive';
return [
$provider->name => "{$provider->display_name} ({$status})",
];
})->toArray();
})
->required()
->default(function () {
$providers = PaymentProvider::orderBy('priority', 'desc')
->orderBy('name')
->get();
if ($providers->isEmpty()) {
return 'no_providers';
}
return PaymentProvider::where('name', 'stripe')->exists()
? 'stripe'
: PaymentProvider::active()->first()?->name;
})
->helperText(function () {
$providers = PaymentProvider::orderBy('priority', 'desc')
->orderBy('name')
->get();
if ($providers->isEmpty()) {
return '⚠️ No payment providers available. Please configure payment providers in the Payment Providers section first.';
}
return 'Select a payment provider for this subscription';
}),
TextInput::make('provider_subscription_id')
->label('Provider Subscription ID'),
]),
]),
Section::make('Trial Management')
->schema([
Grid::make(2)
->schema([
DateTimePicker::make('trial_ends_at')
->label('Trial Ends At'),
TextInput::make('quantity')
->label('Quantity')
->numeric()
->default(1),
]),
])
->collapsible(),
Section::make('Billing Dates')
->schema([
Grid::make(2)
->schema([
DateTimePicker::make('starts_at')
->label('Starts At'),
DateTimePicker::make('ends_at')
->label('Ends At'),
]),
Grid::make(2)
->schema([
DateTimePicker::make('cancelled_at')
->label('Cancelled At'),
DateTimePicker::make('paused_at')
->label('Paused At'),
]),
Grid::make(2)
->schema([
DateTimePicker::make('resumed_at')
->label('Resumed At'),
DateTimePicker::make('last_provider_sync')
->label('Last Provider Sync'),
]),
])
->collapsible(),
Section::make('Cancellation Details')
->schema([
Textarea::make('cancellation_reason')
->label('Reason'),
])
->collapsible()
->visible(fn ($get) => $get('status') === 'cancelled'),
Section::make('Migration Information')
->schema([
TextInput::make('migration_batch_id')
->label('Migration Batch ID'),
Toggle::make('is_migrated')
->label('Is Migrated'),
Textarea::make('legacy_data')
->label('Legacy Data')
->rows(3),
])
->collapsible(),
Tabs::make('Provider Data')
->tabs([
Tab::make('Overview')
->schema([
Section::make('Activation Key')
->schema([
TextEntry::make('provider_data.activation_key')
->label('Activation Key')
->copyable(),
TextEntry::make('provider_data.key_id')
->label('Key ID'),
TextEntry::make('provider_data.redeemed_at')
->label('Redeemed At'),
])
->columns(1)
->hidden(fn ($record) => ! data_get($record, 'provider_data.activation_key')),
Section::make('Plan Details')
->schema([
TextEntry::make('provider_data.plan_details.name')
->label('Plan Name'),
TextEntry::make('provider_data.plan_details.price')
->label('Price')
->prefix('$'),
TextEntry::make('provider_data.plan_details.billing_cycle_display')
->label('Billing Cycle'),
TextEntry::make('provider_data.plan_details.plan_tier')
->badge()
->label('Tier'),
TextEntry::make('provider_data.plan_details.billing_cycle_days')
->label('Duration')
->suffix(' days'),
])
->columns(2)
->hidden(fn ($record) => ! data_get($record, 'provider_data.plan_details')),
Section::make('Provider Info')
->schema([
TextEntry::make('provider_data.provider_info.name')
->label('Provider Name'),
TextEntry::make('provider_data.provider_info.version')
->label('Version'),
TextEntry::make('provider_data.provider_info.processed_at')
->label('Processed At'),
])
->columns(2)
->hidden(fn ($record) => ! data_get($record, 'provider_data.provider_info')),
]),
Tab::make('Features')
->schema([
RepeatableEntry::make('provider_data.plan_details.features')
->schema([
Section::make()
->schema([
TextEntry::make('feature.display_name')
->label('Feature Name')
->weight('bold'),
TextEntry::make('feature.description')
->label('Description')
->columnSpanFull(),
TextEntry::make('feature.category')
->label('Category')
->badge(),
TextEntry::make('feature.type')
->label('Type'),
Section::make('Limit Details')
->schema([
TextEntry::make('limit.limit_value')
->label('Limit Value'),
TextEntry::make('limit.trial_limit_value')
->label('Trial Limit'),
TextEntry::make('limit.limit_type')
->badge()
->label('Limit Type'),
TextEntry::make('limit.is_enabled')
->label('Status')
->badge()
->formatStateUsing(fn ($state) => $state ? 'Enabled' : 'Disabled')
->color(fn ($state) => $state ? 'success' : 'gray'),
])
->columns(2)
->collapsed(),
])
->columns(2),
])
->columnSpanFull(),
])
->hidden(fn ($record) => ! data_get($record, 'provider_data.plan_details.features')),
Tab::make('Raw JSON')
->schema([
TextEntry::make('provider_data')
->formatStateUsing(function ($state) {
if (is_string($state)) {
$data = json_decode($state, true);
} else {
$data = (array) $state;
}
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
})
->state(fn ($record) => $record?->provider_data ?? '{}')
->copyable()
->columnSpanFull(),
]),
])->columnSpanFull(),
]);
}
}