record && $this->record->trialConfiguration) { $data['trialConfiguration'] = $this->record->trialConfiguration->toArray(); } return $data; } public function form(Schema $schema): Schema { return $schema ->components([ // Main Plan Information Tab Tabs::make('Plan Management') ->tabs([ Tab::make('Basic Info') ->label('Plan Information') ->schema([ Grid::make(3)->schema([ TextInput::make('name') ->label('Plan Name') ->required() ->maxLength(255), TextInput::make('price') ->label('Price') ->numeric() ->prefix('$') ->required(), Select::make('billing_cycle_days') ->label('Billing Cycle') ->options([ 30 => 'Monthly', 90 => 'Quarterly', 365 => 'Yearly', 60 => 'Bi-Monthly', 180 => 'Semi-Annual', ]) ->required(), ]), Grid::make(2)->schema([ TextInput::make('product_id') ->label('Product ID') ->required() ->helperText('External product identifier'), TextInput::make('pricing_id') ->label('Pricing ID') ->required() ->helperText('External pricing identifier'), ]), Textarea::make('description') ->label('Description') ->rows(3) ->maxLength(500), Grid::make(3)->schema([ Select::make('plan_tier_id') ->label('Plan Tier') ->relationship('planTier', 'name') ->nullable() ->searchable() ->helperText('Optional tier classification'), Toggle::make('is_active') ->label('Active') ->default(true) ->helperText('Plan is available for new subscriptions'), TextInput::make('sort_order') ->label('Sort Order') ->numeric() ->default(0) ->helperText('Display order in pricing tables'), ]), ]), // Features Management Tab Tab::make('Features') ->label('Features & Limits') ->schema([ TextEntry::make('features_help') ->label('Feature Management') ->state('Configure which features are available and their limits for this plan. You can also set different limits for trial periods.'), Repeater::make('planFeatureLimits') ->label('Feature Limits') ->relationship() ->schema([ Grid::make(2)->schema([ Select::make('plan_feature_id') ->label('Feature') ->options(PlanFeature::active()->ordered()->pluck('display_name', 'id')) ->required() ->reactive() ->afterStateUpdated(fn ($state, callable $set) => $set('feature_type', PlanFeature::find($state)?->type ?? 'boolean') ), Select::make('limit_type') ->label('Limit Type') ->options([ 'monthly' => 'Monthly', 'daily' => 'Daily', 'total' => 'Total', ]) ->default('monthly') ->required(), ]), Grid::make(2)->schema([ Toggle::make('is_enabled') ->label('Enabled') ->default(true), TextInput::make('limit_value') ->label('Limit Value') ->numeric() ->placeholder('Unlimited if empty') ->helperText('Leave empty for unlimited'), ]), Section::make('Trial Settings') ->description('Override settings for trial periods') ->collapsed() ->schema([ Toggle::make('applies_during_trial') ->label('Apply Limits During Trial') ->default(true), TextInput::make('trial_limit_value') ->label('Trial Limit Value') ->numeric() ->placeholder('Use regular limit if empty') ->helperText('Different limit for trial period'), ]), ]) ->columns(1) ->collapsible() ->itemLabel(fn (array $state): ?string => PlanFeature::find($state['plan_feature_id'] ?? null)?->display_name ?? 'New Feature' ), ]), // Payment Providers Tab Tab::make('Providers') ->label('Payment Providers') ->schema([ TextEntry::make('providers_help') ->label('Payment Provider Configuration') ->state('Configure which payment providers are available for this plan. Users will be able to choose from the enabled providers at checkout.'), Repeater::make('planProviders') ->label('Payment Providers') ->relationship() ->schema([ Grid::make(2)->schema([ Select::make('provider') ->label('Provider') ->options([ 'stripe' => 'Stripe', 'lemon_squeezy' => 'Lemon Squeezy', 'polar' => 'Polar.sh', 'oxapay' => 'OxaPay', 'crypto' => 'Crypto', 'activation_key' => 'Activation Key', ]) ->required() ->reactive() ->afterStateUpdated(function ($state, callable $set) { match ($state) { 'stripe' => $set('provider_price_id_label', 'Stripe Price ID'), 'lemon_squeezy' => $set('provider_price_id_label', 'Lemon Squeezy Variant ID'), 'polar' => $set('provider_price_id_label', 'Polar Product ID'), default => $set('provider_price_id_label', 'Provider ID'), }; }), Toggle::make('is_enabled') ->label('Enabled') ->default(true), ]), Grid::make(2)->schema([ TextInput::make('provider_price_id') ->label(fn (callable $get) => $get('provider_price_id_label') ?? 'Provider Price ID') ->helperText('Price/Variant ID from the provider'), TextInput::make('price') ->label('Price Override') ->numeric() ->prefix('$') ->helperText('Override plan price for this provider'), ]), Grid::make(2)->schema([ TextInput::make('currency') ->label('Currency') ->default('USD') ->maxLength(3), TextInput::make('sort_order') ->label('Display Order') ->numeric() ->default(0), ]), ]) ->columns(1) ->collapsible() ->itemLabel(fn (array $state): ?string => $state['provider'] ?? 'New Provider'), ]), // Trial Configuration Tab Tab::make('Trials') ->label('Trial Settings') ->schema([ TextEntry::make('trials_help') ->label('Trial Configuration') ->state('Configure trial settings for this plan. Trials are disabled by default and must be explicitly enabled.'), // Always visible: Enable/Disable toggle Section::make('Trial Status') ->description('Enable or disable trial functionality for this plan') ->schema([ Toggle::make('trialConfiguration.trial_enabled') ->label('Enable Trials') ->default(false) ->reactive() ->afterStateUpdated(function ($state, callable $set) { if (! $state) { $set('trialConfiguration.trial_duration_days', 14); $set('trialConfiguration.trial_requires_payment_method', true); $set('trialConfiguration.trial_auto_converts', true); } }) ->helperText('Toggle to enable trial functionality for this plan'), ]), // Trial configuration section - only visible when trials are enabled Section::make('Trial Configuration') ->description('Configure trial period, limits, and behavior settings') ->collapsed() ->schema([ Grid::make(2)->schema([ TextInput::make('trialConfiguration.trial_duration_days') ->label('Trial Duration (Days)') ->numeric() ->default(14) ->required() ->helperText('Number of days for the trial period'), TextInput::make('trialConfiguration.trial_extension_limit') ->label('Extension Limit') ->numeric() ->default(0) ->required() ->helperText('Maximum number of trial extensions allowed'), ]), Grid::make(3)->schema([ Toggle::make('trialConfiguration.trial_requires_payment_method') ->label('Require Payment Method') ->default(true) ->helperText('Require payment method to start trial'), Toggle::make('trialConfiguration.trial_auto_converts') ->label('Auto-convert to Paid') ->default(true) ->helperText('Automatically convert to paid plan when trial ends'), Select::make('trialConfiguration.trial_conversion_action') ->label('Conversion Action') ->options([ 'upgrade_to_paid' => 'Upgrade to Paid', 'cancel' => 'Cancel Subscription', 'notify' => 'Notify Only', ]) ->required() ->default('upgrade_to_paid') ->helperText('Action to take when trial expires'), ]), Section::make('Trial Messages') ->description('Customize messages shown to users during trial period') ->collapsed() ->schema([ Textarea::make('trialConfiguration.trial_welcome_message') ->label('Welcome Message') ->rows(2) ->placeholder('Message shown when trial starts') ->helperText('Displayed to users when they begin a trial'), Textarea::make('trialConfiguration.trial_expiry_message') ->label('Expiry Message') ->rows(2) ->placeholder('Message shown when trial is about to expire') ->helperText('Displayed to users when trial is ending'), ]), ]) ->visible(fn (callable $get) => $get('trialConfiguration.trial_enabled') ?? false), TextEntry::make('trials_disabled_info') ->label('Trials Currently Disabled') ->state('Enable the "Enable Trials" toggle above to configure trial settings for this plan. Trial functionality allows users to try your plan before committing to a paid subscription.') ->columnSpanFull() ->visible(fn (callable $get) => ! ($get('trialConfiguration.trial_enabled') ?? false)), ]), ]) ->columnSpanFull(), ]); } protected function getHeaderActions(): array { return [ Actions\DeleteAction::make() ->before(function (Model $record) { // Prevent deletion if plan has active subscriptions if ($record->subscriptions()->where('status', 'active')->exists()) { Log::error('Cannot delete plan with active subscriptions'); } }), ]; } protected function getRedirectUrl(): ?string { return self::getResource()::getUrl('index'); } protected function getSavedNotification(): ?Notification { return Notification::make() ->success() ->title('Plan updated') ->body('Plan configuration updated successfully'); } protected function handleRecordUpdate(Model $record, array $data): Model { // Handle trial configuration if (isset($data['trialConfiguration'])) { $trialConfig = $record->trialConfiguration ?? new TrialConfiguration; // Ensure numeric fields have default values $trialData = $data['trialConfiguration']; $trialData['trial_extension_limit'] = $trialData['trial_extension_limit'] ?? 0; $trialData['trial_duration_days'] = $trialData['trial_duration_days'] ?? 14; $trialConfig->fill($trialData); $trialConfig->plan_id = $record->id; $trialConfig->save(); unset($data['trialConfiguration']); } return parent::handleRecordUpdate($record, $data); } }