feat(vps): add interactive estimator + refresh Included card

Implements the design captured in
docs/superpowers/specs/2026-04-26-vps-hosting-estimator-design.md.

Estimator (after the hero on /vps-hosting):
- Workload picker (9 chips) recommends a plan; "Not sure" opens a
  mini-quiz with a 12-app catalog and a traffic+priority follow-up.
- Recommended-plan card with "or pick another" alternates dropdown.
- Add-ons panel: IPv4 stepper (1-8, $8/extra), Windows BYOL toggle,
  4-tier Managed Support radio (Self/Basic/Pro/Pilot @ $0/29/79/99),
  5-tier Off-site Backup radio (None/Lite/Standard/Extended/Vault @
  $0/5/12/25/59).
- Pilot tier gated to VPS-8+ via plan.features.tier; auto-fallback to
  Pro on plan downgrade with snackbar warning.
- Billing cycle toggle (Monthly / Quarterly / Annual) reuses
  per-cycle prices already on plan_prices and plan_config_values.
- Sticky footer with live total, "Order this configuration"
  (deep-links to /checkout/{plan} with all params), and "Copy share
  link" (history.replaceState debounced 300ms).
- Plans-table rows get an "Estimate →" link that pre-fills the
  estimator with that plan and scrolls up.

Backend:
- PlanSeeder: each VPS plan gets features.tier (1-32) for gating.
- ConfigOptionSeeder: scope existing Server Management group to
  dedicated only; add VPS Managed Support and Off-site Backup
  groups with full per-cycle prices.
- routes/marketing.php /vps-hosting: pass addOns + workloadMap +
  appExamples Inertia props.
- CheckoutController::show: build prefilledSelections from
  ?ipv4&windows&managed&backup query params; Vue page hydrates
  configSelections from this prop.

Included With All Plans: rewritten to 13 accurate items with
per-line wording (10 Gbps fair-use uplink, ZFS snapshots free,
KVM virtualization, rDNS/PTR control, OOB console/VNC, 99.9%
SLA, etc.) plus a "Coming soon" badge for DDoS protection.

Tests: 10 Pest feature tests in tests/Feature/Marketing/
VpsHostingEstimatorTest.php cover the page props, both new
seeded groups, plan-tier metadata, Server Management dedicated
scope, configGroups attachment, and checkout query-param
pre-fill round-trip. All 10 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 16:05:01 -04:00
parent d5f97d1240
commit cfa2e4c8d3
39 changed files with 2258 additions and 97 deletions

View File

@@ -70,13 +70,13 @@ class ConfigOptionSeeder extends Seeder
private function seedPresetGroups(): void
{
// ─── Server Management (all dedicated + VPS plans) ──────────────
// ─── Server Management (dedicated only) ─────────────────────────
$serverMgmt = PlanConfigGroup::updateOrCreate(
['name' => 'Server Management'],
[
'description' => 'Add managed support to your server.',
'mode' => 'preset',
'service_type' => null,
'service_type' => 'dedicated',
'is_active' => true,
'sort_order' => 10,
],
@@ -89,12 +89,66 @@ class ConfigOptionSeeder extends Seeder
['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'])
// 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()->syncWithoutDetaching($dedicatedAndVpsPlans);
$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(