feat: implement comprehensive enhanced plan management system

- 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
This commit is contained in:
idevakk
2025-11-21 07:59:21 -08:00
parent 5f5da23a40
commit b497f7796d
27 changed files with 2664 additions and 76 deletions

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PlanFeatureLimit extends Model
{
use HasFactory;
protected $fillable = [
'plan_id',
'plan_feature_id',
'limit_value',
'is_enabled',
'limit_type',
'metadata',
'applies_during_trial',
'trial_limit_value',
];
protected $casts = [
'limit_value' => 'decimal:2',
'trial_limit_value' => 'decimal:2',
'is_enabled' => 'boolean',
'applies_during_trial' => 'boolean',
'metadata' => 'array',
];
/**
* Limit types
*/
const LIMIT_MONTHLY = 'monthly';
const LIMIT_DAILY = 'daily';
const LIMIT_TOTAL = 'total';
/**
* Get the plan that owns this feature limit
*/
public function plan(): BelongsTo
{
return $this->belongsTo(Plan::class);
}
/**
* Get the feature that this limit applies to
*/
public function planFeature(): BelongsTo
{
return $this->belongsTo(PlanFeature::class);
}
/**
* Scope: Enabled limits
*/
public function scopeEnabled($query)
{
return $query->where('is_enabled', true);
}
/**
* Scope: By limit type
*/
public function scopeByType($query, string $type)
{
return $query->where('limit_type', $type);
}
/**
* Check if feature is enabled for this plan
*/
public function isEnabled(): bool
{
return $this->is_enabled;
}
/**
* Get the effective limit value (considering trial status)
*/
public function getEffectiveLimit(bool $isOnTrial = false): ?float
{
if ($isOnTrial && $this->applies_during_trial && $this->trial_limit_value !== null) {
return (float) $this->trial_limit_value;
}
return $this->limit_value ? (float) $this->limit_value : null;
}
/**
* Check if user can use this feature within limits
*/
public function canUseFeature(float $currentUsage = 0, bool $isOnTrial = false): bool
{
if (! $this->isEnabled()) {
return false;
}
$limit = $this->getEffectiveLimit($isOnTrial);
// If limit is null, feature is unlimited
if ($limit === null) {
return true;
}
return $currentUsage < $limit;
}
/**
* Get remaining usage allowance
*/
public function getRemainingUsage(float $currentUsage = 0, bool $isOnTrial = false): float
{
$limit = $this->getEffectiveLimit($isOnTrial);
if ($limit === null) {
return INF;
}
return max(0, $limit - $currentUsage);
}
/**
* Get percentage of limit used
*/
public function getUsagePercentage(float $currentUsage = 0, bool $isOnTrial = false): float
{
$limit = $this->getEffectiveLimit($isOnTrial);
if ($limit === null || $limit == 0) {
return 0;
}
return min(100, ($currentUsage / $limit) * 100);
}
}