From 8fa389f51dfb01522ec96a779400494b52c361e5e6979d85b95413075d4262ce Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Sat, 14 Mar 2026 23:34:28 -0400 Subject: [PATCH] feat: add MigrateVpsPlans command for customer migration Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/Console/Commands/MigrateVpsPlans.php | 100 ++++++++++++++++++ website/tests/Feature/MigrateVpsPlansTest.php | 89 ++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 website/app/Console/Commands/MigrateVpsPlans.php create mode 100644 website/tests/Feature/MigrateVpsPlansTest.php diff --git a/website/app/Console/Commands/MigrateVpsPlans.php b/website/app/Console/Commands/MigrateVpsPlans.php new file mode 100644 index 0000000..9d36ea6 --- /dev/null +++ b/website/app/Console/Commands/MigrateVpsPlans.php @@ -0,0 +1,100 @@ + 'vps-1', + 'vps-micro' => 'vps-2', + 'vps-mini' => 'vps-2', + 'vps-standard' => 'vps-8', + 'vps-plus' => 'vps-16', + 'vps-pro' => 'vps-32', + 'vps-storage-500' => 'stor-500', + 'vps-storage-1tb' => 'stor-1tb', + ]; + + public function handle(): int + { + $dryRun = (bool) $this->option('dry-run'); + + if ($dryRun) { + $this->info('DRY RUN — no changes will be applied.'); + } + + $this->info('Migrating VPS subscriptions and services to new plan IDs...'); + + $migrated = 0; + $skipped = 0; + + foreach (self::MIGRATION_MAP as $oldSlug => $newSlug) { + $oldPlan = Plan::where('slug', $oldSlug)->first(); + $newPlan = Plan::where('slug', $newSlug)->first(); + + if (! $oldPlan || ! $newPlan) { + $this->warn("Skipping {$oldSlug} → {$newSlug}: one or both plans not found."); + $skipped++; + + continue; + } + + // Migrate subscriptions + $subscriptions = Subscription::query()->where('plan_id', $oldPlan->id)->get(); + + foreach ($subscriptions as $subscription) { + $this->line(" Subscription #{$subscription->id} (user {$subscription->user_id}): {$oldSlug} → {$newSlug}"); + + if (! $dryRun) { + $subscription->update(['plan_id' => $newPlan->id]); + + Log::info('Migrated subscription to new plan', [ + 'subscription_id' => $subscription->id, + 'user_id' => $subscription->user_id, + 'old_plan' => $oldSlug, + 'new_plan' => $newSlug, + ]); + } + + $migrated++; + } + + // Migrate services + $services = Service::query()->where('plan_id', $oldPlan->id)->get(); + + foreach ($services as $service) { + $this->line(" Service #{$service->id}: {$oldSlug} → {$newSlug}"); + + if (! $dryRun) { + $service->update(['plan_id' => $newPlan->id]); + } + + $migrated++; + } + } + + $this->newLine(); + $label = $dryRun ? 'would be migrated' : 'migrated'; + $this->info("Migration complete: {$migrated} records {$label}, {$skipped} plan mappings skipped."); + + return self::SUCCESS; + } +} diff --git a/website/tests/Feature/MigrateVpsPlansTest.php b/website/tests/Feature/MigrateVpsPlansTest.php new file mode 100644 index 0000000..84dffe7 --- /dev/null +++ b/website/tests/Feature/MigrateVpsPlansTest.php @@ -0,0 +1,89 @@ +seed(\Database\Seeders\PlanSeeder::class); + + $oldPlan = Plan::create([ + 'name' => 'VPS Nano', + 'slug' => 'vps-nano', + 'description' => 'Old nano plan', + 'service_type' => 'vps', + 'price' => 3.50, + 'billing_cycle' => 'monthly', + 'features' => [], + 'sort_order' => 99, + ]); + + $newPlan = Plan::where('slug', 'vps-1')->first(); + + if (! $newPlan) { + $this->markTestSkipped('New plan vps-1 not found.'); + } + + $user = User::factory()->create(); + + DB::table('subscriptions')->insert([ + 'user_id' => $user->id, + 'type' => 'vps-nano', + 'stripe_id' => 'sub_test_'.uniqid(), + 'stripe_status' => 'active', + 'plan_id' => $oldPlan->id, + 'billing_cycle' => 'monthly', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->artisan('plans:migrate-vps') + ->assertSuccessful(); + + $subscription = DB::table('subscriptions')->where('user_id', $user->id)->first(); + expect($subscription->plan_id)->toBe($newPlan->id); +}); + +it('dry run does not modify data', function () { + $this->seed(\Database\Seeders\PlanSeeder::class); + + $oldPlan = Plan::create([ + 'name' => 'VPS Nano', + 'slug' => 'vps-nano', + 'description' => 'Old nano plan', + 'service_type' => 'vps', + 'price' => 3.50, + 'billing_cycle' => 'monthly', + 'features' => [], + 'sort_order' => 99, + ]); + + $user = User::factory()->create(); + + DB::table('subscriptions')->insert([ + 'user_id' => $user->id, + 'type' => 'vps-nano', + 'stripe_id' => 'sub_dry_'.uniqid(), + 'stripe_status' => 'active', + 'plan_id' => $oldPlan->id, + 'billing_cycle' => 'monthly', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->artisan('plans:migrate-vps', ['--dry-run' => true]) + ->assertSuccessful(); + + $subscription = DB::table('subscriptions')->where('user_id', $user->id)->first(); + expect($subscription->plan_id)->toBe($oldPlan->id); +}); + +it('skips when old plan does not exist', function () { + $this->seed(\Database\Seeders\PlanSeeder::class); + + // No old plans created, so all mappings should be skipped + $this->artisan('plans:migrate-vps') + ->assertSuccessful(); +});