- 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
341 lines
8.9 KiB
PHP
341 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
|
|
class Plan extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'name',
|
|
'description',
|
|
'product_id',
|
|
'pricing_id',
|
|
'shoppy_product_id',
|
|
'accept_stripe',
|
|
'accept_shoppy',
|
|
'oxapay_link',
|
|
'accept_oxapay',
|
|
'price',
|
|
'mailbox_limit',
|
|
'monthly_billing',
|
|
'details',
|
|
// New fields for enhanced plan system
|
|
'plan_tier_id',
|
|
'billing_cycle_days',
|
|
'is_active',
|
|
'sort_order',
|
|
'metadata',
|
|
];
|
|
|
|
protected $casts = [
|
|
'details' => 'json',
|
|
'monthly_billing' => 'boolean',
|
|
'accept_stripe' => 'boolean',
|
|
'accept_shoppy' => 'boolean',
|
|
'accept_oxapay' => 'boolean',
|
|
'is_active' => 'boolean',
|
|
'metadata' => 'array',
|
|
];
|
|
|
|
/**
|
|
* Get the plan tier that this plan belongs to
|
|
*/
|
|
public function planTier(): BelongsTo
|
|
{
|
|
return $this->belongsTo(PlanTier::class);
|
|
}
|
|
|
|
/**
|
|
* Get feature limits for this plan
|
|
*/
|
|
public function planFeatureLimits(): HasMany
|
|
{
|
|
return $this->hasMany(PlanFeatureLimit::class);
|
|
}
|
|
|
|
/**
|
|
* Get permissions for this plan
|
|
*/
|
|
public function planPermissions(): HasMany
|
|
{
|
|
return $this->hasMany(PlanPermission::class);
|
|
}
|
|
|
|
/**
|
|
* Get payment providers for this plan
|
|
*/
|
|
public function planProviders(): HasMany
|
|
{
|
|
return $this->hasMany(PlanProvider::class);
|
|
}
|
|
|
|
/**
|
|
* Get trial configuration for this plan
|
|
*/
|
|
public function trialConfiguration(): HasOne
|
|
{
|
|
return $this->hasOne(TrialConfiguration::class);
|
|
}
|
|
|
|
/**
|
|
* Get subscriptions for this plan
|
|
*/
|
|
public function subscriptions(): HasMany
|
|
{
|
|
return $this->hasMany(Subscription::class);
|
|
}
|
|
|
|
/**
|
|
* Get usage tracking for this plan
|
|
*/
|
|
public function planUsages(): HasMany
|
|
{
|
|
return $this->hasMany(PlanUsage::class);
|
|
}
|
|
|
|
/**
|
|
* Get features associated with this plan (through limits)
|
|
*/
|
|
public function features(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(PlanFeature::class, 'plan_feature_limits')
|
|
->withPivot(['limit_value', 'is_enabled', 'limit_type', 'applies_during_trial', 'trial_limit_value'])
|
|
->withTimestamps();
|
|
}
|
|
|
|
/**
|
|
* Scope: Active plans
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
/**
|
|
* Scope: Ordered by sort order
|
|
*/
|
|
public function scopeOrdered($query)
|
|
{
|
|
return $query->orderBy('sort_order')->orderBy('name');
|
|
}
|
|
|
|
/**
|
|
* Scope: By tier
|
|
*/
|
|
public function scopeByTier($query, $tierId)
|
|
{
|
|
return $query->where('plan_tier_id', $tierId);
|
|
}
|
|
|
|
/**
|
|
* Check if plan has a specific feature enabled
|
|
*/
|
|
public function hasFeature(string $featureName): bool
|
|
{
|
|
$featureLimit = $this->planFeatureLimits()
|
|
->whereHas('planFeature', function ($query) use ($featureName) {
|
|
$query->where('name', $featureName);
|
|
})
|
|
->first();
|
|
|
|
return $featureLimit && $featureLimit->isEnabled();
|
|
}
|
|
|
|
/**
|
|
* Check if user can use a feature within limits
|
|
*/
|
|
public function canUseFeature(string $featureName, float $currentUsage = 0, bool $isOnTrial = false): bool
|
|
{
|
|
$featureLimit = $this->planFeatureLimits()
|
|
->whereHas('planFeature', function ($query) use ($featureName) {
|
|
$query->where('name', $featureName);
|
|
})
|
|
->first();
|
|
|
|
return $featureLimit ? $featureLimit->canUseFeature($currentUsage, $isOnTrial) : false;
|
|
}
|
|
|
|
/**
|
|
* Get remaining usage for a feature
|
|
*/
|
|
public function getRemainingUsage(string $featureName, float $currentUsage = 0, bool $isOnTrial = false): float
|
|
{
|
|
$featureLimit = $this->planFeatureLimits()
|
|
->whereHas('planFeature', function ($query) use ($featureName) {
|
|
$query->where('name', $featureName);
|
|
})
|
|
->first();
|
|
|
|
return $featureLimit ? $featureLimit->getRemainingUsage($currentUsage, $isOnTrial) : 0;
|
|
}
|
|
|
|
/**
|
|
* Check if plan has a specific permission
|
|
*/
|
|
public function hasPermission(string $featureName, string $permission, bool $isOnTrial = false): bool
|
|
{
|
|
$planPermission = $this->planPermissions()
|
|
->whereHas('planFeature', function ($query) use ($featureName) {
|
|
$query->where('name', $featureName);
|
|
})
|
|
->where('permission', $permission)
|
|
->first();
|
|
|
|
return $planPermission ? $planPermission->isEffectivePermission($isOnTrial) : false;
|
|
}
|
|
|
|
/**
|
|
* Get allowed payment providers for this plan
|
|
*/
|
|
public function getAllowedProviders(): array
|
|
{
|
|
return $this->planProviders()
|
|
->enabled()
|
|
->orderBy('sort_order')
|
|
->pluck('provider')
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Check if plan supports a specific payment provider
|
|
*/
|
|
public function supportsProvider(string $provider): bool
|
|
{
|
|
return $this->planProviders()
|
|
->where('provider', $provider)
|
|
->where('is_enabled', true)
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* Get provider configuration for a specific provider
|
|
*/
|
|
public function getProviderConfig(string $provider): ?PlanProvider
|
|
{
|
|
return $this->planProviders()
|
|
->where('provider', $provider)
|
|
->where('is_enabled', true)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Check if plan has trial enabled
|
|
*/
|
|
public function hasTrial(): bool
|
|
{
|
|
return $this->trialConfiguration && $this->trialConfiguration->trial_enabled;
|
|
}
|
|
|
|
/**
|
|
* Get trial configuration
|
|
*/
|
|
public function getTrialConfig(): ?TrialConfiguration
|
|
{
|
|
return $this->trialConfiguration?->trial_enabled ? $this->trialConfiguration : null;
|
|
}
|
|
|
|
/**
|
|
* Get billing cycle in human readable format
|
|
*/
|
|
public function getBillingCycleDisplay(): string
|
|
{
|
|
if ($this->billing_cycle_days) {
|
|
if ($this->billing_cycle_days == 30) {
|
|
return 'Monthly';
|
|
}
|
|
if ($this->billing_cycle_days == 90) {
|
|
return 'Quarterly';
|
|
}
|
|
if ($this->billing_cycle_days == 365) {
|
|
return 'Yearly';
|
|
}
|
|
|
|
return "{$this->billing_cycle_days} days";
|
|
}
|
|
|
|
return $this->monthly_billing ? 'Monthly' : 'Yearly';
|
|
}
|
|
|
|
/**
|
|
* Get plan metadata value
|
|
*/
|
|
public function getMetadata(?string $key = null, $default = null)
|
|
{
|
|
if ($key) {
|
|
return data_get($this->metadata, $key, $default);
|
|
}
|
|
|
|
return $this->metadata;
|
|
}
|
|
|
|
/**
|
|
* Set plan metadata value
|
|
*/
|
|
public function setMetadata(string $key, $value): void
|
|
{
|
|
$data = $this->metadata ?? [];
|
|
data_set($data, $key, $value);
|
|
$this->metadata = $data;
|
|
}
|
|
|
|
/**
|
|
* Get all features with their limits and permissions
|
|
*/
|
|
public function getFeaturesWithLimits(): array
|
|
{
|
|
return $this->planFeatureLimits()
|
|
->with('planFeature')
|
|
->get()
|
|
->map(function ($limit) {
|
|
return [
|
|
'feature' => $limit->planFeature,
|
|
'limit' => $limit,
|
|
'permissions' => $this->planPermissions()
|
|
->where('plan_feature_id', $limit->plan_feature_id)
|
|
->get(),
|
|
];
|
|
})
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Check if this plan is an upgrade from another plan
|
|
*/
|
|
public function isUpgradeFrom(Plan $otherPlan): bool
|
|
{
|
|
// Simple logic: check if this plan has higher tier or price
|
|
if ($this->planTier_id && $otherPlan->planTier_id) {
|
|
return $this->planTier->sort_order > $otherPlan->planTier->sort_order;
|
|
}
|
|
|
|
return $this->price > $otherPlan->price;
|
|
}
|
|
|
|
/**
|
|
* Get upgrade path to this plan
|
|
*/
|
|
public function getUpgradePath(): array
|
|
{
|
|
// Return plans that can be upgraded to this plan
|
|
return Plan::where('id', '!=', $this->id)
|
|
->where(function ($query) {
|
|
$query->where('price', '<', $this->price)
|
|
->orWhereHas('planTier', function ($q) {
|
|
$q->where('sort_order', '<', $this->planTier?->sort_order ?? 0);
|
|
});
|
|
})
|
|
->active()
|
|
->ordered()
|
|
->get()
|
|
->toArray();
|
|
}
|
|
}
|