feat(dedicated): filter drive bay options to chassis-compatible combos

Per-chassis page hides drive combos that require more bays than the
chassis has. R440 (4-bay) no longer shows 8× / 12× options; R640
(8-bay SFF) no longer shows 16× / 24×; R640 NVMe (10-bay) hides
16× NVMe.

Implementation:
- routes/marketing.php /dedicated-servers/{slug}: post-load filter
  on configGroups. For any group whose name contains "Drive Bays",
  parses the leading bay-count digit from each value's slug
  (e.g. "8x8tb-hdd" → 8) and drops values where bay_count >
  chassis features.bay_count. Non-quantity values (e.g. "none")
  always pass through.
- ConfigOptionSeeder: dropped the now-redundant "(R740xd LFF
  only)"/"(R740 16-bay only)" parentheticals from the labels —
  the filter handles compatibility, the labels stay clean.

New test asserts:
- R440 (4 bays) drops 8× and 12× LFF combos
- R740xd LFF (12 bays) keeps all combos
- R640 NVMe (10 bays) drops 16× NVMe combo

13/13 dedicated tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:36:57 -04:00
parent 2a9d36270a
commit 2658576c5b
3 changed files with 56 additions and 6 deletions

View File

@@ -518,8 +518,8 @@ class ConfigOptionSeeder extends Seeder
['label' => '4× 4 TB SATA HDD', 'value' => '4x4tb-hdd', 'monthly' => 48.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' => '2× 8 TB SATA HDD', 'value' => '2x8tb-hdd', 'monthly' => 40.00],
['label' => '4× 8 TB SATA HDD', 'value' => '4x8tb-hdd', 'monthly' => 80.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' => '8× 8 TB SATA HDD', 'value' => '8x8tb-hdd', 'monthly' => 160.00],
['label' => '12× 8 TB SATA HDD (R740xd LFF only)', 'value' => '12x8tb-hdd', 'monthly' => 240.00], ['label' => '12× 8 TB SATA HDD', 'value' => '12x8tb-hdd', 'monthly' => 240.00],
]); ]);
$gen14LffPlanSlugs = ['r440-4lff', 'r540-8lff', 'r740xd-12lff']; $gen14LffPlanSlugs = ['r440-4lff', 'r540-8lff', 'r740xd-12lff'];
@@ -546,8 +546,8 @@ class ConfigOptionSeeder extends Seeder
['label' => '2× 1.92 TB SATA SSD', 'value' => '2x1920gb-ssd', 'monthly' => 36.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' => '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' => '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' => '16× 1.92 TB SATA SSD', 'value' => '16x1920gb-ssd', 'monthly' => 288.00],
['label' => '24× 1.92 TB SATA SSD (R740xd 24-SFF only)', 'value' => '24x1920gb-ssd', 'monthly' => 432.00], ['label' => '24× 1.92 TB SATA SSD', 'value' => '24x1920gb-ssd', 'monthly' => 432.00],
]); ]);
$gen14SffPlanSlugs = ['r640-8sff', 'r740-16sff', 'r740xd-24sff']; $gen14SffPlanSlugs = ['r640-8sff', 'r740-16sff', 'r740xd-24sff'];
@@ -573,8 +573,8 @@ class ConfigOptionSeeder extends Seeder
['label' => '4× 1 TB U.2 NVMe', 'value' => '4x1tb-nvme', 'monthly' => 88.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' => '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' => '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' => '8× 2 TB U.2 NVMe', 'value' => '8x2tb-nvme', 'monthly' => 384.00],
['label' => '16× 2 TB U.2 NVMe (R740xd NVMe 24-bay only)', 'value' => '16x2tb-nvme', 'monthly' => 768.00], ['label' => '16× 2 TB U.2 NVMe', 'value' => '16x2tb-nvme', 'monthly' => 768.00],
]); ]);
$gen14NvmePlanSlugs = ['r640-10nvme', 'r740xd-24nvme']; $gen14NvmePlanSlugs = ['r640-10nvme', 'r740xd-24nvme'];

View File

@@ -114,6 +114,31 @@ Route::get('/dedicated-servers/{slug}', function (string $slug) {
->orderBy('sort_order') ->orderBy('sort_order')
->get(); ->get();
// Drive-bay combos are seeded with value slugs that start with the
// bay count (e.g. "8x8tb-hdd" → 8 bays). Filter out combos that
// don't fit this chassis so customers never see options that can't
// physically deploy on their build.
$bayCount = (int) ($plan->features['bay_count'] ?? 0);
if ($bayCount > 0) {
foreach ($configGroups as $group) {
if (! str_contains($group->name, 'Drive Bays')) {
continue;
}
foreach ($group->options as $option) {
$filtered = $option->values->filter(function ($value) use ($bayCount): bool {
if (preg_match('/^(\d+)x/', $value->value, $m)) {
return (int) $m[1] <= $bayCount;
}
// 'none' (and any other non-quantity value) always passes.
return true;
})->values();
$option->setRelation('values', $filtered);
}
}
}
return Inertia::render('Marketing/DedicatedServerDetail', [ return Inertia::render('Marketing/DedicatedServerDetail', [
'plan' => $plan, 'plan' => $plan,
'configGroups' => $configGroups, 'configGroups' => $configGroups,

View File

@@ -133,6 +133,31 @@ test('seeder creates the 9 dedicated 14th-gen config groups', function (): void
expect($names)->toContain('Dedicated 14th Gen — NVMe Drive Bays'); expect($names)->toContain('Dedicated 14th Gen — NVMe Drive Bays');
}); });
test('drive bay options are filtered to those that fit chassis bay count', function (): void {
// R440 has 4 LFF bays — combos requiring more bays should be hidden.
$r440Response = $this->get('/dedicated-servers/r440-4lff');
$r440Groups = collect($r440Response->viewData('page')['props']['configGroups']);
$lff = $r440Groups->firstWhere('name', 'Dedicated 14th Gen — LFF Drive Bays');
$values = collect($lff['options'][0]['values'])->pluck('value')->all();
expect($values)->toContain('none', '2x4tb-hdd', '4x4tb-hdd', '2x8tb-hdd', '4x8tb-hdd');
expect($values)->not->toContain('8x8tb-hdd', '12x8tb-hdd'); // 8 and 12 bays don't fit in 4
// R740xd LFF has 12 bays — should see all combos.
$r740xdResponse = $this->get('/dedicated-servers/r740xd-12lff');
$r740xdGroups = collect($r740xdResponse->viewData('page')['props']['configGroups']);
$lff740 = $r740xdGroups->firstWhere('name', 'Dedicated 14th Gen — LFF Drive Bays');
$values740 = collect($lff740['options'][0]['values'])->pluck('value')->all();
expect($values740)->toContain('8x8tb-hdd', '12x8tb-hdd');
// R640 NVMe has 10 bays — 16× shouldn't fit but 8× should.
$r640Response = $this->get('/dedicated-servers/r640-10nvme');
$r640Groups = collect($r640Response->viewData('page')['props']['configGroups']);
$nvme = $r640Groups->firstWhere('name', 'Dedicated 14th Gen — NVMe Drive Bays');
$valuesNvme = collect($nvme['options'][0]['values'])->pluck('value')->all();
expect($valuesNvme)->toContain('8x2tb-nvme');
expect($valuesNvme)->not->toContain('16x2tb-nvme'); // 16 bays don't fit in 10
});
test('drive bay groups are attached to chassis by bay type', function (): void { test('drive bay groups are attached to chassis by bay type', function (): void {
// LFF chassis should have LFF Drive Bays attached // LFF chassis should have LFF Drive Bays attached
$r440 = Plan::where('slug', 'r440-4lff')->first(); $r440 = Plan::where('slug', 'r440-4lff')->first();