feat: complete pre-launch audit — frontend polish, churn prevention, login history, financial reports, configurable checkout

Includes all work from phases 6-9+ and frontend polish rounds 1 & 2:

- Login history with device trust, new device notifications, session management
- Churn prevention: cancellation surveys, winback campaigns with email sequences
- Financial reports: revenue, P&L, tax, aging, refund, subscription reports with PDF/CSV/JSON export
- Configurable checkout: plan config groups/options, build-your-own VPS
- Frontend polish: fix broken legal links, add SEO meta tags, favicon, font display=swap,
  Head titles on all 14 marketing pages, mobile responsive fixes, AuthLayout legal footer,
  remove false 24/7 claims, hide empty stats, correct uptime SLA to 99.9%,
  GameServers notify buttons linked to /contact, 301 redirects for /terms and /privacy
- WHMCS migration scripts
- Update legal page effective dates to March 16, 2026

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-03-16 11:39:25 -04:00
parent 5be235d35e
commit b4ef90465c
187 changed files with 27317 additions and 1840 deletions

View File

@@ -0,0 +1,473 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Plan;
use App\Models\PlanConfigGroup;
use App\Models\PlanConfigOption;
use App\Models\PlanConfigValue;
use Illuminate\Database\Seeder;
class ConfigOptionSeeder extends Seeder
{
public function run(): void
{
$this->seedBuildYourOwnGroups();
$this->seedPresetGroups();
}
private function seedBuildYourOwnGroups(): void
{
// ─── VPS Builder ────────────────────────────────────────────────
$vpsBuilder = PlanConfigGroup::updateOrCreate(
['name' => 'VPS Builder'],
[
'description' => 'Build your own custom VPS with the exact resources you need.',
'mode' => 'build_your_own',
'service_type' => 'vps',
'is_active' => true,
'sort_order' => 1,
],
);
$this->seedSliderOption($vpsBuilder, 'CPU Cores', 'cpu_cores', 1, 16, 1, 'cores', 0.003, 2.00, 1);
$this->seedSliderOption($vpsBuilder, 'RAM', 'ram_gb', 1, 64, 1, 'GB', 0.0015, 1.00, 2);
$this->seedSliderOption($vpsBuilder, 'SSD Storage', 'disk_gb', 25, 1000, 25, 'GB', 0.0001, 0.05, 3);
// ─── MySQL Builder ──────────────────────────────────────────────
$mysqlBuilder = PlanConfigGroup::updateOrCreate(
['name' => 'MySQL Builder'],
[
'description' => 'Build your own managed MySQL instance.',
'mode' => 'build_your_own',
'service_type' => 'mysql',
'is_active' => true,
'sort_order' => 2,
],
);
$this->seedSliderOption($mysqlBuilder, 'Storage', 'storage_gb', 5, 500, 5, 'GB', 0.0003, 0.20, 1);
$this->seedSliderOption($mysqlBuilder, 'Max Connections', 'max_connections', 50, 1000, 50, 'connections', 0.0001, 0.05, 2);
// ─── Game Server Builder ────────────────────────────────────────
$gameBuilder = PlanConfigGroup::updateOrCreate(
['name' => 'Game Server Builder'],
[
'description' => 'Build your own custom game server.',
'mode' => 'build_your_own',
'service_type' => 'game',
'is_active' => true,
'sort_order' => 3,
],
);
$this->seedSliderOption($gameBuilder, 'RAM', 'ram_gb', 1, 16, 1, 'GB', 0.002, 1.50, 1);
$this->seedSliderOption($gameBuilder, 'Storage', 'disk_gb', 10, 200, 10, 'GB', 0.0001, 0.08, 2);
$this->seedSliderOption($gameBuilder, 'Player Slots', 'player_slots', 10, 200, 10, 'slots', 0.0001, 0.05, 3);
}
private function seedPresetGroups(): void
{
// ─── Server Management (all dedicated + VPS plans) ──────────────
$serverMgmt = PlanConfigGroup::updateOrCreate(
['name' => 'Server Management'],
[
'description' => 'Add managed support to your server.',
'mode' => 'preset',
'service_type' => null,
'is_active' => true,
'sort_order' => 10,
],
);
$mgmtOption = $this->seedDropdownOption($serverMgmt, 'Server Management', null, false, 1);
$this->seedValues($mgmtOption, [
['label' => 'None', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => 'Semi-Managed', 'value' => 'semi_managed', 'monthly' => 25.00],
['label' => 'Fully Managed', 'value' => 'fully_managed', 'monthly' => 60.00],
]);
// Attach to all active dedicated AND vps plans
$dedicatedAndVpsPlans = Plan::query()
->whereIn('service_type', ['dedicated', 'vps'])
->whereIn('status', ['active', 'internal'])
->pluck('id');
$serverMgmt->plans()->syncWithoutDetaching($dedicatedAndVpsPlans);
// ─── VPS Add-ons ────────────────────────────────────────────────
$vpsAddons = PlanConfigGroup::updateOrCreate(
['name' => 'VPS Add-ons'],
[
'description' => 'Additional options for your VPS.',
'mode' => 'preset',
'service_type' => 'vps',
'is_active' => true,
'sort_order' => 11,
],
);
// IPv4 Addresses (quantity)
$ipv4Option = $this->seedQuantityOption($vpsAddons, 'IPv4 Addresses', 1, 8, 'addresses', 3.00, 1);
// Windows License (checkbox)
$winOption = $this->seedCheckboxOption($vpsAddons, 'Windows License', 2);
$this->seedValues($winOption, [
['label' => 'Yes (Free BYOL)', 'value' => 'yes', 'monthly' => 0, 'is_default' => false],
]);
// Attach to all active VPS plans
$vpsSlugs = ['vps-1', 'vps-2', 'vps-3-custom', 'vps-4', 'vps-8', 'vps-16', 'vps-32', 'stor-500', 'stor-1tb'];
$vpsPlans = Plan::query()->whereIn('slug', $vpsSlugs)->pluck('id');
$vpsAddons->plans()->syncWithoutDetaching($vpsPlans);
// ─── Dedicated RAM - DDR4 ──────────────────────────────────────
$ddr4Ram = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated RAM - DDR4'],
[
'description' => 'Choose the DDR4 ECC RAM configuration for your dedicated server.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 20,
],
);
$ramOption = $this->seedDropdownOption($ddr4Ram, 'DDR4 ECC RAM', null, true, 1);
$this->seedValues($ramOption, [
['label' => '32 GB', 'value' => '32', 'monthly' => 0, 'is_default' => true],
['label' => '64 GB', 'value' => '64', 'monthly' => 15.00],
['label' => '96 GB', 'value' => '96', 'monthly' => 20.00],
['label' => '128 GB', 'value' => '128', 'monthly' => 28.00],
['label' => '192 GB', 'value' => '192', 'monthly' => 40.00],
['label' => '256 GB', 'value' => '256', 'monthly' => 55.00],
['label' => '384 GB', 'value' => '384', 'monthly' => 80.00],
['label' => '512 GB', 'value' => '512', 'monthly' => 120.00],
]);
$ramPlanSlugs = ['dell-r440', 'dell-r640', 'dell-r540', 'dell-r740'];
$ramPlans = Plan::query()->whereIn('slug', $ramPlanSlugs)->pluck('id');
$ddr4Ram->plans()->syncWithoutDetaching($ramPlans);
// ─── Dedicated Network ──────────────────────────────────────────
$dedicatedNetwork = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated Network'],
[
'description' => 'Network configuration for your dedicated server.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 21,
],
);
// Network Port Speed (radio, required)
$portSpeedOption = $this->seedRadioOption($dedicatedNetwork, 'Network Port Speed', true, 1);
$this->seedValues($portSpeedOption, [
['label' => 'Unmetered 100Mbit', 'value' => '100mbit_unmetered', 'monthly' => 0, 'is_default' => true],
['label' => 'Metered 1Gbit', 'value' => '1gbit_metered', 'monthly' => 0],
['label' => 'Unmetered 2Gbit', 'value' => '2gbit_unmetered', 'monthly' => 150.00],
['label' => 'Unmetered 5Gbit', 'value' => '5gbit_unmetered', 'monthly' => 325.00],
['label' => 'Unmetered 10Gbit', 'value' => '10gbit_unmetered', 'monthly' => 650.00],
]);
// Public Bandwidth (dropdown)
$publicBwOption = $this->seedDropdownOption($dedicatedNetwork, 'Public Bandwidth', null, false, 2);
$this->seedValues($publicBwOption, [
['label' => '20TB', 'value' => '20tb', 'monthly' => 0, 'is_default' => true],
['label' => '50TB', 'value' => '50tb', 'monthly' => 45.00],
['label' => '100TB', 'value' => '100tb', 'monthly' => 100.00],
['label' => 'Unmetered 1Gbps', 'value' => 'unmetered_1gbps', 'monthly' => 150.00],
]);
// Private Bandwidth (radio)
$privateBwOption = $this->seedRadioOption($dedicatedNetwork, 'Private Bandwidth', false, 3);
$this->seedValues($privateBwOption, [
['label' => '1Gbit', 'value' => '1gbit', 'monthly' => 0, 'is_default' => true],
['label' => '10Gbit', 'value' => '10gbit', 'monthly' => 85.00],
]);
$allDedicatedPlans = Plan::query()
->where('service_type', 'dedicated')
->whereIn('status', ['active', 'internal'])
->pluck('id');
$dedicatedNetwork->plans()->syncWithoutDetaching($allDedicatedPlans);
// ─── M.2 NVMe ──────────────────────────────────────────────────
$nvme = PlanConfigGroup::updateOrCreate(
['name' => 'M.2 NVMe'],
[
'description' => 'Add M.2 NVMe storage to your dedicated server.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 23,
],
);
$nvmeOption = $this->seedDropdownOption($nvme, 'M.2 NVMe', null, false, 1);
$this->seedValues($nvmeOption, [
['label' => 'None', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => '2x 500GB', 'value' => '2x500gb', 'monthly' => 18.00],
['label' => '2x 1TB', 'value' => '2x1tb', 'monthly' => 30.00],
['label' => '2x 2TB', 'value' => '2x2tb', 'monthly' => 75.00],
]);
$nvme->plans()->syncWithoutDetaching($ramPlans);
// ─── Veeam Enterprise Plus Options ─────────────────────────────
$veeamEntPlus = PlanConfigGroup::updateOrCreate(
['name' => 'Veeam Enterprise Plus Options'],
[
'description' => 'Configure your Veeam Enterprise Plus backup solution.',
'mode' => 'preset',
'service_type' => 'backups',
'is_active' => true,
'sort_order' => 30,
],
);
$this->seedQuantityOption($veeamEntPlus, 'Virtual Machines', 0, 100, 'VMs', 15.00, 1);
$this->seedQuantityOption($veeamEntPlus, 'Server Agents', 0, 50, 'agents', 15.00, 2);
$this->seedQuantityOption($veeamEntPlus, 'Workstation Agents', 0, 100, 'agents', 6.00, 3);
$this->seedQuantityOption($veeamEntPlus, 'NAS (per 500GB)', 0, 50, 'units', 14.00, 4);
$this->seedQuantityOption($veeamEntPlus, 'Office 365 Users', 0, 500, 'users', 3.00, 5);
$veeamEntPlusPlan = Plan::query()->where('slug', 'veeam-enterprise-plus')->pluck('id');
$veeamEntPlus->plans()->syncWithoutDetaching($veeamEntPlusPlan);
// ─── Veeam Standard Options ────────────────────────────────────
$veeamStandard = PlanConfigGroup::updateOrCreate(
['name' => 'Veeam Standard Options'],
[
'description' => 'Configure your Veeam Standard backup solution.',
'mode' => 'preset',
'service_type' => 'backups',
'is_active' => true,
'sort_order' => 31,
],
);
$this->seedQuantityOption($veeamStandard, 'Virtual Machines', 0, 100, 'VMs', 7.00, 1);
$this->seedQuantityOption($veeamStandard, 'Server Agents', 0, 50, 'agents', 15.00, 2);
$this->seedQuantityOption($veeamStandard, 'Workstation Agents', 0, 100, 'agents', 6.00, 3);
$this->seedQuantityOption($veeamStandard, 'NAS (per 500GB)', 0, 50, 'units', 14.00, 4);
$this->seedQuantityOption($veeamStandard, 'Office 365 Users', 0, 500, 'users', 3.00, 5);
$veeamStandardPlan = Plan::query()->where('slug', 'veeam-standard')->pluck('id');
$veeamStandard->plans()->syncWithoutDetaching($veeamStandardPlan);
// ─── Veeam Cloud Connect Options ───────────────────────────────
$veeamCloudConnect = PlanConfigGroup::updateOrCreate(
['name' => 'Veeam Cloud Connect Options'],
[
'description' => 'Configure your Veeam Cloud Connect backup and replication solution.',
'mode' => 'preset',
'service_type' => 'backups',
'is_active' => true,
'sort_order' => 32,
],
);
$this->seedQuantityOption($veeamCloudConnect, 'Virtual Machines', 0, 100, 'VMs', 15.00, 1);
$this->seedQuantityOption($veeamCloudConnect, 'Server Agents', 0, 50, 'agents', 15.00, 2);
$this->seedQuantityOption($veeamCloudConnect, 'Workstation Agents', 0, 100, 'agents', 6.00, 3);
$this->seedQuantityOption($veeamCloudConnect, 'NAS (per 500GB)', 0, 50, 'units', 14.00, 4);
$this->seedQuantityOption($veeamCloudConnect, 'Office 365 Users', 0, 500, 'users', 3.00, 5);
$this->seedQuantityOption($veeamCloudConnect, 'Capacity Storage', 1, 100, 'TB', 4.00, 6, required: true);
$this->seedQuantityOption($veeamCloudConnect, 'Replicated VMs', 0, 50, 'VMs', 30.00, 7);
$supportOption = $this->seedDropdownOption($veeamCloudConnect, 'Support Level', null, true, 8);
$this->seedValues($supportOption, [
['label' => 'Basic', 'value' => 'basic', 'monthly' => 30.00, 'is_default' => true],
['label' => 'Enhanced', 'value' => 'enhanced', 'monthly' => 60.00],
['label' => 'Enhanced Plus', 'value' => 'enhanced_plus', 'monthly' => 200.00],
]);
$veeamCloudConnectPlan = Plan::query()->where('slug', 'veeam-cloud-connect')->pluck('id');
$veeamCloudConnect->plans()->syncWithoutDetaching($veeamCloudConnectPlan);
}
/**
* Create/update a slider option for BYO groups.
*/
private function seedSliderOption(
PlanConfigGroup $group,
string $name,
string $provisioningKey,
int $min,
int $max,
int $step,
string $unit,
float $hourly,
float $monthly,
int $sortOrder,
): PlanConfigOption {
return PlanConfigOption::updateOrCreate(
['group_id' => $group->id, 'name' => $name],
[
'type' => 'slider',
'provisioning_key' => $provisioningKey,
'required' => true,
'is_active' => true,
'min_qty' => $min,
'max_qty' => $max,
'step' => $step,
'unit_label' => $unit,
'hourly_price' => $hourly,
'monthly_price' => $monthly,
'quarterly_price' => $this->quarterlyPrice($monthly),
'semi_annual_price' => $this->semiAnnualPrice($monthly),
'annual_price' => $this->annualPrice($monthly),
'sort_order' => $sortOrder,
],
);
}
/**
* Create/update a dropdown option.
*/
private function seedDropdownOption(
PlanConfigGroup $group,
string $name,
?string $provisioningKey,
bool $required,
int $sortOrder,
): PlanConfigOption {
return PlanConfigOption::updateOrCreate(
['group_id' => $group->id, 'name' => $name],
[
'type' => 'dropdown',
'provisioning_key' => $provisioningKey,
'required' => $required,
'is_active' => true,
'sort_order' => $sortOrder,
],
);
}
/**
* Create/update a radio option.
*/
private function seedRadioOption(
PlanConfigGroup $group,
string $name,
bool $required,
int $sortOrder,
): PlanConfigOption {
return PlanConfigOption::updateOrCreate(
['group_id' => $group->id, 'name' => $name],
[
'type' => 'radio',
'provisioning_key' => null,
'required' => $required,
'is_active' => true,
'sort_order' => $sortOrder,
],
);
}
/**
* Create/update a checkbox option.
*/
private function seedCheckboxOption(
PlanConfigGroup $group,
string $name,
int $sortOrder,
): PlanConfigOption {
return PlanConfigOption::updateOrCreate(
['group_id' => $group->id, 'name' => $name],
[
'type' => 'checkbox',
'provisioning_key' => null,
'required' => false,
'is_active' => true,
'sort_order' => $sortOrder,
],
);
}
/**
* Create/update a quantity option.
*/
private function seedQuantityOption(
PlanConfigGroup $group,
string $name,
int $min,
int $max,
string $unit,
float $monthly,
int $sortOrder,
bool $required = false,
): PlanConfigOption {
return PlanConfigOption::updateOrCreate(
['group_id' => $group->id, 'name' => $name],
[
'type' => 'quantity',
'provisioning_key' => null,
'required' => $required,
'is_active' => true,
'min_qty' => $min,
'max_qty' => $max,
'step' => 1,
'unit_label' => $unit,
'monthly_price' => $monthly,
'quarterly_price' => $this->quarterlyPrice($monthly),
'semi_annual_price' => $this->semiAnnualPrice($monthly),
'annual_price' => $this->annualPrice($monthly),
'sort_order' => $sortOrder,
],
);
}
/**
* Seed values for a dropdown/radio/checkbox option.
*
* @param array<int, array{label: string, value: string, monthly: float, is_default?: bool}> $values
*/
private function seedValues(PlanConfigOption $option, array $values): void
{
foreach ($values as $index => $data) {
$monthly = $data['monthly'];
PlanConfigValue::updateOrCreate(
['option_id' => $option->id, 'label' => $data['label']],
[
'value' => $data['value'],
'hourly_price' => 0,
'monthly_price' => $monthly,
'quarterly_price' => $this->quarterlyPrice($monthly),
'semi_annual_price' => $this->semiAnnualPrice($monthly),
'annual_price' => $this->annualPrice($monthly),
'is_default' => $data['is_default'] ?? false,
'sort_order' => $index,
],
);
}
}
/**
* quarterly = monthly * 3 * 0.95
*/
private function quarterlyPrice(float $monthly): float
{
return round($monthly * 3 * 0.95, 2);
}
/**
* semi_annual = monthly * 6 * 0.90
*/
private function semiAnnualPrice(float $monthly): float
{
return round($monthly * 6 * 0.90, 2);
}
/**
* annual = monthly * 12 * 0.85
*/
private function annualPrice(float $monthly): float
{
return round($monthly * 12 * 0.85, 2);
}
}