- Create 7 new models with full relationships and business logic:
* PlanFeature: Define available features with categories and types
* PlanFeatureLimit: Manage usage limits per plan with trial overrides
* PlanPermission: Granular permissions system for features
* PlanProvider: Multi-provider payment configuration
* PlanTier: Hierarchical plan structure with upgrade paths
* PlanUsage: Real-time usage tracking and analytics
* TrialConfiguration: Advanced trial settings per plan
- Enhance Plan model with 25+ new methods:
* Feature checking: hasFeature(), canUseFeature(), getRemainingUsage()
* Permission system: hasPermission() with trial support
* Payment providers: getAllowedProviders(), supportsProvider()
* Trial management: hasTrial(), getTrialConfig()
* Upgrade paths: isUpgradeFrom(), getUpgradePath()
* Utility methods: getBillingCycleDisplay(), metadata handling
- Completely redesign PlanResource with tabbed interface:
* Basic Info: Core plan configuration with dynamic billing cycles
* Features & Limits: Dynamic feature management with trial overrides
* Payment Providers: Multi-provider configuration (Stripe, Lemon Squeezy, etc.)
* Trial Settings: Advanced trial configuration with always-visible toggle
- Create new Filament resources:
* PlanFeatureResource: Manage available features by category
* PlanTierResource: Hierarchical tier management with parent-child relationships
- Implement comprehensive data migration:
* Migrate legacy plan data to new enhanced system
* Create default features (mailbox accounts, email forwarding, etc.)
* Preserve existing payment provider configurations
* Set up trial configurations (disabled for legacy plans)
* Handle duplicate data gracefully with rollback support
- Add proper database constraints and indexes:
* Unique constraints on plan-feature relationships
* Foreign key constraints with cascade deletes
* Performance indexes for common queries
* JSON metadata columns for flexible configuration
- Fix trial configuration form handling:
* Add required validation for numeric fields
* Implement proper null handling with defaults
* Add type casting for all numeric fields
* Ensure database constraint compliance
186 lines
6.4 KiB
PHP
186 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Filament\Resources\PlanTierResource\Pages\CreatePlanTier;
|
|
use App\Filament\Resources\PlanTierResource\Pages\EditPlanTier;
|
|
use App\Filament\Resources\PlanTierResource\Pages\ListPlanTiers;
|
|
use App\Models\PlanTier;
|
|
use Filament\Actions\BulkActionGroup;
|
|
use Filament\Actions\CreateAction;
|
|
use Filament\Actions\DeleteAction;
|
|
use Filament\Actions\DeleteBulkAction;
|
|
use Filament\Actions\EditAction;
|
|
use Filament\Forms;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Components\Grid;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Support\Icons\Heroicon;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class PlanTierResource extends Resource
|
|
{
|
|
protected static ?string $model = PlanTier::class;
|
|
|
|
protected static string|null|\BackedEnum $navigationIcon = Heroicon::OutlinedServerStack;
|
|
|
|
protected static string|null|\UnitEnum $navigationGroup = 'Subscription Management';
|
|
|
|
protected static ?int $navigationSort = 3;
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->schema([
|
|
Grid::make(2)->schema([
|
|
Forms\Components\TextInput::make('name')
|
|
->label('Tier Name')
|
|
->required()
|
|
->maxLength(255)
|
|
->helperText('e.g., Basic, Pro, Enterprise'),
|
|
|
|
Forms\Components\TextInput::make('sort_order')
|
|
->label('Sort Order')
|
|
->numeric()
|
|
->default(0)
|
|
->helperText('Display order in hierarchy'),
|
|
]),
|
|
|
|
Forms\Components\Textarea::make('description')
|
|
->label('Description')
|
|
->rows(3)
|
|
->maxLength(500)
|
|
->helperText('Describe this tier level'),
|
|
|
|
Forms\Components\Select::make('parent_tier_id')
|
|
->label('Parent Tier')
|
|
->relationship('parentTier', 'name')
|
|
->nullable()
|
|
->searchable()
|
|
->helperText('Optional parent tier for hierarchical structure'),
|
|
|
|
Forms\Components\KeyValue::make('metadata')
|
|
->label('Metadata')
|
|
->keyLabel('Key')
|
|
->valueLabel('Value')
|
|
->reorderable()
|
|
->addable()
|
|
->deletable()
|
|
->helperText('Additional tier metadata'),
|
|
]);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('name')
|
|
->label('Tier Name')
|
|
->searchable()
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('parentTier.name')
|
|
->label('Parent Tier')
|
|
->badge()
|
|
->color('info')
|
|
->placeholder('Root Tier')
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('description')
|
|
->label('Description')
|
|
->limit(50)
|
|
->searchable(),
|
|
|
|
Tables\Columns\TextColumn::make('sort_order')
|
|
->label('Order')
|
|
->sortable()
|
|
->alignCenter(),
|
|
|
|
Tables\Columns\TextColumn::make('plans_count')
|
|
->label('Plans')
|
|
->counts('plans')
|
|
->badge()
|
|
->color('primary')
|
|
->sortable(false),
|
|
])
|
|
->filters([
|
|
Tables\Filters\SelectFilter::make('parent_tier_id')
|
|
->label('Parent Tier')
|
|
->relationship('parentTier', 'name')
|
|
->searchable()
|
|
->placeholder('All tiers'),
|
|
|
|
Tables\Filters\Filter::make('has_parent')
|
|
->label('Has Parent')
|
|
->query(fn (Builder $query): Builder => $query->whereNotNull('parent_tier_id'))
|
|
->toggle(),
|
|
|
|
Tables\Filters\Filter::make('is_root')
|
|
->label('Root Tiers Only')
|
|
->query(fn (Builder $query): Builder => $query->whereNull('parent_tier_id'))
|
|
->toggle(),
|
|
])
|
|
->recordActions([
|
|
EditAction::make(),
|
|
DeleteAction::make()
|
|
->before(function (PlanTier $record) {
|
|
// Prevent deletion if tier has child tiers or plans
|
|
if ($record->childTiers()->exists()) {
|
|
Log::error('Cannot delete tier that has child tiers');
|
|
}
|
|
if ($record->plans()->exists()) {
|
|
Log::error('Cannot delete tier that has plans assigned');
|
|
}
|
|
}),
|
|
])
|
|
->toolbarActions([
|
|
BulkActionGroup::make([
|
|
DeleteBulkAction::make()
|
|
->before(function ($records) {
|
|
foreach ($records as $record) {
|
|
if ($record->childTiers()->exists()) {
|
|
Log::error('Cannot delete tiers that have child tiers');
|
|
}
|
|
if ($record->plans()->exists()) {
|
|
Log::error('Cannot delete tiers that have plans assigned');
|
|
}
|
|
}
|
|
}),
|
|
]),
|
|
])
|
|
->emptyStateActions([
|
|
CreateAction::make(),
|
|
])
|
|
->defaultSort('sort_order', 'asc');
|
|
}
|
|
|
|
public static function getRelations(): array
|
|
{
|
|
return [
|
|
//
|
|
];
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => ListPlanTiers::route('/'),
|
|
'create' => CreatePlanTier::route('/create'),
|
|
'edit' => EditPlanTier::route('/{record}/edit'),
|
|
];
|
|
}
|
|
|
|
public static function getNavigationBadge(): ?string
|
|
{
|
|
return static::getModel()::count();
|
|
}
|
|
|
|
public static function getNavigationBadgeColor(): ?string
|
|
{
|
|
return 'info';
|
|
}
|
|
}
|