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:
@@ -0,0 +1,518 @@
|
||||
# Configurable Options & Build Your Own System
|
||||
|
||||
**Date:** 2026-03-16
|
||||
**Status:** Approved
|
||||
**Author:** Claude + Andrew
|
||||
|
||||
## Overview
|
||||
|
||||
Add a configurable options system that serves two purposes:
|
||||
1. **Preset plan add-ons** — Customers customize orders with dropdowns, radios, quantities (RAM tiers, drive bays, management level)
|
||||
2. **Build Your Own** — Customers build custom specs from scratch using sliders with per-unit pricing
|
||||
|
||||
Both use the same database schema. Build Your Own is a special mode of configurable option groups where options are slider-type with per-unit pricing.
|
||||
|
||||
## Service Type Support
|
||||
|
||||
| Service Type | Preset Config Options | Build Your Own | Hourly Billing |
|
||||
|-------------|----------------------|---------------|---------------|
|
||||
| VPS | Yes (IPv4, Windows, Management, Block Storage) | Yes (CPU/RAM/Disk sliders) | Yes (with monthly cap) |
|
||||
| Dedicated | Yes (RAM, Drive Bays, NVMe, Network, RAID, Management) | No | No |
|
||||
| Hosting | No | No | No |
|
||||
| MySQL | Yes (Region) | Yes (Storage/Connections sliders) | Yes (with monthly cap) |
|
||||
| Game | No (existing plans) | Yes (RAM/Disk/Slots sliders) | Yes (with monthly cap) |
|
||||
| Backups | Yes (qty-based: VMs, Agents, Storage) | No | No |
|
||||
|
||||
## Database Schema
|
||||
|
||||
### plan_config_groups
|
||||
|
||||
Stores groups of related configurable options. Groups can be preset (attached to specific plans) or build-your-own (attached to a service type).
|
||||
|
||||
```sql
|
||||
plan_config_groups
|
||||
id bigint unsigned PK auto_increment
|
||||
name varchar(255) NOT NULL
|
||||
description text NULL
|
||||
mode varchar(20) NOT NULL DEFAULT 'preset' -- 'preset' | 'build_your_own'
|
||||
service_type varchar(50) NULL -- only for BYO groups
|
||||
is_active bool NOT NULL DEFAULT true -- enables soft-disabling without deletion
|
||||
sort_order int NOT NULL DEFAULT 0
|
||||
created_at timestamp NULL
|
||||
updated_at timestamp NULL
|
||||
deleted_at timestamp NULL -- SoftDeletes for archiving
|
||||
```
|
||||
|
||||
### plan_config_group_plan (pivot)
|
||||
|
||||
Many-to-many pivot between config groups and plans. Only used for preset groups.
|
||||
|
||||
```sql
|
||||
plan_config_group_plan
|
||||
plan_config_group_id bigint unsigned FK CASCADE
|
||||
plan_id bigint unsigned FK CASCADE
|
||||
UNIQUE(plan_config_group_id, plan_id)
|
||||
```
|
||||
|
||||
### plan_config_options
|
||||
|
||||
Individual configurable options within a group.
|
||||
|
||||
```sql
|
||||
plan_config_options
|
||||
id bigint unsigned PK auto_increment
|
||||
group_id bigint unsigned FK CASCADE
|
||||
name varchar(255) NOT NULL
|
||||
description text NULL
|
||||
type varchar(50) NOT NULL -- dropdown, radio, quantity, checkbox, text, slider
|
||||
provisioning_key varchar(100) NULL -- machine-readable key: cpu_cores, ram_gb, disk_gb, etc.
|
||||
required bool NOT NULL DEFAULT false
|
||||
is_active bool NOT NULL DEFAULT true -- enables soft-disabling individual options
|
||||
min_qty int NULL -- for quantity/slider types
|
||||
max_qty int NULL -- for quantity/slider types
|
||||
step int NULL DEFAULT 1 -- slider step increment
|
||||
unit_label varchar(50) NULL -- "cores", "GB", "slots", "addresses"
|
||||
hourly_price decimal(10,4) NULL -- per-unit hourly price (slider/qty)
|
||||
monthly_price decimal(10,2) NULL -- per-unit monthly price (slider/qty)
|
||||
quarterly_price decimal(10,2) NULL
|
||||
semi_annual_price decimal(10,2) NULL
|
||||
annual_price decimal(10,2) NULL
|
||||
sort_order int NOT NULL DEFAULT 0
|
||||
created_at timestamp NULL
|
||||
updated_at timestamp NULL
|
||||
```
|
||||
|
||||
**Type behavior:**
|
||||
- `dropdown` — VSelect, single choice from values list
|
||||
- `radio` — VRadioGroup, single choice from values list
|
||||
- `quantity` — VTextField (number), uses per-unit pricing from option row
|
||||
- `checkbox` — VCheckbox, on/off, price from single value row
|
||||
- `text` — VTextField, no pricing (e.g., hostname)
|
||||
- `slider` — VSlider, uses per-unit pricing from option row, respects min/max/step
|
||||
|
||||
### plan_config_values
|
||||
|
||||
Discrete choices for dropdown/radio/checkbox options. Not used for slider/quantity/text types (those use the per-unit price on the option itself).
|
||||
|
||||
```sql
|
||||
plan_config_values
|
||||
id bigint unsigned PK auto_increment
|
||||
option_id bigint unsigned FK CASCADE
|
||||
label varchar(255) NOT NULL
|
||||
value varchar(255) NULL -- internal/raw value
|
||||
hourly_price decimal(10,4) NOT NULL DEFAULT 0
|
||||
monthly_price decimal(10,2) NOT NULL DEFAULT 0
|
||||
quarterly_price decimal(10,2) NOT NULL DEFAULT 0
|
||||
semi_annual_price decimal(10,2) NOT NULL DEFAULT 0
|
||||
annual_price decimal(10,2) NOT NULL DEFAULT 0
|
||||
is_default bool NOT NULL DEFAULT false
|
||||
sort_order int NOT NULL DEFAULT 0
|
||||
created_at timestamp NULL
|
||||
updated_at timestamp NULL
|
||||
```
|
||||
|
||||
### subscription_config_selections
|
||||
|
||||
What the customer selected, with prices locked at time of purchase.
|
||||
|
||||
```sql
|
||||
subscription_config_selections
|
||||
id bigint unsigned PK auto_increment
|
||||
subscription_id bigint unsigned FK CASCADE
|
||||
option_id bigint unsigned FK RESTRICT -- RESTRICT: cannot delete option with existing selections
|
||||
value_id bigint unsigned FK SET NULL NULL -- SET NULL: if value deleted, selection preserved
|
||||
quantity int NULL -- for quantity/slider
|
||||
text_value varchar(500) NULL -- for text type
|
||||
locked_price decimal(10,4) NOT NULL -- total price for this selection (4 decimals for hourly precision)
|
||||
locked_hourly_price decimal(10,4) NULL
|
||||
billing_cycle varchar(50) NOT NULL
|
||||
is_custom_build bool NOT NULL DEFAULT false
|
||||
created_at timestamp NULL
|
||||
updated_at timestamp NULL
|
||||
UNIQUE(subscription_id, option_id)
|
||||
```
|
||||
|
||||
### New plans (base $0, internal status for BYO checkout)
|
||||
|
||||
BYO custom plans use `status: 'internal'` — a new status value that:
|
||||
- Is treated as available for checkout (bypasses `isAvailable()` check)
|
||||
- Is excluded from public plan listings (pricing page, marketing pages)
|
||||
- Is distinct from 'hidden' (which is for grandfathered legacy plans)
|
||||
|
||||
The `Plan::isAvailable()` method is updated: `return in_array($this->status, ['active', 'internal']);`
|
||||
|
||||
- `vps-custom` (service_type: vps, status: internal)
|
||||
- `mysql-custom` (service_type: mysql, status: internal)
|
||||
- `game-custom` (service_type: game, status: internal)
|
||||
|
||||
## Stripe Billing Strategy
|
||||
|
||||
### Preset Plans with Config Options
|
||||
Base plan has a pre-existing Stripe price. Config add-on total is computed at checkout and a **single dynamic Stripe price** is created via `Stripe\Price::create()` for the total amount (base + add-ons). This ensures the Stripe invoice reflects the actual amount charged.
|
||||
|
||||
```php
|
||||
$totalMonthly = $plan->priceForCycle($cycle) + $configTotal;
|
||||
$stripePrice = \Stripe\Price::create([
|
||||
'unit_amount' => (int) ($totalMonthly * 100),
|
||||
'currency' => 'usd',
|
||||
'recurring' => ['interval' => $stripeInterval],
|
||||
'product' => $plan->stripe_product_id,
|
||||
]);
|
||||
```
|
||||
|
||||
### Build Your Own
|
||||
Same approach — total computed from slider values, single dynamic Stripe price created. The `{type}-custom` plan has a Stripe product but no pre-set price.
|
||||
|
||||
### Price Changes / Upgrades (Future)
|
||||
When a customer upgrades config options on an existing subscription, a new Stripe price is created and the subscription is swapped to it. Deferred to v2.
|
||||
|
||||
## Coupon Application
|
||||
|
||||
Coupons apply to the **total computed price** (base plan + all config selections), not just the base price. For BYO where base is $0, the coupon applies to the config total. The coupon discount is applied before creating the Stripe price:
|
||||
|
||||
```php
|
||||
$totalBeforeCoupon = $basePlanPrice + $configTotal;
|
||||
$discount = $coupon->calculateDiscount($totalBeforeCoupon);
|
||||
$finalPrice = $totalBeforeCoupon - $discount;
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
### PlanConfigGroup
|
||||
- `use SoftDeletes`
|
||||
- `hasMany(PlanConfigOption::class, 'group_id')`
|
||||
- `belongsToMany(Plan::class, 'plan_config_group_plan')`
|
||||
- Scopes: `preset()`, `buildYourOwn()`, `forServiceType($type)`, `active()`
|
||||
|
||||
### PlanConfigOption
|
||||
- `belongsTo(PlanConfigGroup::class, 'group_id')`
|
||||
- `hasMany(PlanConfigValue::class, 'option_id')`
|
||||
- Scope: `active()`
|
||||
- Methods: `isSlider()`, `isQuantity()`, `isDropdown()`, `isRadio()`, `isCheckbox()`, `isText()`
|
||||
- Method: `calculatePrice(int $quantity, string $cycle): float`
|
||||
- Method: `getHourlyPrice(int $quantity): float`
|
||||
|
||||
### PlanConfigValue
|
||||
- `belongsTo(PlanConfigOption::class, 'option_id')`
|
||||
- Method: `getPriceForCycle(string $cycle): float`
|
||||
|
||||
### SubscriptionConfigSelection
|
||||
- `belongsTo(Subscription::class)`
|
||||
- `belongsTo(PlanConfigOption::class, 'option_id')`
|
||||
- `belongsTo(PlanConfigValue::class, 'value_id')`
|
||||
|
||||
### Plan (updated)
|
||||
- Add: `configGroups(): BelongsToMany`
|
||||
- Update: `isAvailable()` returns true for `'active'` and `'internal'` status
|
||||
- Update: `scopePublic()` scope excludes `'internal'` and `'hidden'` plans from public listings
|
||||
|
||||
### Subscription (updated)
|
||||
- Add: `configSelections(): HasMany`
|
||||
- Add: `totalConfigPrice(string $cycle): float`
|
||||
- Add: `totalHourlyPrice(): float`
|
||||
|
||||
### TypeScript Interfaces (types/index.ts)
|
||||
|
||||
```typescript
|
||||
export interface PlanConfigGroup {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
mode: 'preset' | 'build_your_own'
|
||||
service_type: string | null
|
||||
is_active: boolean
|
||||
sort_order: number
|
||||
options: PlanConfigOption[]
|
||||
plans?: Plan[]
|
||||
}
|
||||
|
||||
export interface PlanConfigOption {
|
||||
id: number
|
||||
group_id: number
|
||||
name: string
|
||||
description: string | null
|
||||
type: 'dropdown' | 'radio' | 'quantity' | 'checkbox' | 'text' | 'slider'
|
||||
provisioning_key: string | null
|
||||
required: boolean
|
||||
is_active: boolean
|
||||
min_qty: number | null
|
||||
max_qty: number | null
|
||||
step: number | null
|
||||
unit_label: string | null
|
||||
hourly_price: number | null
|
||||
monthly_price: number | null
|
||||
quarterly_price: number | null
|
||||
semi_annual_price: number | null
|
||||
annual_price: number | null
|
||||
sort_order: number
|
||||
values: PlanConfigValue[]
|
||||
}
|
||||
|
||||
export interface PlanConfigValue {
|
||||
id: number
|
||||
option_id: number
|
||||
label: string
|
||||
value: string | null
|
||||
hourly_price: number
|
||||
monthly_price: number
|
||||
quarterly_price: number
|
||||
semi_annual_price: number
|
||||
annual_price: number
|
||||
is_default: boolean
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface SubscriptionConfigSelection {
|
||||
id: number
|
||||
subscription_id: number
|
||||
option_id: number
|
||||
value_id: number | null
|
||||
quantity: number | null
|
||||
text_value: string | null
|
||||
locked_price: number
|
||||
locked_hourly_price: number | null
|
||||
billing_cycle: string
|
||||
is_custom_build: boolean
|
||||
option?: PlanConfigOption
|
||||
value?: PlanConfigValue
|
||||
}
|
||||
```
|
||||
|
||||
## Admin UI
|
||||
|
||||
### Pages
|
||||
- `Admin/ConfigGroups/Index.vue` — List all groups with VDataTable, filter tabs (All/Preset/BYO)
|
||||
- `Admin/ConfigGroups/Create.vue` — Create form with inline options + values builder
|
||||
- `Admin/ConfigGroups/Edit.vue` — Edit form, same structure as create
|
||||
|
||||
### Admin Controller: ConfigGroupController
|
||||
- `index()` — paginated list with option/plan counts
|
||||
- `create()` — form with available plans
|
||||
- `store(StoreConfigGroupRequest)` — create group + options + values in transaction
|
||||
- `edit(PlanConfigGroup)` — form pre-populated
|
||||
- `update(UpdateConfigGroupRequest, PlanConfigGroup)` — sync options/values
|
||||
- `destroy(PlanConfigGroup)` — delete (soft-block if selections exist)
|
||||
|
||||
### Form Request: StoreConfigGroupRequest
|
||||
Validates nested structure:
|
||||
- name, description, mode, service_type
|
||||
- options[].name, options[].type, options[].required, options[].min_qty, options[].max_qty, options[].step
|
||||
- options[].values[].label, options[].values[].monthly_price, etc.
|
||||
|
||||
### Admin Navigation
|
||||
Under Infrastructure: `{ title: 'Config Options', to: '/config-groups', icon: 'tabler-adjustments' }`
|
||||
|
||||
### Admin Routes
|
||||
```
|
||||
GET /config-groups → ConfigGroupController@index
|
||||
GET /config-groups/create → ConfigGroupController@create
|
||||
POST /config-groups → ConfigGroupController@store
|
||||
GET /config-groups/{id}/edit → ConfigGroupController@edit
|
||||
PUT /config-groups/{id} → ConfigGroupController@update
|
||||
DELETE /config-groups/{id} → ConfigGroupController@destroy
|
||||
```
|
||||
|
||||
## Checkout Flow
|
||||
|
||||
### Path 1: Preset Plan (enhanced existing flow)
|
||||
|
||||
```
|
||||
Pricing Page → Pick Plan → /checkout/{plan_id}?cycle=monthly
|
||||
```
|
||||
|
||||
`Checkout/Show.vue` enhanced:
|
||||
- After billing cycle selection, loads configurable option groups for the plan
|
||||
- Renders options by type (VSelect, VRadioGroup, VSlider, VCheckbox, VTextField)
|
||||
- Price summary updates live: base + config selections
|
||||
- Existing VPS provisioning config (OS, SSH key) remains separate
|
||||
|
||||
### Path 2: Build Your Own (new)
|
||||
|
||||
```
|
||||
Pricing Page → Toggle "Build Your Own" → Configure sliders → Deploy Now
|
||||
→ /checkout/custom/{serviceType}?config={encoded_selections}
|
||||
```
|
||||
|
||||
New route: `GET /checkout/custom/{serviceType}` renders Checkout/Show.vue in custom mode.
|
||||
|
||||
### Checkout/Show.vue Modes
|
||||
- `mode: 'preset'` — plan card + optional config options
|
||||
- `mode: 'custom'` — slider configurator as main content, no plan card
|
||||
|
||||
### Price Summary Sidebar (both modes)
|
||||
```
|
||||
Order Summary
|
||||
─────────────
|
||||
Base plan: $30.00 (or $0 for BYO)
|
||||
RAM: 64 GB +$15.00
|
||||
NVMe: 2x 1TB +$30.00
|
||||
Management: Semi +$25.00
|
||||
──────────────────────
|
||||
Monthly: $100.00
|
||||
Hourly: $0.137/hr
|
||||
Monthly cap: $100.00
|
||||
|
||||
Billing: [Monthly ▼]
|
||||
[ Deploy Now ]
|
||||
```
|
||||
|
||||
### CheckoutController Changes
|
||||
|
||||
**show(Plan $plan)** — enhanced to load config groups:
|
||||
```php
|
||||
$configGroups = $plan->configGroups()->with('options.values')->get();
|
||||
// Pass to Inertia as 'configGroups' prop
|
||||
```
|
||||
|
||||
**showCustom(string $serviceType)** — new method for BYO:
|
||||
```php
|
||||
$configGroup = PlanConfigGroup::buildYourOwn()->forServiceType($serviceType)->first();
|
||||
$plan = Plan::where('slug', "{$serviceType}-custom")->firstOrFail();
|
||||
// Render same checkout component in 'custom' mode
|
||||
```
|
||||
|
||||
**store(Request $request, Plan $plan)** — enhanced to validate and save selections:
|
||||
```php
|
||||
// Validate config selections against option types/ranges
|
||||
// Calculate and lock prices at time of purchase
|
||||
// Create subscription
|
||||
// Create subscription_config_selections rows
|
||||
// Dispatch SubscriptionCreated event
|
||||
```
|
||||
|
||||
### Validation Rules for Config Selections
|
||||
- Dropdown/radio: value_id must belong to the option
|
||||
- Quantity/slider: quantity must be between min_qty and max_qty, divisible by step
|
||||
- Checkbox: value_id present (checked) or null (unchecked)
|
||||
- Text: string, max 500 chars
|
||||
- Required options must have a selection
|
||||
|
||||
## Pricing Page Changes
|
||||
|
||||
`Marketing/Pricing.vue` enhanced:
|
||||
- Segmented toggle at top: **Preset Plans | Build Your Own**
|
||||
- Service type tabs work in both modes
|
||||
- **In BYO mode:** Only tabs for supported service types are shown (VPS, MySQL, Game). If user is on a non-BYO tab (e.g., Dedicated) and toggles to BYO, auto-switch to the first supported tab (VPS).
|
||||
- Preset mode: existing plan cards (unchanged)
|
||||
- Build Your Own mode: vertical slider configurator (Layout A — sliders left, price summary right)
|
||||
- Billing cycle switcher works in both modes (hourly shown only in BYO mode)
|
||||
- "Deploy Now" in BYO routes to `/checkout/custom/{serviceType}`
|
||||
|
||||
## Build Your Own Configurator UI
|
||||
|
||||
Layout A: Vertical Sliders + Side Summary
|
||||
|
||||
**Left panel (sliders):**
|
||||
- Each resource as a labeled VSlider with current value display
|
||||
- Min/max labels at slider ends
|
||||
- Per-unit price shown next to current value
|
||||
- Sliders use EZSCALE navy blue (#1d4ed8) accent
|
||||
|
||||
**Right panel (sticky summary):**
|
||||
- Hourly rate (large)
|
||||
- Monthly cap
|
||||
- Line-item breakdown (CPU: $X, RAM: $X, Disk: $X)
|
||||
- Billing cycle selector
|
||||
- "Deploy Now" CTA button
|
||||
|
||||
## Build Your Own Resource Configuration
|
||||
|
||||
### VPS
|
||||
| Resource | Min | Max | Step | Unit | Hourly/unit | Monthly/unit |
|
||||
|----------|-----|-----|------|------|-------------|-------------|
|
||||
| CPU Cores | 1 | 16 | 1 | cores | $0.003 | $2.00 |
|
||||
| RAM | 1 | 64 | 1 | GB | $0.0015 | $1.00 |
|
||||
| SSD Storage | 25 | 1000 | 25 | GB | $0.0001 | $0.05 |
|
||||
| Bandwidth | — | — | — | — | — | Included |
|
||||
|
||||
### MySQL
|
||||
| Resource | Min | Max | Step | Unit | Hourly/unit | Monthly/unit |
|
||||
|----------|-----|-----|------|------|-------------|-------------|
|
||||
| Storage | 5 | 500 | 5 | GB | $0.0003 | $0.20 |
|
||||
| Max Connections | 50 | 1000 | 50 | conns | $0.0001 | $0.05 |
|
||||
| Daily Backups | 0 | 1 | 1 | toggle | — | $2.00 |
|
||||
|
||||
### Game Servers
|
||||
| Resource | Min | Max | Step | Unit | Hourly/unit | Monthly/unit |
|
||||
|----------|-----|-----|------|------|-------------|-------------|
|
||||
| RAM | 1 | 16 | 1 | GB | $0.002 | $1.50 |
|
||||
| Storage | 10 | 200 | 10 | GB | $0.0001 | $0.08 |
|
||||
| Player Slots | 10 | 200 | 10 | slots | $0.0001 | $0.05 |
|
||||
|
||||
*Note: Per-unit prices are initial values. Adjusted via admin UI without code changes.*
|
||||
|
||||
## Hourly Billing
|
||||
|
||||
### Display Model (MVP)
|
||||
- Hourly rate calculated and displayed to customer: `sum(unit_hourly_price × quantity)`
|
||||
- Monthly cap calculated: `sum(unit_monthly_price × quantity)`
|
||||
- Customer billed at monthly cap on their billing cycle
|
||||
- Actual hourly metering deferred to Phase 2
|
||||
|
||||
### Future: Real Hourly Metering (Phase 2)
|
||||
- `usage_records` table tracks server uptime hours
|
||||
- Cron job calculates charges: `min(hourly_rate × hours, monthly_cap)`
|
||||
- Prepaid credit system for hourly customers
|
||||
- Server auto-suspend when credits depleted
|
||||
|
||||
## Provisioning Integration
|
||||
|
||||
### Preset Plans
|
||||
Existing provisioning flow unchanged. Config selections stored but provisioning uses the plan's package_id.
|
||||
|
||||
### Build Your Own VPS
|
||||
VirtFusion's `servers_create` API accepts raw resource parameters alongside a packageId. For BYO:
|
||||
1. Use a designated BYO package in VirtFusion as a base template (handles network profile, storage profile, etc.)
|
||||
2. Override resource params from slider selections using `provisioning_key` mapping:
|
||||
- `cpuCores` ← option where `provisioning_key = 'cpu_cores'`
|
||||
- `memory` ← option where `provisioning_key = 'ram_gb'` × 1024 (MB)
|
||||
- `storage` ← option where `provisioning_key = 'disk_gb'`
|
||||
3. The `provisioning_key` field on `plan_config_options` makes this mapping explicit and admin-configurable
|
||||
|
||||
**Note:** If VirtFusion requires a package for server creation, create a minimal "BYO Base" package in VirtFusion with minimum resources and override via the API params. Test this approach before implementation — if VirtFusion doesn't support resource overrides on `servers_create`, fall back to creating dynamic packages per BYO order via the packages API.
|
||||
|
||||
### Build Your Own MySQL
|
||||
Manual provisioning for MVP. The subscription is created with config selections stored, and an admin notification is sent for manual setup. Automated provisioning deferred until a MySQL provisioning API is available.
|
||||
|
||||
### Build Your Own Game
|
||||
Pterodactyl API supports resource allocation on server creation (`memory`, `disk`, `cpu`). Map from `provisioning_key` values on the slider options, similar to VPS approach.
|
||||
|
||||
## Migration Script Integration
|
||||
|
||||
### Phase 2 Enhancement
|
||||
Import WHMCS configurable option groups, options, and values:
|
||||
- Map WHMCS config group IDs to EZSCALE config group IDs
|
||||
- Create plan_config_groups with mode='preset'
|
||||
- Attach to mapped plans via pivot
|
||||
- Import all options and values with pricing
|
||||
|
||||
### Phase 3 Enhancement
|
||||
Import customer config selections:
|
||||
- For each WHMCS service with configoptions, create subscription_config_selections
|
||||
- Lock prices from WHMCS recurring amounts
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- PlanConfigOption::calculatePrice() with various types
|
||||
- PlanConfigOption::getHourlyPrice() calculation
|
||||
- Subscription::totalConfigPrice() aggregation
|
||||
- Subscription::totalHourlyPrice() aggregation
|
||||
|
||||
### Feature Tests
|
||||
- Admin CRUD for config groups (create/read/update/delete)
|
||||
- Nested option/value creation and validation
|
||||
- Preset checkout with config options selected
|
||||
- BYO checkout with slider values
|
||||
- Price calculation matches expected totals
|
||||
- Config selections locked at purchase price
|
||||
- Required option validation
|
||||
- Slider min/max/step validation
|
||||
- Customer cannot access admin config pages
|
||||
|
||||
## Out of Scope (Future)
|
||||
- Real-time hourly billing with usage metering (Phase 2)
|
||||
- Config option dependencies (e.g., NVMe requires RAID H730)
|
||||
- Config option stock/inventory limits
|
||||
- Upgrade/downgrade config options on existing subscriptions
|
||||
- Config option comparison across plans
|
||||
Reference in New Issue
Block a user