- 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
140 lines
3.1 KiB
PHP
140 lines
3.1 KiB
PHP
<?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);
|
|
}
|
|
}
|