feat(dedicated): add PCIe NVMe Add-in group (Sabrent adapter combos)

New separate-category config group for stacking high-performance
M.2 NVMe storage in any free PCIe slot, on top of the bay-attached
drives. Doesn't compete with bay drives — uses PCIe slots
independently.

Two adapter paths:
- Sabrent EC-PCIE (\$17.98) — single M.2 NVMe in any PCIe x4+ slot
- Sabrent EC-P4BF (\$99.99) — 4× M.2 NVMe in one PCIe 4.0 x16 slot,
  requires PCIe bifurcation support (R440/R540/R640/R740 verified;
  R740xd LFF rear PCIe may need check at build time)

10 curated combos + "None" default. Pricing: 12-month payback ×
1.5x markup on (Sabrent retail drive cost + adapter cost).

Single-drive (EC-PCIE):
  1× 1 TB Rocket 4 Plus     +\$35
  1× 2 TB Rocket 4 Plus     +\$40
  1× 4 TB Rocket 4 Plus     +\$115
  1× 8 TB Rocket 4 Plus     +\$465
  1× 2 TB Rocket 5 Gen5     +\$65
  1× 4 TB Rocket 5 Gen5     +\$180

4-drive bifurcation (EC-P4BF):
  4× 1 TB Rocket 4 Plus     +\$140
  4× 2 TB Rocket 4 Plus     +\$160
  4× 4 TB Rocket 4 Plus     +\$455
  4× 8 TB Rocket 4 Plus     +\$1,865 (extreme density, ~32 TB raw)

Drive prices scraped from Sabrent's Shopify product JSON
(/products/{slug}.json) on 2026-04-26.

Attached to LFF + SFF chassis (R440 / R540 / R640 / R740 /
R740xd SFF / R740xd LFF). Skip on R640 NVMe / R740xd NVMe — those
chassis route all PCIe lanes to the U.2 backplane, so no slots
are free for add-in cards.

Tests: bumped group count to 11; added a test verifying chassis
attachment correctness. 15/15 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 20:03:34 -04:00
parent 168b06cd7d
commit c74ca7f554
2 changed files with 63 additions and 2 deletions

View File

@@ -533,6 +533,52 @@ class ConfigOptionSeeder extends Seeder
$gen14PrivateNet->plans()->syncWithoutDetaching($gen14AllPlans); $gen14PrivateNet->plans()->syncWithoutDetaching($gen14AllPlans);
// ─── Dedicated 14th Gen — PCIe NVMe Add-in ──────────────────────
// Separate category from bay-attached drives — uses empty PCIe slots
// via Sabrent EC-PCIE (single-drive) or EC-P4BF (4-drive bifurcation
// x16) adapter cards. Customers can stack this on top of bay drives
// for fast scratch / cache / metadata storage.
// Pricing: 12-month payback × 1.5x markup on (Sabrent retail drive
// cost + adapter cost). Drive prices scraped from sabrent.com on
// 2026-04-26: Rocket 4 Plus 1TB \$249.99 / 2TB \$289.99 / 4TB \$879.99
// / 8TB \$3,699.99; Rocket 5 Gen5 1TB \$400 / 2TB \$499.99 / 4TB \$1,400.
// Adapters: EC-PCIE \$17.98, EC-P4BF \$99.99.
$gen14PciNvme = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — PCIe NVMe Add-in'],
[
'description' => 'Add high-performance M.2 NVMe storage in any free PCIe slot via Sabrent adapter cards. Single-drive (EC-PCIE) fits any x4+ slot; 4-drive (EC-P4BF) needs an x16 slot with bifurcation support.',
'mode' => 'preset',
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 47,
],
);
$gen14PciNvmeOption = $this->seedRadioOption($gen14PciNvme, 'PCIe NVMe Add-in', false, 1);
$this->seedValues($gen14PciNvmeOption, [
['label' => 'No PCIe NVMe add-in', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
// Single-drive via EC-PCIE adapter (Gen4)
['label' => '1× 1 TB Rocket 4 Plus M.2 NVMe (EC-PCIE)', 'value' => '1x1tb-r4p-pcie', 'monthly' => 35.00],
['label' => '1× 2 TB Rocket 4 Plus M.2 NVMe (EC-PCIE)', 'value' => '1x2tb-r4p-pcie', 'monthly' => 40.00],
['label' => '1× 4 TB Rocket 4 Plus M.2 NVMe (EC-PCIE)', 'value' => '1x4tb-r4p-pcie', 'monthly' => 115.00],
['label' => '1× 8 TB Rocket 4 Plus M.2 NVMe (EC-PCIE)', 'value' => '1x8tb-r4p-pcie', 'monthly' => 465.00],
// Single-drive Gen5 via EC-PCIE
['label' => '1× 2 TB Rocket 5 Gen5 M.2 NVMe (EC-PCIE)', 'value' => '1x2tb-r5-pcie', 'monthly' => 65.00],
['label' => '1× 4 TB Rocket 5 Gen5 M.2 NVMe (EC-PCIE)', 'value' => '1x4tb-r5-pcie', 'monthly' => 180.00],
// 4-drive bifurcation via EC-P4BF (requires PCIe x16 + bifurcation)
['label' => '4× 1 TB Rocket 4 Plus M.2 NVMe (EC-P4BF)', 'value' => '4x1tb-r4p-p4bf', 'monthly' => 140.00],
['label' => '4× 2 TB Rocket 4 Plus M.2 NVMe (EC-P4BF)', 'value' => '4x2tb-r4p-p4bf', 'monthly' => 160.00],
['label' => '4× 4 TB Rocket 4 Plus M.2 NVMe (EC-P4BF)', 'value' => '4x4tb-r4p-p4bf', 'monthly' => 455.00],
['label' => '4× 8 TB Rocket 4 Plus M.2 NVMe (EC-P4BF)', 'value' => '4x8tb-r4p-p4bf', 'monthly' => 1865.00],
]);
// Attach to LFF + SFF chassis (have free PCIe slots). Skip on the
// U.2 NVMe-native chassis since they don't have PCIe slots free
// for add-in cards (lanes are routed to the U.2 backplane).
$gen14PciNvmeSlugs = ['r440-4lff', 'r540-8lff', 'r640-8sff', 'r740-16sff', 'r740xd-24sff', 'r740xd-12lff'];
$gen14PciNvmePlans = Plan::query()->whereIn('slug', $gen14PciNvmeSlugs)->pluck('id');
$gen14PciNvme->plans()->syncWithoutDetaching($gen14PciNvmePlans);
// ─── Dedicated 14th Gen — LFF Drive Bays ──────────────────────── // ─── Dedicated 14th Gen — LFF Drive Bays ────────────────────────
$gen14Lff = PlanConfigGroup::updateOrCreate( $gen14Lff = PlanConfigGroup::updateOrCreate(
['name' => 'Dedicated 14th Gen — LFF Drive Bays'], ['name' => 'Dedicated 14th Gen — LFF Drive Bays'],

View File

@@ -148,13 +148,13 @@ test('1.5 TB RAM is filtered out for chassis with 16 DIMM slots', function (): v
} }
}); });
test('seeder creates the 10 dedicated 14th-gen config groups', function (): void { test('seeder creates the 11 dedicated 14th-gen config groups', function (): void {
$names = PlanConfigGroup::query() $names = PlanConfigGroup::query()
->where('name', 'like', 'Dedicated 14th Gen%') ->where('name', 'like', 'Dedicated 14th Gen%')
->pluck('name') ->pluck('name')
->all(); ->all();
expect(count($names))->toBe(10); expect(count($names))->toBe(11);
expect($names)->toContain('Dedicated 14th Gen — CPU Upgrade'); expect($names)->toContain('Dedicated 14th Gen — CPU Upgrade');
expect($names)->toContain('Dedicated 14th Gen — CPU Upgrade (R740xd)'); expect($names)->toContain('Dedicated 14th Gen — CPU Upgrade (R740xd)');
expect($names)->toContain('Dedicated 14th Gen — RAM Upgrade'); expect($names)->toContain('Dedicated 14th Gen — RAM Upgrade');
@@ -162,11 +162,26 @@ test('seeder creates the 10 dedicated 14th-gen config groups', function (): void
expect($names)->toContain('Dedicated 14th Gen — Bandwidth'); expect($names)->toContain('Dedicated 14th Gen — Bandwidth');
expect($names)->toContain('Dedicated 14th Gen — IPv4 Block'); expect($names)->toContain('Dedicated 14th Gen — IPv4 Block');
expect($names)->toContain('Dedicated 14th Gen — Private Networking'); expect($names)->toContain('Dedicated 14th Gen — Private Networking');
expect($names)->toContain('Dedicated 14th Gen — PCIe NVMe Add-in');
expect($names)->toContain('Dedicated 14th Gen — LFF Drive Bays'); expect($names)->toContain('Dedicated 14th Gen — LFF Drive Bays');
expect($names)->toContain('Dedicated 14th Gen — SFF Drive Bays'); expect($names)->toContain('Dedicated 14th Gen — SFF Drive Bays');
expect($names)->toContain('Dedicated 14th Gen — NVMe Drive Bays'); expect($names)->toContain('Dedicated 14th Gen — NVMe Drive Bays');
}); });
test('PCIe NVMe Add-in attaches only to LFF + SFF chassis, not pure-NVMe', function (): void {
foreach (['r440-4lff', 'r540-8lff', 'r640-8sff', 'r740-16sff', 'r740xd-24sff', 'r740xd-12lff'] as $slug) {
$response = $this->get("/dedicated-servers/{$slug}");
$names = collect($response->viewData('page')['props']['configGroups'])->pluck('name');
expect($names)->toContain('Dedicated 14th Gen — PCIe NVMe Add-in');
}
foreach (['r640-10nvme', 'r740xd-24nvme'] as $slug) {
$response = $this->get("/dedicated-servers/{$slug}");
$names = collect($response->viewData('page')['props']['configGroups'])->pluck('name');
expect($names)->not->toContain('Dedicated 14th Gen — PCIe NVMe Add-in');
}
});
test('drive bay options are filtered to those that fit chassis bay count', function (): void { 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. // R440 has 4 LFF bays — combos requiring more bays should be hidden.
$r440Response = $this->get('/dedicated-servers/r440-4lff'); $r440Response = $this->get('/dedicated-servers/r440-4lff');