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:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plan_features', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // e.g., 'email_forwarding', 'auto_reply_rules'
|
||||
$table->string('display_name'); // e.g., 'Email Forwarding', 'Auto-Reply Rules'
|
||||
$table->text('description')->nullable();
|
||||
$table->string('category'); // e.g., 'core', 'advanced', 'premium'
|
||||
$table->string('type'); // e.g., 'boolean', 'numeric', 'toggle'
|
||||
$table->json('metadata')->nullable(); // Additional feature-specific data
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name']);
|
||||
$table->index(['category', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plan_features');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plan_feature_limits', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('plan_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('plan_feature_id')->constrained()->onDelete('cascade');
|
||||
$table->decimal('limit_value', 10, 2)->nullable(); // Numeric limit (e.g., 1000 emails)
|
||||
$table->boolean('is_enabled')->default(true); // Feature is enabled/disabled
|
||||
$table->string('limit_type')->default('monthly'); // monthly, daily, total
|
||||
$table->json('metadata')->nullable(); // Additional limit-specific data
|
||||
$table->boolean('applies_during_trial')->default(true); // Whether limit applies during trial
|
||||
$table->decimal('trial_limit_value', 10, 2)->nullable(); // Different limit for trial period
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['plan_id', 'plan_feature_id']);
|
||||
$table->index(['plan_id', 'is_enabled']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plan_feature_limits');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plan_permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('plan_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('plan_feature_id')->constrained()->onDelete('cascade');
|
||||
$table->string('permission'); // e.g., 'can_export', 'can_use_api'
|
||||
$table->boolean('is_granted')->default(true);
|
||||
$table->json('conditions')->nullable(); // Additional conditions for permission
|
||||
$table->boolean('applies_during_trial')->default(true);
|
||||
$table->boolean('trial_permission_override')->nullable(); // Different trial permission
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['plan_id', 'plan_feature_id', 'permission']);
|
||||
$table->index(['plan_id', 'is_granted']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plan_permissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plan_providers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('plan_id')->constrained()->onDelete('cascade');
|
||||
$table->string('provider'); // stripe, lemon_squeezy, polar, etc.
|
||||
$table->string('provider_price_id')->nullable(); // Price ID from provider
|
||||
$table->string('provider_variant_id')->nullable(); // Variant ID from provider
|
||||
$table->string('provider_product_id')->nullable(); // Product ID from provider
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
$table->decimal('price', 10, 2)->nullable();
|
||||
$table->string('currency', 3)->default('USD');
|
||||
$table->json('provider_data')->nullable(); // Provider-specific configuration
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['plan_id', 'provider']);
|
||||
$table->index(['plan_id', 'is_enabled']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plan_providers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plan_tiers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // Basic, Pro, Enterprise
|
||||
$table->text('description')->nullable();
|
||||
$table->foreignId('parent_tier_id')->nullable()->constrained('plan_tiers')->onDelete('set null');
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['parent_tier_id']);
|
||||
$table->index(['sort_order']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plan_tiers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plan_usages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('plan_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('plan_feature_id')->constrained()->onDelete('cascade');
|
||||
$table->decimal('usage_amount', 10, 2)->default(0);
|
||||
$table->string('usage_type')->default('monthly'); // monthly, daily, total
|
||||
$table->date('period_start'); // Usage period start date
|
||||
$table->date('period_end'); // Usage period end date
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'plan_feature_id', 'usage_type', 'period_start']);
|
||||
$table->index(['user_id', 'period_start']);
|
||||
$table->index(['plan_feature_id', 'period_start']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('plan_usages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('trial_configurations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('plan_id')->constrained()->onDelete('cascade');
|
||||
$table->boolean('trial_enabled')->default(false);
|
||||
$table->integer('trial_duration_days')->default(14);
|
||||
$table->boolean('trial_requires_payment_method')->default(true);
|
||||
$table->boolean('trial_auto_converts')->default(true);
|
||||
$table->string('trial_conversion_action')->default('upgrade_to_paid'); // upgrade_to_paid, cancel, notify
|
||||
$table->integer('trial_extension_limit')->default(0); // Max extensions allowed
|
||||
$table->json('trial_feature_overrides')->nullable(); // Features to limit during trial
|
||||
$table->text('trial_welcome_message')->nullable();
|
||||
$table->text('trial_expiry_message')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['plan_id']);
|
||||
$table->index(['trial_enabled']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('trial_configurations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('plans', function (Blueprint $table) {
|
||||
$table->foreignId('plan_tier_id')->nullable()->after('details')->constrained()->onDelete('set null');
|
||||
$table->integer('billing_cycle_days')->nullable()->after('plan_tier_id'); // 30, 90, 365, custom
|
||||
$table->boolean('is_active')->default(true)->after('billing_cycle_days');
|
||||
$table->integer('sort_order')->default(0)->after('is_active');
|
||||
$table->json('metadata')->nullable()->after('sort_order');
|
||||
|
||||
$table->index(['is_active', 'sort_order']);
|
||||
$table->index(['plan_tier_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('plans', function (Blueprint $table) {
|
||||
$table->dropForeign(['plan_tier_id']);
|
||||
$table->dropColumn([
|
||||
'plan_tier_id',
|
||||
'billing_cycle_days',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
'metadata',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Create default plan features for legacy functionality
|
||||
$this->createDefaultPlanFeatures();
|
||||
|
||||
// Migrate existing plans to new system
|
||||
$this->migrateExistingPlans();
|
||||
|
||||
// Update legacy plan structure
|
||||
$this->updateLegacyPlanStructure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default plan features that match legacy functionality
|
||||
*/
|
||||
private function createDefaultPlanFeatures(): void
|
||||
{
|
||||
$features = [
|
||||
[
|
||||
'name' => 'mailbox_accounts',
|
||||
'display_name' => 'Mailbox Accounts',
|
||||
'description' => 'Number of mailbox accounts allowed',
|
||||
'category' => 'core',
|
||||
'type' => 'numeric',
|
||||
'is_active' => true,
|
||||
'sort_order' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'advanced_filters',
|
||||
'display_name' => 'Advanced Filters',
|
||||
'description' => 'Advanced email filtering and rules',
|
||||
'category' => 'advanced',
|
||||
'type' => 'boolean',
|
||||
'is_active' => true,
|
||||
'sort_order' => 10,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'api_access',
|
||||
'display_name' => 'API Access',
|
||||
'description' => 'Access to developer API',
|
||||
'category' => 'advanced',
|
||||
'type' => 'boolean',
|
||||
'is_active' => true,
|
||||
'sort_order' => 11,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'priority_support',
|
||||
'display_name' => 'Priority Support',
|
||||
'description' => 'Priority customer support',
|
||||
'category' => 'premium',
|
||||
'type' => 'boolean',
|
||||
'is_active' => true,
|
||||
'sort_order' => 20,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
];
|
||||
|
||||
// Use insertOrIgnore to avoid conflicts with existing features
|
||||
foreach ($features as $feature) {
|
||||
DB::table('plan_features')->insertOrIgnore([$feature]);
|
||||
}
|
||||
|
||||
// Ensure email_forwarding exists with correct sort order
|
||||
DB::table('plan_features')
|
||||
->where('name', 'email_forwarding')
|
||||
->update([
|
||||
'sort_order' => 2,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing plans to use the new feature limits system
|
||||
*/
|
||||
private function migrateExistingPlans(): void
|
||||
{
|
||||
$plans = DB::table('plans')->get();
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
// Get feature IDs for mapping
|
||||
$mailboxFeature = DB::table('plan_features')->where('name', 'mailbox_accounts')->first();
|
||||
$emailForwardingFeature = DB::table('plan_features')->where('name', 'email_forwarding')->first();
|
||||
$advancedFiltersFeature = DB::table('plan_features')->where('name', 'advanced_filters')->first();
|
||||
$apiAccessFeature = DB::table('plan_features')->where('name', 'api_access')->first();
|
||||
$prioritySupportFeature = DB::table('plan_features')->where('name', 'priority_support')->first();
|
||||
|
||||
// Create feature limits for each plan (only if they don't exist)
|
||||
$existingLimitIds = DB::table('plan_feature_limits')
|
||||
->where('plan_id', $plan->id)
|
||||
->pluck('plan_feature_id')
|
||||
->toArray();
|
||||
|
||||
$featureLimits = [];
|
||||
|
||||
// Mailbox limit (from legacy mailbox_limit) - only if not already exists
|
||||
if ($mailboxFeature && ! in_array($mailboxFeature->id, $existingLimitIds)) {
|
||||
$featureLimits[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'plan_feature_id' => $mailboxFeature->id,
|
||||
'limit_type' => 'total',
|
||||
'limit_value' => $plan->mailbox_limit,
|
||||
'is_enabled' => true,
|
||||
'applies_during_trial' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// Email forwarding (enabled for all plans) - only if not already exists
|
||||
if ($emailForwardingFeature && ! in_array($emailForwardingFeature->id, $existingLimitIds)) {
|
||||
$featureLimits[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'plan_feature_id' => $emailForwardingFeature->id,
|
||||
'limit_type' => 'boolean',
|
||||
'limit_value' => 1, // true
|
||||
'is_enabled' => true,
|
||||
'applies_during_trial' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// Advanced features based on plan tier/price
|
||||
$isPremiumPlan = $plan->price >= 20;
|
||||
|
||||
if ($advancedFiltersFeature && ! in_array($advancedFiltersFeature->id, $existingLimitIds)) {
|
||||
$featureLimits[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'plan_feature_id' => $advancedFiltersFeature->id,
|
||||
'limit_type' => 'boolean',
|
||||
'limit_value' => $isPremiumPlan ? 1 : 0,
|
||||
'is_enabled' => true,
|
||||
'applies_during_trial' => false, // Not available during trial for advanced features
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($apiAccessFeature && ! in_array($apiAccessFeature->id, $existingLimitIds)) {
|
||||
$featureLimits[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'plan_feature_id' => $apiAccessFeature->id,
|
||||
'limit_type' => 'boolean',
|
||||
'limit_value' => $isPremiumPlan ? 1 : 0,
|
||||
'is_enabled' => true,
|
||||
'applies_during_trial' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($prioritySupportFeature && ! in_array($prioritySupportFeature->id, $existingLimitIds)) {
|
||||
$featureLimits[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'plan_feature_id' => $prioritySupportFeature->id,
|
||||
'limit_type' => 'boolean',
|
||||
'limit_value' => $plan->price >= 25 ? 1 : 0,
|
||||
'is_enabled' => true,
|
||||
'applies_during_trial' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// Insert only new feature limits
|
||||
if (! empty($featureLimits)) {
|
||||
DB::table('plan_feature_limits')->insert($featureLimits);
|
||||
}
|
||||
|
||||
// Create plan provider entries based on legacy accept_* columns
|
||||
$this->createPlanProviders($plan);
|
||||
|
||||
// Create trial configuration (disabled for legacy plans)
|
||||
$this->createTrialConfiguration($plan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create plan provider entries based on legacy provider acceptance columns
|
||||
*/
|
||||
private function createPlanProviders($plan): void
|
||||
{
|
||||
// Check if providers already exist for this plan
|
||||
$existingProviders = DB::table('plan_providers')
|
||||
->where('plan_id', $plan->id)
|
||||
->exists();
|
||||
|
||||
if ($existingProviders) {
|
||||
return; // Skip if providers already exist
|
||||
}
|
||||
|
||||
$providers = [];
|
||||
|
||||
// Stripe
|
||||
if ($plan->accept_stripe) {
|
||||
$providers[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'provider' => 'stripe',
|
||||
'provider_price_id' => $plan->pricing_id,
|
||||
'price' => $plan->price,
|
||||
'currency' => 'USD',
|
||||
'is_enabled' => true,
|
||||
'sort_order' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// OxaPay
|
||||
if ($plan->accept_oxapay && $plan->oxapay_link) {
|
||||
$providers[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'provider' => 'oxapay',
|
||||
'provider_price_id' => $plan->oxapay_link,
|
||||
'price' => $plan->price,
|
||||
'currency' => 'USD',
|
||||
'is_enabled' => true,
|
||||
'sort_order' => 5,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// Shoppy (if it exists)
|
||||
if ($plan->accept_shoppy && $plan->shoppy_product_id) {
|
||||
$providers[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'provider' => 'shoppy',
|
||||
'provider_price_id' => $plan->shoppy_product_id,
|
||||
'price' => $plan->price,
|
||||
'currency' => 'USD',
|
||||
'is_enabled' => true,
|
||||
'sort_order' => 3,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// If no providers were explicitly enabled, enable Stripe by default
|
||||
if (empty($providers) && $plan->pricing_id) {
|
||||
$providers[] = [
|
||||
'plan_id' => $plan->id,
|
||||
'provider' => 'stripe',
|
||||
'provider_price_id' => $plan->pricing_id,
|
||||
'price' => $plan->price,
|
||||
'currency' => 'USD',
|
||||
'is_enabled' => true,
|
||||
'sort_order' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($providers)) {
|
||||
DB::table('plan_providers')->insert($providers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create trial configuration for legacy plans (disabled by default)
|
||||
*/
|
||||
private function createTrialConfiguration($plan): void
|
||||
{
|
||||
// Check if trial configuration already exists
|
||||
$existingTrial = DB::table('trial_configurations')
|
||||
->where('plan_id', $plan->id)
|
||||
->exists();
|
||||
|
||||
if ($existingTrial) {
|
||||
return; // Skip if trial config already exists
|
||||
}
|
||||
|
||||
DB::table('trial_configurations')->insert([
|
||||
'plan_id' => $plan->id,
|
||||
'trial_enabled' => false, // Disabled for legacy plans
|
||||
'trial_duration_days' => 14,
|
||||
'trial_requires_payment_method' => true,
|
||||
'trial_auto_converts' => true,
|
||||
'trial_extension_limit' => 0,
|
||||
'trial_conversion_action' => 'upgrade_to_paid',
|
||||
'trial_welcome_message' => null,
|
||||
'trial_expiry_message' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update legacy plan structure - mark old columns as deprecated but keep them for rollback
|
||||
*/
|
||||
private function updateLegacyPlanStructure(): void
|
||||
{
|
||||
// Note: We're keeping the legacy columns for now to ensure rollback capability
|
||||
// In a future migration, we can remove these columns:
|
||||
// - mailbox_limit (replaced by plan_feature_limits)
|
||||
// - monthly_billing (replaced by billing_cycle_days)
|
||||
// - accept_stripe, accept_shoppy, accept_oxapay (replaced by plan_providers)
|
||||
// - shoppy_product_id, oxapay_link (moved to plan_providers)
|
||||
|
||||
// For now, just ensure billing_cycle_days is properly set based on monthly_billing
|
||||
DB::statement('
|
||||
UPDATE plans
|
||||
SET billing_cycle_days = CASE
|
||||
WHEN monthly_billing = 1 THEN 30
|
||||
ELSE 365
|
||||
END
|
||||
WHERE billing_cycle_days IS NULL OR billing_cycle_days = 0
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Remove created plan features (except email_forwarding which existed before)
|
||||
DB::table('plan_features')->whereIn('name', [
|
||||
'mailbox_accounts',
|
||||
'advanced_filters',
|
||||
'api_access',
|
||||
'priority_support',
|
||||
])->delete();
|
||||
|
||||
// Remove feature limits, providers, and trial configurations for migrated plans
|
||||
$planIds = DB::table('plans')->pluck('id');
|
||||
|
||||
DB::table('plan_feature_limits')->whereIn('plan_id', $planIds)->delete();
|
||||
DB::table('plan_providers')->whereIn('plan_id', $planIds)->delete();
|
||||
DB::table('trial_configurations')->whereIn('plan_id', $planIds)->delete();
|
||||
|
||||
// Restore mailbox_limit from feature limits if possible
|
||||
$mailboxFeature = DB::table('plan_features')->where('name', 'mailbox_accounts')->first();
|
||||
if ($mailboxFeature) {
|
||||
$limits = DB::table('plan_feature_limits')
|
||||
->where('plan_feature_id', $mailboxFeature->id)
|
||||
->get();
|
||||
|
||||
foreach ($limits as $limit) {
|
||||
DB::table('plans')
|
||||
->where('id', $limit->plan_id)
|
||||
->update(['mailbox_limit' => $limit->limit_value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user