Files
website/website/database/seeders/ConfigOptionSeeder.php
Andrew 61afa4ed14 feat(dedicated): drive bay configurator + sticky-summary fix + reword setup fee
Three changes bundled:

1. Drive bay configurator (3 new ConfigOptionSeeder groups):
   - LFF Drive Bays (3.5") — 7 starter combos from "None" to
     12× 8 TB SATA HDD; attached to R440 / R540 / R740xd LFF
   - SFF Drive Bays (2.5") — 8 starter combos from "None" to
     24× 1.92 TB SATA SSD; attached to R640 / R740 / R740xd SFF
   - NVMe Drive Bays (U.2) — 7 starter combos from "None" to
     16× 2 TB U.2 NVMe; attached to R640 NVMe / R740xd NVMe
   Combos enforce chassis bay-count constraints via labels
   ("R740xd LFF only"); customers wanting heterogeneous setups
   use the post-order ticket flow.

2. Sticky build-summary fix: previously the BuildSummary card
   slid under the Vuetify navbar at scroll. Moved sticky from
   the inner card to the .detail-grid__summary wrapper, removed
   align-items: start so the right grid cell stretches to the
   configurator column's height (giving sticky a tall enough
   container), and offset top by 80px (64px navbar + 16px
   breathing room). Mobile path drops sticky entirely.

3. Setup fee reword — "Hardware acquisition" was leaking our
   cost structure and making the fee feel like procurement
   passthrough. Now reads "Server provisioning & deployment"
   in BuildSummary, and the FAQ describes what the fee covers
   (build, racking, iDRAC config, deployment) without exposing
   margins. Same shift across the non-refundable note: "once
   your build starts" instead of "once hardware is purchased."

Detail page bay-strategy callout updated: drive selection IS now
self-serve, so the callout pivots to "need a custom drive layout?"
pointing customers with mixed-size / hot-spare / RAID-preference
needs to the contact form.

Tests: updated count assertion to 9 groups, added a new test
verifying drive bay groups attach to chassis by bay type.
22/22 of my session's Pest tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:25:33 -04:00

767 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 (dedicated only) ─────────────────────────
$serverMgmt = PlanConfigGroup::updateOrCreate(
['name' => 'Server Management'],
[
'description' => 'Add managed support to your server.',
'mode' => 'preset',
'service_type' => 'dedicated',
'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 dedicated plans only (VPS plans use the dedicated VPS Managed Support group below)
$dedicatedPlanIds = Plan::query()
->where('service_type', 'dedicated')
->whereIn('status', ['active', 'internal'])
->pluck('id');
$serverMgmt->plans()->sync($dedicatedPlanIds);
// ─── VPS Managed Support ────────────────────────────────────────
// 4-tier radio: Self ($0) / Basic ($29) / Pro ($79) / Pilot ($99, VPS-8+).
// Pilot tier gating (min plan tier 8) is enforced in the frontend.
$vpsManaged = PlanConfigGroup::updateOrCreate(
['name' => 'VPS Managed Support'],
[
'description' => 'Choose how hands-on you want our team to be with your VPS.',
'mode' => 'preset',
'service_type' => 'vps',
'is_active' => true,
'sort_order' => 10,
],
);
$vpsManagedOption = $this->seedRadioOption($vpsManaged, 'Managed Support', false, 1);
$this->seedValues($vpsManagedOption, [
['label' => 'Self-Managed', 'value' => 'self', 'monthly' => 0, 'is_default' => true],
['label' => 'Managed Basic', 'value' => 'basic', 'monthly' => 29.00],
['label' => 'Managed Pro', 'value' => 'pro', 'monthly' => 79.00],
['label' => 'Pilot', 'value' => 'pilot', 'monthly' => 99.00],
]);
$vpsPlanIds = Plan::query()
->where('service_type', 'vps')
->whereIn('status', ['active', 'internal'])
->pluck('id');
$vpsManaged->plans()->syncWithoutDetaching($vpsPlanIds);
// ─── Off-site Backup (VPS) ──────────────────────────────────────
// 5-tier radio: None / Lite (7d, 200GB, $5) / Standard (30d, 500GB, $12)
// / Extended (90d, 1TB, $25) / Vault (1yr, 2TB, $59).
// StorJ-backed, encrypted at rest. Restores included (no egress charge).
$vpsBackup = PlanConfigGroup::updateOrCreate(
['name' => 'Off-site Backup'],
[
'description' => 'Encrypted off-site backups stored on StorJ. Daily snapshots, free restores.',
'mode' => 'preset',
'service_type' => 'vps',
'is_active' => true,
'sort_order' => 12,
],
);
$vpsBackupOption = $this->seedRadioOption($vpsBackup, 'Backup Tier', false, 1);
$this->seedValues($vpsBackupOption, [
['label' => 'None', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => 'Lite (7-day, 200 GB)', 'value' => 'lite', 'monthly' => 5.00],
['label' => 'Standard (30-day, 500 GB)', 'value' => 'standard', 'monthly' => 12.00],
['label' => 'Extended (90-day, 1 TB)', 'value' => 'extended', 'monthly' => 25.00],
['label' => 'Vault (1-year, 2 TB)', 'value' => 'vault', 'monthly' => 59.00],
]);
$vpsBackup->plans()->syncWithoutDetaching($vpsPlanIds);
// ─── 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', 8.00, 1);
// Drop the legacy Windows License option if it exists (BYOL is now communicated
// via marketing copy + OS template selection at checkout).
PlanConfigOption::query()
->where('group_id', $vpsAddons->id)
->where('name', 'Windows License')
->delete();
// 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);
// ─── Dedicated 14th Gen — CPU Upgrade ───────────────────────────
$gen14CpuUpgrade = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — CPU Upgrade'],
[
'description' => 'Upgrade from the 2× Xeon Gold 6230 baseline to a higher core count or clock speed.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 40,
],
);
$gen14CpuOption = $this->seedRadioOption($gen14CpuUpgrade, 'CPU Upgrade', false, 1);
$this->seedValues($gen14CpuOption, [
['label' => 'Baseline: 2× Xeon Gold 6230 (40C / 2.10 GHz)', 'value' => 'gold-6230-baseline', 'monthly' => 0, 'is_default' => true],
// High-clock SKU for per-core licensed workloads (Microsoft SQL,
// Oracle, single-threaded compute). Fewer cores, higher base clock.
['label' => 'High-clock: 2× Xeon Gold 6244 (16C / 3.60 GHz)', 'value' => 'gold-6244', 'monthly' => 25.00],
['label' => 'Upgrade: 2× Xeon Gold 6248 (40C / 2.50 GHz)', 'value' => 'gold-6248', 'monthly' => 35.00],
// Cascade Lake Refresh of the 6230 — same gen, more cores at the
// same clock. Sweet-spot mid-tier between baseline and 6248R.
['label' => 'Mid-tier: 2× Xeon Gold 6230R (52C / 2.10 GHz)', 'value' => 'gold-6230r', 'monthly' => 50.00],
['label' => 'Upgrade: 2× Xeon Gold 6248R (48C / 3.00 GHz)', 'value' => 'gold-6248r', 'monthly' => 75.00],
['label' => 'Upgrade: 2× Xeon Gold 6258R (56C / 2.70 GHz)', 'value' => 'gold-6258r', 'monthly' => 145.00],
]);
$gen14CpuPlanSlugs = ['r440-4lff', 'r540-8lff', 'r640-8sff', 'r740-16sff'];
$gen14CpuPlans = Plan::query()->whereIn('slug', $gen14CpuPlanSlugs)->pluck('id');
$gen14CpuUpgrade->plans()->syncWithoutDetaching($gen14CpuPlans);
// ─── Dedicated 14th Gen — CPU Upgrade (R740xd) ──────────────────
$gen14CpuUpgradeXd = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — CPU Upgrade (R740xd)'],
[
'description' => 'R740xd-specific CPU options. Higher TDP support than non-xd variants.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 41,
],
);
$gen14CpuXdOption = $this->seedRadioOption($gen14CpuUpgradeXd, 'CPU Upgrade', false, 1);
$this->seedValues($gen14CpuXdOption, [
['label' => 'Baseline: 2× Xeon Gold 6230 (40C / 2.10 GHz)', 'value' => 'gold-6230-baseline', 'monthly' => 0, 'is_default' => true],
['label' => 'Upgrade: 2× Xeon Gold 6248R (48C / 3.00 GHz)', 'value' => 'gold-6248r', 'monthly' => 75.00],
['label' => 'Upgrade: 2× Xeon Gold 6258R (56C / 2.70 GHz)', 'value' => 'gold-6258r', 'monthly' => 145.00],
['label' => 'Upgrade: 2× Xeon Platinum 8280 (56C / 2.70 GHz)', 'value' => 'platinum-8280', 'monthly' => 285.00],
]);
$gen14CpuXdPlanSlugs = ['r740xd-24sff', 'r740xd-12lff', 'r740xd-24nvme'];
$gen14CpuXdPlans = Plan::query()->whereIn('slug', $gen14CpuXdPlanSlugs)->pluck('id');
$gen14CpuUpgradeXd->plans()->syncWithoutDetaching($gen14CpuXdPlans);
// ─── Dedicated 14th Gen — RAM Upgrade ───────────────────────────
$gen14Ram = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — RAM Upgrade'],
[
'description' => 'DDR4-2400 ECC. RDIMM up to 128 GB; LRDIMM at 256 GB and above.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 42,
],
);
$gen14RamOption = $this->seedRadioOption($gen14Ram, 'RAM Upgrade', false, 1);
$this->seedValues($gen14RamOption, [
['label' => '32 GB DDR4-2400 ECC RDIMM (baseline)', 'value' => '32', 'monthly' => 0, 'is_default' => true],
['label' => '64 GB DDR4-2400 ECC RDIMM', 'value' => '64', 'monthly' => 35.00],
['label' => '128 GB DDR4-2400 ECC RDIMM', 'value' => '128', 'monthly' => 90.00],
['label' => '256 GB DDR4-2400 ECC LRDIMM', 'value' => '256', 'monthly' => 195.00],
['label' => '512 GB DDR4-2400 ECC LRDIMM', 'value' => '512', 'monthly' => 380.00],
['label' => '1 TB DDR4-2400 ECC LRDIMM', 'value' => '1024', 'monthly' => 580.00],
['label' => '1.5 TB DDR4-2400 ECC LRDIMM (R640/R740/R740xd only)', 'value' => '1536', 'monthly' => 780.00],
]);
$gen14AllPlanSlugs = ['r440-4lff', 'r540-8lff', 'r640-8sff', 'r740-16sff', 'r740xd-24sff', 'r740xd-12lff', 'r640-10nvme', 'r740xd-24nvme'];
$gen14AllPlans = Plan::query()->whereIn('slug', $gen14AllPlanSlugs)->pluck('id');
$gen14Ram->plans()->syncWithoutDetaching($gen14AllPlans);
// ─── Dedicated 14th Gen — Operating System ──────────────────────
$gen14Os = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — Operating System'],
[
'description' => 'Choose the OS image installed at provisioning time. Windows requires your own license (BYOL).',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 43,
],
);
$gen14OsOption = $this->seedRadioOption($gen14Os, 'Operating System', false, 1);
$this->seedValues($gen14OsOption, [
['label' => 'No OS (BYO image / custom PXE)', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => 'AlmaLinux 9', 'value' => 'alma9', 'monthly' => 0],
['label' => 'Ubuntu 24.04 LTS', 'value' => 'ubuntu24', 'monthly' => 0],
['label' => 'Debian 12', 'value' => 'debian12', 'monthly' => 0],
['label' => 'Rocky Linux 9', 'value' => 'rocky9', 'monthly' => 0],
['label' => 'Windows Server 2022 (BYOL)', 'value' => 'windows-2022-byol', 'monthly' => 0],
]);
$gen14Os->plans()->syncWithoutDetaching($gen14AllPlans);
// ─── Dedicated 14th Gen — Bandwidth ─────────────────────────────
$gen14Bandwidth = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — Bandwidth'],
[
'description' => 'Network port speed and bandwidth allocation. 10 Gbps tiers include traffic packages with overage protection.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 44,
],
);
$gen14BandwidthOption = $this->seedRadioOption($gen14Bandwidth, 'Bandwidth', false, 1);
$this->seedValues($gen14BandwidthOption, [
['label' => '1 Gbps unmetered (baseline)', 'value' => '1g-unmetered', 'monthly' => 0, 'is_default' => true],
['label' => '10 Gbps + 10 TB included', 'value' => '10g-10tb', 'monthly' => 45.00],
['label' => '10 Gbps + 50 TB included', 'value' => '10g-50tb', 'monthly' => 95.00],
['label' => '10 Gbps + 100 TB included', 'value' => '10g-100tb', 'monthly' => 175.00],
['label' => '10 Gbps unmetered (fair-use AUP)', 'value' => '10g-unmetered-fair-use', 'monthly' => 295.00],
]);
$gen14Bandwidth->plans()->syncWithoutDetaching($gen14AllPlans);
// ─── Dedicated 14th Gen — IPv4 Block ────────────────────────────
$gen14Ipv4 = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — IPv4 Block'],
[
'description' => 'Additional IPv4 addresses beyond the 1 included with every server. Blocks of /28 or larger require ARIN justification — we provide the template; you fill in your use case before the IPs are allocated.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 45,
],
);
$gen14Ipv4Option = $this->seedRadioOption($gen14Ipv4, 'IPv4 Block', false, 1);
// ARIN justification policy applies to anything above /29; the group
// description (above) tells customers about it, so we don't repeat
// it on every tier label.
$this->seedValues($gen14Ipv4Option, [
['label' => '1 included (no extra)', 'value' => '1', 'monthly' => 0, 'is_default' => true],
['label' => '/29 (5 usable)', 'value' => '5', 'monthly' => 12.00],
['label' => '/28 (13 usable) — justification required', 'value' => '13', 'monthly' => 36.00],
['label' => '/27 (29 usable) — justification required', 'value' => '29', 'monthly' => 80.00],
['label' => '/26 (61 usable) — justification required', 'value' => '61', 'monthly' => 145.00],
['label' => '/25 (125 usable) — justification required', 'value' => '125', 'monthly' => 275.00],
['label' => '/24 (253 usable) — justification required', 'value' => '253', 'monthly' => 499.00],
]);
$gen14Ipv4->plans()->syncWithoutDetaching($gen14AllPlans);
// ─── Dedicated 14th Gen — LFF Drive Bays ────────────────────────
$gen14Lff = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — LFF Drive Bays'],
[
'description' => 'Pick a starter drive configuration for the 3.5" LFF bays. Customers needing a heterogeneous setup (mixed sizes per bay, hot spares, etc.) can request a custom layout via the post-order ticket flow.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 50,
],
);
$gen14LffOption = $this->seedRadioOption($gen14Lff, 'LFF Drive Bays', false, 1);
$this->seedValues($gen14LffOption, [
['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => '2× 4 TB SATA HDD', 'value' => '2x4tb-hdd', 'monthly' => 24.00],
['label' => '4× 4 TB SATA HDD', 'value' => '4x4tb-hdd', 'monthly' => 48.00],
['label' => '2× 8 TB SATA HDD', 'value' => '2x8tb-hdd', 'monthly' => 40.00],
['label' => '4× 8 TB SATA HDD', 'value' => '4x8tb-hdd', 'monthly' => 80.00],
['label' => '8× 8 TB SATA HDD (R540 / R740xd LFF only)', 'value' => '8x8tb-hdd', 'monthly' => 160.00],
['label' => '12× 8 TB SATA HDD (R740xd LFF only)', 'value' => '12x8tb-hdd', 'monthly' => 240.00],
]);
$gen14LffPlanSlugs = ['r440-4lff', 'r540-8lff', 'r740xd-12lff'];
$gen14LffPlans = Plan::query()->whereIn('slug', $gen14LffPlanSlugs)->pluck('id');
$gen14Lff->plans()->syncWithoutDetaching($gen14LffPlans);
// ─── Dedicated 14th Gen — SFF Drive Bays ────────────────────────
$gen14Sff = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — SFF Drive Bays'],
[
'description' => 'Pick a starter drive configuration for the 2.5" SFF bays. Mixed-size setups via post-order ticket.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 51,
],
);
$gen14SffOption = $this->seedRadioOption($gen14Sff, 'SFF Drive Bays', false, 1);
$this->seedValues($gen14SffOption, [
['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => '2× 480 GB SATA SSD', 'value' => '2x480gb-ssd', 'monthly' => 20.00],
['label' => '4× 480 GB SATA SSD', 'value' => '4x480gb-ssd', 'monthly' => 40.00],
['label' => '2× 1.92 TB SATA SSD', 'value' => '2x1920gb-ssd', 'monthly' => 36.00],
['label' => '4× 1.92 TB SATA SSD', 'value' => '4x1920gb-ssd', 'monthly' => 72.00],
['label' => '8× 1.92 TB SATA SSD', 'value' => '8x1920gb-ssd', 'monthly' => 144.00],
['label' => '16× 1.92 TB SATA SSD (R740 16-bay / R740xd 24-SFF only)', 'value' => '16x1920gb-ssd', 'monthly' => 288.00],
['label' => '24× 1.92 TB SATA SSD (R740xd 24-SFF only)', 'value' => '24x1920gb-ssd', 'monthly' => 432.00],
]);
$gen14SffPlanSlugs = ['r640-8sff', 'r740-16sff', 'r740xd-24sff'];
$gen14SffPlans = Plan::query()->whereIn('slug', $gen14SffPlanSlugs)->pluck('id');
$gen14Sff->plans()->syncWithoutDetaching($gen14SffPlans);
// ─── Dedicated 14th Gen — NVMe Drive Bays ───────────────────────
$gen14Nvme = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — NVMe Drive Bays'],
[
'description' => 'Pick a starter NVMe drive configuration. U.2 bays direct-attach to CPU PCIe lanes — no RAID controller in the data path; software RAID (ZFS / mdraid / btrfs) only.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 52,
],
);
$gen14NvmeOption = $this->seedRadioOption($gen14Nvme, 'NVMe Drive Bays', false, 1);
$this->seedValues($gen14NvmeOption, [
['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => '2× 1 TB U.2 NVMe', 'value' => '2x1tb-nvme', 'monthly' => 44.00],
['label' => '4× 1 TB U.2 NVMe', 'value' => '4x1tb-nvme', 'monthly' => 88.00],
['label' => '2× 2 TB U.2 NVMe', 'value' => '2x2tb-nvme', 'monthly' => 96.00],
['label' => '4× 2 TB U.2 NVMe', 'value' => '4x2tb-nvme', 'monthly' => 192.00],
['label' => '8× 2 TB U.2 NVMe (R640 NVMe 10-bay / R740xd NVMe 24-bay)', 'value' => '8x2tb-nvme', 'monthly' => 384.00],
['label' => '16× 2 TB U.2 NVMe (R740xd NVMe 24-bay only)', 'value' => '16x2tb-nvme', 'monthly' => 768.00],
]);
$gen14NvmePlanSlugs = ['r640-10nvme', 'r740xd-24nvme'];
$gen14NvmePlans = Plan::query()->whereIn('slug', $gen14NvmePlanSlugs)->pluck('id');
$gen14Nvme->plans()->syncWithoutDetaching($gen14NvmePlans);
}
/**
* 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);
}
}