Idempotent provisioning, service soft-delete, Plans page redesign, doc updates

Part A: Fix duplicate Service creation on provisioning retry
- All 4 provisioning services use Service::firstOrCreate() keyed on
  subscription_id+service_type to prevent duplicates on queue retries
- HandleSubscriptionCreated sends notification before provisioning,
  no longer re-throws on failure
- RetryProvisioningCommand simplified to reuse existing Service records

Part B: Plans/Pricing page complete redesign
- Service type tabs (VPS, Dedicated, Web Hosting, MySQL)
- Billing cycle segmented toggle (monthly/quarterly/semi-annual/annual)
- Feature icons per service type, Popular/Best Value badges
- Stock indicators, effective monthly price calculations

Part C: Admin service soft-delete/archive
- Service model uses SoftDeletes trait
- Admin can archive and restore services
- Show archived toggle on services list
- Migration adds deleted_at column

Docs: Updated TASKS.md, CLAUDE.md, PROJECT_DEVELOPMENT.md, MEMORY.md
- Phase 3 marked complete, test counts updated (252 passing)
- SupportPal references replaced with standalone ticket system
- Frontend design skill background rule added
- Closed GitHub issues #3, #6, #7, #8, #9

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-10 06:30:57 -05:00
parent bf4f5f97c0
commit 45d25d61ba
101 changed files with 13225 additions and 1888 deletions

View File

@@ -42,39 +42,39 @@ class DemoDataSeeder extends Seeder
$adminUser = User::role('admin')->first();
$adminId = $adminUser?->id ?? 1;
// ─── 1. Create ~300 Customers ─────────────────────────────────
// ─── 1. Create 1000 Customers ─────────────────────────────────
$this->command->info('Creating customers...');
$customers = $this->createCustomers();
// ─── 2. Create ~500 Subscriptions ────────────────────────────
// ─── 2. Create ~1500 Subscriptions ────────────────────────────
$this->command->info('Creating subscriptions...');
$subscriptionMap = $this->createSubscriptions($customers, $plans);
// ─── 3. Create ~400 Services ──────────────────────────────────
// ─── 3. Create ~1200 Services (70% VPS) ───────────────────────
$this->command->info('Creating services...');
$this->createServices($customers, $plans, $subscriptionMap);
// ─── 4. Create ~800 Invoices with Items ──────────────────────
// ─── 4. Create ~2000 Invoices with Items ──────────────────────
$this->command->info('Creating invoices...');
$invoiceIds = $this->createInvoices($customers, $plans, $subscriptionMap);
// ─── 5. Create ~600 Payment Transactions ─────────────────────
// ─── 5. Create ~1500 Payment Transactions ─────────────────────
$this->command->info('Creating payment transactions...');
$this->createPaymentTransactions($customers, $invoiceIds);
// ─── 6. Create ~150 Orders ────────────────────────────────────
// ─── 6. Create ~400 Orders ────────────────────────────────────
$this->command->info('Creating orders...');
$this->createOrders($customers, $plans);
// ─── 7. Create ~200 Support Tickets with Replies ──────────────
// ─── 7. Create ~500 Support Tickets with Replies ──────────────
$this->command->info('Creating support tickets...');
$this->createSupportTickets($customers, $adminId);
// ─── 8. Create ~50 Coupons ───────────────────────────────────
// ─── 8. Create ~100 Coupons ───────────────────────────────────
$this->command->info('Creating coupons...');
$this->createCoupons();
// ─── 9. Create ~100 Audit Logs ────────────────────────────────
// ─── 9. Create ~300 Audit Logs ────────────────────────────────
$this->command->info('Creating audit logs...');
$this->createAuditLogs($customers, $adminId);
@@ -82,7 +82,7 @@ class DemoDataSeeder extends Seeder
}
/**
* Create ~300 customers with profiles.
* Create 1000 customers with profiles.
*
* @return \Illuminate\Support\Collection<int, User>
*/
@@ -90,16 +90,16 @@ class DemoDataSeeder extends Seeder
{
$customers = collect();
$statuses = array_merge(
array_fill(0, 270, 'active'),
array_fill(0, 15, 'suspended'),
array_fill(0, 10, 'banned'),
array_fill(0, 5, 'pending'),
array_fill(0, 900, 'active'),
array_fill(0, 50, 'suspended'),
array_fill(0, 30, 'banned'),
array_fill(0, 20, 'pending'),
);
shuffle($statuses);
$faker = fake();
$batchSize = 50;
$totalCustomers = 300;
$batchSize = 100;
$totalCustomers = 1000;
for ($i = 0; $i < $totalCustomers; $i += $batchSize) {
$batchCount = min($batchSize, $totalCustomers - $i);
@@ -138,7 +138,7 @@ class DemoDataSeeder extends Seeder
}
/**
* Create ~500 subscriptions linked to real plans.
* Create ~1500 subscriptions linked to real plans (70% VPS).
*
* Returns a map of subscription_id => [user_id, plan_id] for use by other seeders.
*
@@ -150,12 +150,19 @@ class DemoDataSeeder extends Seeder
{
$subscriptionMap = [];
$statuses = ['active', 'active', 'active', 'active', 'active', 'active', 'active', 'canceled', 'past_due', 'trialing'];
$total = 500;
$total = 1500;
// Separate plans by type for weighted distribution
$vpsPlans = $plans->where('service_type', 'vps');
$otherPlans = $plans->whereNotIn('service_type', ['vps']);
$rows = [];
for ($i = 0; $i < $total; $i++) {
$customer = $customers->random();
$plan = $plans->random();
// 70% VPS, 30% other services
$plan = (rand(1, 100) <= 70 && $vpsPlans->isNotEmpty())
? $vpsPlans->random()
: ($otherPlans->isNotEmpty() ? $otherPlans->random() : $plans->random());
$status = $statuses[array_rand($statuses)];
$createdAt = $customer->created_at->copy()->addDays(rand(0, 60));
@@ -243,21 +250,28 @@ class DemoDataSeeder extends Seeder
];
$serviceStatuses = array_merge(
array_fill(0, 300, 'active'),
array_fill(0, 40, 'suspended'),
array_fill(0, 30, 'pending'),
array_fill(0, 30, 'terminated'),
array_fill(0, 1000, 'active'),
array_fill(0, 100, 'suspended'),
array_fill(0, 50, 'pending'),
array_fill(0, 50, 'terminated'),
);
shuffle($serviceStatuses);
$subIds = array_keys($subscriptionMap);
$rows = [];
$faker = fake();
$total = 400;
$total = 1200;
// Separate plans by type for weighted distribution
$vpsPlans = $plans->where('service_type', 'vps');
$otherPlans = $plans->whereNotIn('service_type', ['vps']);
for ($i = 0; $i < $total; $i++) {
$customer = $customers->random();
$plan = $plans->random();
// 70% VPS, 30% other services
$plan = (rand(1, 100) <= 70 && $vpsPlans->isNotEmpty())
? $vpsPlans->random()
: ($otherPlans->isNotEmpty() ? $otherPlans->random() : $plans->random());
$status = $serviceStatuses[$i] ?? 'active';
$serviceType = $plan->service_type;
$platform = $platformMap[$serviceType] ?? 'virtfusion';
@@ -310,7 +324,7 @@ class DemoDataSeeder extends Seeder
}
/**
* Create ~800 invoices with line items.
* Create ~2000 invoices with line items.
*
* @param \Illuminate\Support\Collection<int, User> $customers
* @param \Illuminate\Support\Collection<int, Plan> $plans
@@ -320,10 +334,10 @@ class DemoDataSeeder extends Seeder
private function createInvoices(\Illuminate\Support\Collection $customers, \Illuminate\Support\Collection $plans, array $subscriptionMap): array
{
$invoiceStatuses = array_merge(
array_fill(0, 500, 'paid'),
array_fill(0, 150, 'pending'),
array_fill(0, 100, 'overdue'),
array_fill(0, 50, 'void'),
array_fill(0, 1400, 'paid'),
array_fill(0, 300, 'pending'),
array_fill(0, 200, 'overdue'),
array_fill(0, 100, 'void'),
);
shuffle($invoiceStatuses);
@@ -331,7 +345,7 @@ class DemoDataSeeder extends Seeder
$invoiceRows = [];
$invoiceTracker = [];
$faker = fake();
$total = 800;
$total = 2000;
for ($i = 0; $i < $total; $i++) {
$customer = $customers->random();
@@ -449,17 +463,17 @@ class DemoDataSeeder extends Seeder
{
$rows = [];
$faker = fake();
$total = 600;
$total = 1500;
// Use paid/pending invoices for payment transactions
$paidInvoices = array_filter($invoiceData, fn ($inv) => $inv['status'] === 'paid');
$paidInvoices = array_values($paidInvoices);
$transactionStatuses = array_merge(
array_fill(0, 480, 'succeeded'),
array_fill(0, 60, 'failed'),
array_fill(0, 40, 'refunded'),
array_fill(0, 20, 'pending'),
array_fill(0, 1200, 'succeeded'),
array_fill(0, 150, 'failed'),
array_fill(0, 100, 'refunded'),
array_fill(0, 50, 'pending'),
);
shuffle($transactionStatuses);
@@ -522,16 +536,16 @@ class DemoDataSeeder extends Seeder
private function createOrders(\Illuminate\Support\Collection $customers, \Illuminate\Support\Collection $plans): void
{
$orderStatuses = array_merge(
array_fill(0, 80, 'completed'),
array_fill(0, 30, 'pending'),
array_fill(0, 25, 'processing'),
array_fill(0, 15, 'cancelled'),
array_fill(0, 250, 'completed'),
array_fill(0, 70, 'pending'),
array_fill(0, 50, 'processing'),
array_fill(0, 30, 'cancelled'),
);
shuffle($orderStatuses);
$rows = [];
$faker = fake();
$total = 150;
$total = 400;
for ($i = 0; $i < $total; $i++) {
$customer = $customers->random();
@@ -610,15 +624,15 @@ class DemoDataSeeder extends Seeder
];
$ticketStatuses = array_merge(
array_fill(0, 60, 'open'),
array_fill(0, 50, 'in_progress'),
array_fill(0, 40, 'waiting'),
array_fill(0, 50, 'closed'),
array_fill(0, 150, 'open'),
array_fill(0, 125, 'in_progress'),
array_fill(0, 100, 'waiting'),
array_fill(0, 125, 'closed'),
);
shuffle($ticketStatuses);
$priorities = ['low', 'low', 'medium', 'medium', 'medium', 'high', 'high', 'urgent'];
$total = 200;
$total = 500;
$ticketRows = [];
$ticketMeta = [];
@@ -746,7 +760,7 @@ class DemoDataSeeder extends Seeder
'PARTNER', 'AGENCY', 'RESELLER', 'BULK', 'ENTERPRISE',
];
$total = 50;
$total = 100;
for ($i = 0; $i < $total; $i++) {
$type = $faker->randomElement(['percentage', 'percentage', 'fixed_amount']);
@@ -820,7 +834,7 @@ class DemoDataSeeder extends Seeder
$faker = fake();
$rows = [];
$total = 100;
$total = 300;
$userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',

View File

@@ -11,181 +11,193 @@ class PlanSeeder extends Seeder
{
public function run(): void
{
Plan::query()->delete();
// Archive old VPS plans instead of deleting (preserves foreign key relationships)
Plan::query()
->where('service_type', 'vps')
->whereNotIn('slug', [
'vps-nano', 'vps-micro', 'vps-mini', 'vps-standard',
'vps-plus', 'vps-pro', 'vps-storage-500', 'vps-storage-1tb',
])
->update(['status' => 'archived']);
$plans = [
// ─── VPS Plans ───────────────────────────────────────────────
// ─── VPS Plans (2026 NVMe Lineup) ────────────────────────────
[
'name' => 'Micro VPS',
'slug' => 'micro-vps',
'description' => 'Lightweight VPS for simple tasks, testing, and small projects.',
'name' => 'Nano',
'slug' => 'vps-nano',
'description' => 'Entry-level NVMe VPS for simple tasks, testing, and lightweight applications.',
'service_type' => 'vps',
'price' => 4.20,
'price' => 3.50,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '1 vCPU',
'ram' => '1 GB',
'storage' => '25 GB SSD',
'storage' => '15 GB NVMe',
'bandwidth' => '2 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 1,
],
[
'name' => 'Mini VPS',
'slug' => 'mini-vps',
'description' => 'Compact VPS with extra memory for light workloads.',
'name' => 'Micro',
'slug' => 'vps-micro',
'description' => 'NVMe VPS with 2 GB RAM - double the RAM of competitors at the same price point.',
'service_type' => 'vps',
'price' => 6.00,
'price' => 5.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '1 vCPU',
'ram' => '2 GB',
'storage' => '50 GB SSD',
'bandwidth' => '4 TB',
'storage' => '30 GB NVMe',
'bandwidth' => '3 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 2,
],
[
'name' => 'Dev Starter',
'slug' => 'dev-starter',
'description' => 'Dual-core VPS ideal for development environments and staging.',
'name' => 'Mini',
'slug' => 'vps-mini',
'description' => 'Hero plan with 4 GB RAM and NVMe storage - beats Hetzner CX22 with faster disks.',
'service_type' => 'vps',
'price' => 8.00,
'price' => 8.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '2 vCPU',
'ram' => '2 GB',
'storage' => '60 GB SSD',
'ram' => '4 GB',
'storage' => '50 GB NVMe',
'bandwidth' => '4 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 3,
],
[
'name' => 'Basic VPS',
'slug' => 'basic-vps',
'description' => 'Balanced VPS for web apps, databases, and general-purpose workloads.',
'name' => 'Standard',
'slug' => 'vps-standard',
'description' => 'Premium 8 GB RAM VPS with NVMe - double the RAM of competitors at half the price.',
'service_type' => 'vps',
'price' => 12.00,
'price' => 14.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '2 vCPU',
'ram' => '4 GB',
'storage' => '80 GB SSD',
'ram' => '8 GB',
'storage' => '80 GB NVMe',
'bandwidth' => '6 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 4,
],
[
'name' => 'Storage Box',
'slug' => 'storage-box',
'description' => 'High-storage VPS for backups, media, and file-heavy applications.',
'name' => 'Plus',
'slug' => 'vps-plus',
'description' => 'High-RAM VPS with 12 GB memory and quad-core CPU for demanding applications.',
'service_type' => 'vps',
'price' => 15.00,
'price' => 22.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '4 vCPU',
'ram' => '12 GB',
'storage' => '120 GB NVMe',
'bandwidth' => '8 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 5,
],
[
'name' => 'Pro',
'slug' => 'vps-pro',
'description' => 'Ultimate RAM VPS with 16 GB memory and NVMe for production workloads.',
'service_type' => 'vps',
'price' => 29.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '4 vCPU',
'ram' => '16 GB',
'storage' => '160 GB NVMe',
'bandwidth' => '10 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 6,
],
[
'name' => 'Storage-500',
'slug' => 'vps-storage-500',
'description' => 'Storage-focused VPS with 500 GB SATA SSD for backups, media, and file storage.',
'service_type' => 'vps',
'price' => 24.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '2 vCPU',
'ram' => '2 GB',
'ram' => '4 GB',
'storage' => '500 GB SSD',
'bandwidth' => '8 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
],
'sort_order' => 5,
],
[
'name' => 'Standard VPS',
'slug' => 'standard-vps',
'description' => 'Quad-core VPS with 8 GB RAM for production applications.',
'service_type' => 'vps',
'price' => 15.60,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '4 vCPU',
'ram' => '8 GB',
'storage' => '160 GB SSD',
'bandwidth' => '8 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
],
'sort_order' => 6,
],
[
'name' => 'RAM Optimized',
'slug' => 'ram-optimized',
'description' => 'Memory-optimized VPS for databases, caching, and in-memory workloads.',
'service_type' => 'vps',
'price' => 19.00,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '4 vCPU',
'ram' => '16 GB',
'storage' => '240 GB SSD',
'bandwidth' => '10 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 7,
],
[
'name' => 'Advanced VPS',
'slug' => 'advanced-vps',
'description' => 'Six-core VPS with 16 GB RAM for demanding applications and multi-service setups.',
'name' => 'Storage-1TB',
'slug' => 'vps-storage-1tb',
'description' => 'Mass storage VPS with 1 TB SATA SSD for large-scale file storage and archives.',
'service_type' => 'vps',
'price' => 21.60,
'price' => 44.95,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '6 vCPU',
'ram' => '16 GB',
'storage' => '320 GB SSD',
'bandwidth' => '10 TB',
'cpu' => '4 vCPU',
'ram' => '8 GB',
'storage' => '1 TB SSD',
'bandwidth' => '12 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
'virtfusion_package_id' => 1,
'virtfusion_user_id' => 1,
'virtfusion_hypervisor_id' => 1,
],
'sort_order' => 8,
],
[
'name' => 'Pro VPS',
'slug' => 'pro-vps',
'description' => 'Eight-core powerhouse with 32 GB RAM for enterprise workloads and heavy traffic.',
'service_type' => 'vps',
'price' => 30.00,
'billing_cycle' => 'monthly',
'features' => [
'cpu' => '8 vCPU',
'ram' => '32 GB',
'storage' => '640 GB SSD',
'bandwidth' => '16 TB',
'ipv4' => '1 IPv4',
'ipv6' => '1 /64 IPv6',
'control_panel' => 'VirtFusion',
'os' => 'Linux & Windows (BYOL)',
],
'sort_order' => 9,
],
// ─── Dedicated Server Plans ──────────────────────────────────
[

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Plan;
use Illuminate\Database\Seeder;
class UpdateVirtFusionPackageIds extends Seeder
{
public function run(): void
{
// Map Laravel plans to VirtFusion package IDs
$mapping = [
'Nano' => 43, // Base Package (will use custom specs)
'Micro' => 19, // Micro
'Mini' => 20, // Mini
'Standard' => 22, // Standard
'Plus' => 23, // Advanced
'Pro' => 24, // Pro
'Storage-500' => 41, // Storage Box
'Storage-1TB' => 41, // Storage Box
];
foreach ($mapping as $planName => $packageId) {
$plan = Plan::where('name', $planName)->where('service_type', 'vps')->first();
if ($plan) {
$features = $plan->features ?? [];
$features['virtfusion_package_id'] = $packageId;
$plan->update(['features' => $features]);
$this->command->info("Updated {$planName} with VirtFusion package ID {$packageId}");
}
}
}
}