['IPFS', 'ipfs'], 'flux_node' => ['Flux', 'flux', 'FluxOS'], 'horizon_node' => ['Horizen', 'horizon', 'Zen'], 'ethereum_node' => ['Ethereum', 'eth-docker'], 'bitcoin_node' => ['Bitcoin'], ]; private ?array $availableImages = null; public function __construct( private HostingPricingService $pricing, private ContaboProvider $contaboProvider, private ServerPanelCapabilityService $panelCapabilities, ) {} public function passwordPattern(): string { return self::PASSWORD_PATTERN; } /** * @return array */ public function frontendPayload(HostingProduct $product): array { $defaults = $this->defaultSelections($product); return [ 'id' => $product->id, 'name' => $product->name, 'currency' => $product->display_currency, 'prices' => $this->baseCyclePrices($product), 'setup_fees' => $this->setupFeesByCycle($product), 'catalog' => $this->catalogForProduct($product), 'defaults' => $defaults, 'panel_snapshot' => $this->panelCapabilities->describeSelection($product, $defaults, $this->catalogForProduct($product)), ]; } /** * @return array */ public function catalogForProduct(HostingProduct $product): array { $images = $this->imageOptions($product); $defaultImage = $this->defaultImageOption($images)['value'] ?? null; $defaultImageMeta = $defaultImage ? $this->findOption($images, $defaultImage) : null; return [ 'images' => $images, 'regions' => $this->optionCollection($product, (array) config('hosting.server_order.regions', [])), 'licenses' => $this->optionCollection($product, (array) config('hosting.server_order.licenses', [])), 'applications' => $this->applicationOptions($product), 'additional_ip' => $this->optionCollection($product, (array) config('hosting.server_order.additional_ip', [])), 'private_networking' => $this->optionCollection($product, (array) config('hosting.server_order.private_networking', [])), 'storage_types' => $this->optionCollection($product, (array) config('hosting.server_order.storage_types', [])), 'object_storage' => $this->optionCollection($product, (array) config('hosting.server_order.object_storage', [])), 'linux_default_users' => $this->defaultUserOptions($product, 'linux'), 'windows_default_users' => $this->defaultUserOptions($product, 'windows'), 'default_image_os_family' => $defaultImageMeta['os_family'] ?? 'linux', ]; } /** * @param array $input * @return array{selection: array, quote: array} */ public function selectionAndQuote(HostingProduct $product, array $input): array { $catalog = $this->catalogForProduct($product); $defaults = $this->defaultSelections($product); $billingCycle = (string) ($input['billing_cycle'] ?? CustomerHostingOrder::CYCLE_MONTHLY); $selection = [ 'image' => $this->validatedOptionValue($catalog['images'], (string) ($input['image'] ?? $defaults['image'])), 'region' => $this->validatedOptionValue($catalog['regions'], (string) ($input['region'] ?? $defaults['region'])), 'license' => $this->validatedOptionValue($catalog['licenses'], (string) ($input['license'] ?? $defaults['license'])), 'application' => $this->validatedOptionValue($catalog['applications'], (string) ($input['application'] ?? $defaults['application'])), 'additional_ip' => $this->validatedOptionValue($catalog['additional_ip'], (string) ($input['additional_ip'] ?? $defaults['additional_ip'])), 'private_networking' => $this->validatedOptionValue($catalog['private_networking'], (string) ($input['private_networking'] ?? $defaults['private_networking'])), 'storage_type' => $this->validatedOptionValue($catalog['storage_types'], (string) ($input['storage_type'] ?? $defaults['storage_type'])), 'object_storage' => $this->validatedOptionValue($catalog['object_storage'], (string) ($input['object_storage'] ?? $defaults['object_storage'])), ]; $image = $this->findOption($catalog['images'], $selection['image']); $defaultUsers = $this->defaultUserOptions($product, (string) ($image['os_family'] ?? 'linux')); $selection['default_user'] = $this->validatedOptionValue( $defaultUsers, (string) ($input['default_user'] ?? $image['default_user'] ?? array_key_first($defaultUsers)) ); $this->assertCompatibleSelections($selection, $catalog); $quote = $this->quote($product, $selection, $billingCycle, $catalog); return [ 'selection' => $selection, 'quote' => $quote, 'selection_summary' => $this->selectionSummary($product, $selection, $catalog), 'panel_snapshot' => $this->panelCapabilities->describeSelection($product, $selection, $catalog), ]; } /** * @param array $selection * @param array|null $catalog * @return array */ public function quote(HostingProduct $product, array $selection, string $billingCycle, ?array $catalog = null): array { $catalog ??= $this->catalogForProduct($product); $basePrices = $this->baseCyclePrices($product); $baseTotal = (float) ($basePrices[$billingCycle] ?? 0); $lines = [[ 'code' => 'base_plan', 'label' => $product->name, 'amount' => round($baseTotal, 2), 'automated' => true, ]]; $manualFollowUp = []; $setupFee = $this->setupFeeForCycle($product, $billingCycle); if ($setupFee > 0) { $lines[] = [ 'code' => 'setup_fee', 'label' => $this->setupFeeLabel($product, $billingCycle), 'amount' => round($setupFee, 2), 'automated' => true, 'one_time' => true, ]; } foreach ([ 'image' => 'images', 'region' => 'regions', 'license' => 'licenses', 'application' => 'applications', 'additional_ip' => 'additional_ip', 'private_networking' => 'private_networking', 'storage_type' => 'storage_types', 'object_storage' => 'object_storage', ] as $selectionKey => $catalogKey) { $value = (string) ($selection[$selectionKey] ?? ''); if ($value === '') { continue; } $option = $this->findOption($catalog[$catalogKey], $value); if (! $option) { continue; } $amount = (float) ($option['prices'][$billingCycle] ?? 0); if ($amount <= 0) { continue; } $lines[] = [ 'code' => $selectionKey, 'label' => (string) ($option['label'] ?? $value), 'amount' => round($amount, 2), 'automated' => (bool) ($option['automated'] ?? false), ]; if (! ($option['automated'] ?? false)) { $manualFollowUp[] = (string) ($option['label'] ?? $value); } } $total = round(collect($lines)->sum('amount'), 2); return [ 'currency' => $product->display_currency, 'billing_cycle' => $billingCycle, 'months' => $this->cycleMonths($billingCycle), 'base_total' => round($baseTotal, 2), 'line_items' => $lines, 'total' => $total, 'manual_follow_up' => array_values(array_unique($manualFollowUp)), ]; } /** * @param array $selection * @return array */ public function provisioningConfig(HostingProduct $product, array $selection, string $serverName, string $billingCycle): array { $catalog = $this->catalogForProduct($product); $image = $this->findOption($catalog['images'], (string) ($selection['image'] ?? '')); $license = $this->findOption($catalog['licenses'], (string) ($selection['license'] ?? '')); $application = $this->findOption($catalog['applications'], (string) ($selection['application'] ?? '')); $region = $this->findOption($catalog['regions'], (string) ($selection['region'] ?? '')); $additionalIp = $this->findOption($catalog['additional_ip'], (string) ($selection['additional_ip'] ?? '')); $privateNetworking = $this->findOption($catalog['private_networking'], (string) ($selection['private_networking'] ?? '')); $storageType = $this->findOption($catalog['storage_types'], (string) ($selection['storage_type'] ?? '')); $objectStorage = $this->findOption($catalog['object_storage'], (string) ($selection['object_storage'] ?? '')); $addOns = []; foreach ([$image, $additionalIp, $privateNetworking, $storageType] as $option) { $addOnKey = (string) ($option['add_on'] ?? ''); if ($addOnKey !== '') { $addOns[$addOnKey] = new \stdClass(); } } $manualFollowUp = []; foreach ([$license, $application, $storageType, $objectStorage] as $option) { if ($option && ! ($option['automated'] ?? false)) { $manualFollowUp[] = (string) ($option['label'] ?? $option['value'] ?? 'Option'); } } $applicationId = $application['application_id'] ?? null; $userData = $this->cloudInitForOption((array) $application, (string) ($selection['default_user'] ?? ($image['default_user'] ?? 'root'))); return [ 'product_id' => $product->contabo_product_id, 'image_id' => ! ($image['is_install_later'] ?? false) ? ($image['image_id'] ?? $selection['image'] ?? config('hosting.vps.default_image')) : null, 'region' => (string) ($region['value'] ?? $selection['region'] ?? $product->contabo_region ?? config('hosting.vps.default_region', 'EU')), 'display_name' => $serverName, 'default_user' => (string) ($selection['default_user'] ?? ($image['default_user'] ?? 'root')), 'license' => $license['license'] ?? null, 'panel' => $license['panel'] ?? null, 'application_id' => is_string($applicationId) && $applicationId !== '' ? $applicationId : null, 'user_data' => $userData, 'period' => $this->cycleMonths($billingCycle), 'add_ons' => $addOns, 'manual_follow_up' => array_values(array_unique($manualFollowUp)), 'panel_snapshot' => $this->panelCapabilities->describeSelection($product, $selection, $catalog), ]; } /** * @return array */ public function defaultSelections(HostingProduct $product): array { $catalog = $this->catalogForProduct($product); $defaultImage = $catalog['images'][0]['value'] ?? null; $defaultImageMeta = $defaultImage ? $this->findOption($catalog['images'], $defaultImage) : null; $defaultUserOptions = $this->defaultUserOptions($product, (string) ($defaultImageMeta['os_family'] ?? 'linux')); return [ 'image' => $defaultImage, 'region' => (string) ($catalog['regions'][0]['value'] ?? 'EU'), 'license' => 'none', 'application' => 'none', 'additional_ip' => 'none', 'private_networking' => 'disabled', 'storage_type' => 'included', 'object_storage' => 'none', 'default_user' => (string) ($defaultUserOptions[0]['value'] ?? 'root'), ]; } /** * @param array $selection * @param array|null $catalog * @return array> */ public function selectionSummary(HostingProduct $product, array $selection, ?array $catalog = null): array { $catalog ??= $this->catalogForProduct($product); $image = $this->findOption($catalog['images'], (string) ($selection['image'] ?? '')); $defaultUsers = $this->defaultUserOptions($product, (string) ($image['os_family'] ?? 'linux')); return array_values(array_filter([ $this->selectionSummaryItem('image', 'Image', $this->findOption($catalog['images'], (string) ($selection['image'] ?? ''))), $this->selectionSummaryItem('region', 'Region', $this->findOption($catalog['regions'], (string) ($selection['region'] ?? ''))), $this->selectionSummaryItem('license', 'License', $this->findOption($catalog['licenses'], (string) ($selection['license'] ?? ''))), $this->selectionSummaryItem('application', 'Preinstalled App', $this->findOption($catalog['applications'], (string) ($selection['application'] ?? ''))), $this->selectionSummaryItem('default_user', 'Default User', $this->findOption($defaultUsers, (string) ($selection['default_user'] ?? '')), true), $this->selectionSummaryItem('additional_ip', 'IP Address', $this->findOption($catalog['additional_ip'], (string) ($selection['additional_ip'] ?? ''))), $this->selectionSummaryItem('private_networking', 'Private Networking', $this->findOption($catalog['private_networking'], (string) ($selection['private_networking'] ?? ''))), $this->selectionSummaryItem('storage_type', 'Storage Type', $this->findOption($catalog['storage_types'], (string) ($selection['storage_type'] ?? ''))), $this->selectionSummaryItem('object_storage', 'Object Storage', $this->findOption($catalog['object_storage'], (string) ($selection['object_storage'] ?? ''))), ])); } /** * @return array */ public function baseCyclePrices(HostingProduct $product): array { $cycles = [ CustomerHostingOrder::CYCLE_MONTHLY, CustomerHostingOrder::CYCLE_QUARTERLY, CustomerHostingOrder::CYCLE_SEMIANNUAL, CustomerHostingOrder::CYCLE_YEARLY, ]; $prices = []; foreach ($cycles as $cycle) { $price = $product->getPriceForCycle($cycle); if ($price !== null) { $prices[$cycle] = (float) $price; } } return $prices; } /** * @return array */ public function setupFeesByCycle(HostingProduct $product): array { $fees = []; foreach (array_keys($this->baseCyclePrices($product)) as $cycle) { $fees[$cycle] = $this->setupFeeForCycle($product, $cycle); } return $fees; } public function setupFeeForCycle(HostingProduct $product, string $billingCycle): float { if (! $product->requiresContabo()) { return 0.0; } $productId = trim((string) $product->contabo_product_id); if ($productId === '') { return 0.0; } foreach ((array) config('hosting.pricing.setup_fee_rules', []) as $rule) { if (! in_array($productId, array_map('strval', (array) ($rule['product_ids'] ?? [])), true)) { continue; } if (! in_array($billingCycle, array_map('strval', (array) ($rule['billing_cycles'] ?? [])), true)) { continue; } if (isset($rule['fixed_eur_by_cycle'])) { $eurAmount = (float) ($rule['fixed_eur_by_cycle'][$billingCycle] ?? 0); if ($eurAmount <= 0) { return 0.0; } $eurToGhs = app(\App\Services\ExchangeRateService::class)->getEurToGhs(); $margin = $this->pricing->getMargin($product->category); $ghs = $eurAmount * $eurToGhs * (1 + ($margin / 100)); return round($ghs / 5) * 5; } $monthlyPrice = (float) $product->getPriceForCycle(CustomerHostingOrder::CYCLE_MONTHLY); $multiplier = (float) ($rule['monthly_price_multiplier'] ?? 1); return round($monthlyPrice * $multiplier, 2); } return 0.0; } private function setupFeeLabel(HostingProduct $product, string $billingCycle): string { $productId = trim((string) $product->contabo_product_id); foreach ((array) config('hosting.pricing.setup_fee_rules', []) as $rule) { if ( in_array($productId, array_map('strval', (array) ($rule['product_ids'] ?? [])), true) && in_array($billingCycle, array_map('strval', (array) ($rule['billing_cycles'] ?? [])), true) ) { return (string) ($rule['label'] ?? 'One-time setup fee'); } } return 'One-time setup fee'; } public function cycleMonths(string $billingCycle): int { return match ($billingCycle) { CustomerHostingOrder::CYCLE_MONTHLY => 1, CustomerHostingOrder::CYCLE_QUARTERLY => 3, CustomerHostingOrder::CYCLE_SEMIANNUAL => 6, CustomerHostingOrder::CYCLE_YEARLY => 12, CustomerHostingOrder::CYCLE_BIENNIAL => 24, default => 1, }; } /** * @param array> $options * @return array|null */ private function findOption(array $options, ?string $value): ?array { foreach ($options as $option) { if ((string) ($option['value'] ?? '') === (string) $value) { return $option; } } return null; } /** * @param array> $options */ private function validatedOptionValue(array $options, string $value): string { return $this->findOption($options, $value)['value'] ?? (string) ($options[0]['value'] ?? ''); } /** * @param array> $options * @return array> */ private function optionCollection(HostingProduct $product, array $options): array { $items = []; foreach ($options as $value => $option) { if (! $this->optionIsAvailable($option, (string) $value)) { continue; } $items[] = [ 'value' => (string) $value, 'label' => (string) ($option['label'] ?? $value), 'description' => (string) ($option['description'] ?? ''), 'monthly_usd' => (float) ($option['monthly_usd'] ?? 0), 'prices' => $this->cyclePricesForRecurringUsd($product, (float) ($option['monthly_usd'] ?? 0)), 'license' => $option['license'] ?? null, 'panel' => $option['panel'] ?? null, 'application_id' => $option['application_id'] ?? null, 'cloud_init_preset' => $option['cloud_init_preset'] ?? null, 'add_on' => $option['add_on'] ?? null, 'compatible_os_families' => $this->normalizedOsFamilies($option['compatible_os_families'] ?? []), 'requires_image' => (bool) ($option['requires_image'] ?? false), 'requires_managed_stack_image' => (bool) ($option['requires_managed_stack_image'] ?? false), 'automated' => (bool) ($option['automated'] ?? false), ]; } return $items; } /** * @return array> */ private function imageOptions(HostingProduct $product): array { if ($this->availableImages !== null) { return $this->availableImages; } $images = []; try { foreach ($this->contaboProvider->getAvailableImages() as $image) { $rule = $this->matchImageRule($image); $images[] = [ 'value' => (string) $image['id'], 'label' => (string) ($image['name'] ?? ($rule['label'] ?? 'Image')), 'description' => (string) ($image['description'] ?? ($rule['label'] ?? '')), 'image_id' => (string) $image['id'], 'os_family' => (string) ($rule['os_family'] ?? 'linux'), 'default_user' => (string) ($rule['default_user'] ?? 'root'), 'monthly_usd' => (float) ($rule['monthly_usd'] ?? 0), 'prices' => $this->cyclePricesForRecurringUsd($product, (float) ($rule['monthly_usd'] ?? 0)), 'automated' => true, 'add_on' => ! empty($rule['requires_custom_image_addon']) ? 'customImage' : null, ]; } } catch (\Throwable) { // Fall back to configured image list when the API is unavailable. } if ($images !== []) { usort($images, fn (array $a, array $b): int => strcmp((string) ($a['label'] ?? ''), (string) ($b['label'] ?? ''))); return $this->availableImages = $this->prependInstallLaterImage($product, $images); } $fallback = []; foreach ((array) config('hosting.server_order.fallback_images', []) as $image) { $fallback[] = [ 'value' => (string) ($image['value'] ?? ''), 'label' => (string) ($image['label'] ?? 'Image'), 'description' => (string) ($image['description'] ?? ''), 'image_id' => (string) ($image['value'] ?? ''), 'os_family' => (string) ($image['os_family'] ?? 'linux'), 'default_user' => (string) ($image['default_user'] ?? 'root'), 'monthly_usd' => (float) ($image['monthly_usd'] ?? 0), 'prices' => $this->cyclePricesForRecurringUsd($product, (float) ($image['monthly_usd'] ?? 0)), 'automated' => (bool) ($image['automated'] ?? false), 'add_on' => ! empty($image['requires_custom_image_addon']) ? 'customImage' : null, ]; } return $this->availableImages = $this->prependInstallLaterImage($product, $fallback); } /** * @param array $image * @return array */ private function matchImageRule(array $image): array { $name = strtolower(trim((string) ($image['name'] ?? ''))); $standardImage = (bool) ($image['standard_image'] ?? true); $rules = (array) config('hosting.server_order.image_pricing_rules', []); foreach ($rules as $rule) { $isCustomRule = (bool) ($rule['custom_image'] ?? false); if ($isCustomRule && ! $standardImage) { return $rule; } foreach ((array) ($rule['match'] ?? []) as $needle) { if ($needle !== '' && str_contains($name, strtolower((string) $needle))) { return $rule; } } } return [ 'label' => $image['name'] ?? 'Image', 'monthly_usd' => 0, 'os_family' => str_contains(strtolower((string) ($image['os_type'] ?? 'linux')), 'win') ? 'windows' : 'linux', 'default_user' => str_contains(strtolower((string) ($image['os_type'] ?? 'linux')), 'win') ? 'administrator' : 'root', 'requires_custom_image_addon' => ! $standardImage, ]; } /** * @return array> */ private function defaultUserOptions(HostingProduct $product, string $osFamily): array { $configured = str_starts_with($osFamily, 'win') ? (array) config('hosting.server_order.windows_default_users', []) : (array) config('hosting.server_order.linux_default_users', []); $options = []; foreach ($configured as $value => $option) { $options[] = [ 'value' => (string) $value, 'label' => (string) ($option['label'] ?? $value), ]; } return $options; } /** * @return array> */ private function applicationOptions(HostingProduct $product): array { $configApps = (array) config('hosting.server_order.applications', []); $cachedIds = \Illuminate\Support\Facades\Cache::get('contabo_blockchain_app_ids', []); if ($this->hasMissingConfiguredApplicationIds($configApps)) { $resolvedIds = $this->resolveApplicationIdsFromCatalog($configApps); if ($resolvedIds !== []) { $cachedIds = array_merge($cachedIds, $resolvedIds); \Illuminate\Support\Facades\Cache::put('contabo_blockchain_app_ids', $cachedIds, now()->addDays(7)); } } foreach ($configApps as $key => $option) { $appId = $option['application_id'] ?? null; if (($appId === null || $appId === '') && isset($cachedIds[$key])) { $configApps[$key]['application_id'] = $cachedIds[$key]['id']; $appId = $configApps[$key]['application_id']; } $preset = trim((string) ($option['cloud_init_preset'] ?? '')); $resolvedAppId = trim((string) $appId); if ($key !== 'none' && $resolvedAppId === '' && $preset === '') { $configApps[$key]['allow_unresolved'] = true; $configApps[$key]['automated'] = false; $description = trim((string) ($configApps[$key]['description'] ?? '')); if ($description !== '' && ! str_contains(strtolower($description), 'manual')) { $configApps[$key]['description'] = $description.' Manual setup may be required after provisioning.'; } } } return $this->optionCollection($product, $configApps); } /** * @param array> $configApps */ private function hasMissingConfiguredApplicationIds(array $configApps): bool { foreach ($configApps as $key => $option) { if (! array_key_exists($key, self::APPLICATION_NAME_MAPPINGS)) { continue; } if (trim((string) ($option['application_id'] ?? '')) === '') { return true; } } return false; } /** * @param array> $configApps * @return array */ private function resolveApplicationIdsFromCatalog(array $configApps): array { try { $applications = $this->contaboProvider->getAvailableApplications(); } catch (\Throwable) { return []; } $mapped = []; foreach (self::APPLICATION_NAME_MAPPINGS as $configKey => $searchTerms) { if (! isset($configApps[$configKey])) { continue; } if (trim((string) ($configApps[$configKey]['application_id'] ?? '')) !== '') { continue; } foreach ($applications as $app) { $name = strtolower((string) ($app['name'] ?? '')); foreach ($searchTerms as $term) { if ($term !== '' && str_contains($name, strtolower($term))) { $mapped[$configKey] = [ 'id' => (string) ($app['id'] ?? ''), 'name' => (string) ($app['name'] ?? $configKey), 'description' => (string) ($app['description'] ?? ''), ]; break 2; } } } } return array_filter($mapped, static fn (array $app): bool => $app['id'] !== ''); } /** * @param array> $images * @return array|null */ private function defaultImageOption(array $images): ?array { foreach ($images as $image) { if (! ($image['is_install_later'] ?? false)) { return $image; } } return $images[0] ?? null; } /** * @param array> $images * @return array> */ private function prependInstallLaterImage(HostingProduct $product, array $images): array { array_unshift($images, [ 'value' => self::INSTALL_LATER_IMAGE, 'label' => 'Install Later', 'description' => 'Provision the server first and choose the final OS installation afterward.', 'image_id' => null, 'os_family' => 'linux', 'default_user' => 'root', 'monthly_usd' => 0.0, 'prices' => $this->cyclePricesForRecurringUsd($product, 0.0), 'automated' => true, 'add_on' => null, 'is_install_later' => true, ]); return $images; } /** * @param array $selection * @param array $catalog */ private function assertCompatibleSelections(array $selection, array $catalog): void { $image = $this->findOption($catalog['images'], (string) ($selection['image'] ?? '')); $license = $this->findOption($catalog['licenses'], (string) ($selection['license'] ?? '')); $application = $this->findOption($catalog['applications'], (string) ($selection['application'] ?? '')); $errors = []; if (! $this->optionSupportsImage($license, $image)) { $errors['license'] = 'The selected control panel is not compatible with this image.'; } if (! $this->optionSupportsImage($application, $image)) { $errors['application'] = 'The selected preinstalled app is not compatible with this image.'; } if ($errors !== []) { throw ValidationException::withMessages($errors); } } /** * @param array|null $option * @param array|null $image */ private function optionSupportsImage(?array $option, ?array $image): bool { if (! $option) { return true; } $imageLabel = strtolower((string) ($image['label'] ?? '')); $bundledPanel = $this->bundledPanelFromImageLabel($imageLabel); $optionLicense = strtolower((string) ($option['license'] ?? '')); if (($option['requires_image'] ?? false) && ($image['is_install_later'] ?? false)) { return false; } if (($option['requires_managed_stack_image'] ?? false) && ! $this->imageSupportsManagedStack($image)) { return false; } if ($bundledPanel !== null && (string) ($option['value'] ?? '') === 'none') { return false; } if ($bundledPanel === 'cpanel' && str_starts_with($optionLicense, 'plesk')) { return false; } if ($bundledPanel === 'plesk' && str_starts_with($optionLicense, 'cpanel')) { return false; } $families = (array) ($option['compatible_os_families'] ?? []); if ($families === []) { return true; } $osFamily = strtolower((string) ($image['os_family'] ?? 'linux')); foreach ($families as $family) { if ($family !== '' && str_starts_with($osFamily, strtolower((string) $family))) { return true; } } return false; } /** * @param array|null $image */ private function imageSupportsManagedStack(?array $image): bool { if (! $image || ($image['is_install_later'] ?? false)) { return false; } $imageLabel = strtolower((string) ($image['label'] ?? '')); $osFamily = strtolower((string) ($image['os_family'] ?? 'linux')); if (str_starts_with($osFamily, 'win') || $this->imageIncludesVendorPanel($imageLabel)) { return false; } foreach ((array) config('hosting.server_order.managed_stack_supported_images', []) as $supported) { $supportedOsFamily = strtolower((string) ($supported['os_family'] ?? 'linux')); if ($supportedOsFamily !== '' && $supportedOsFamily !== $osFamily) { continue; } foreach ((array) ($supported['match'] ?? []) as $needle) { $needle = strtolower(trim((string) $needle)); if ($needle !== '' && str_contains($imageLabel, $needle)) { return true; } } } return false; } private function imageIncludesVendorPanel(string $imageLabel): bool { return $this->bundledPanelFromImageLabel($imageLabel) !== null; } private function bundledPanelFromImageLabel(string $imageLabel): ?string { if (str_contains($imageLabel, 'cpanel') || str_contains($imageLabel, 'whm')) { return 'cpanel'; } if (str_contains($imageLabel, 'plesk')) { return 'plesk'; } return null; } /** * @param array $option */ private function optionIsAvailable(array $option, string $value): bool { if ($value === 'none') { return true; } if (! empty($option['allow_unresolved'])) { return true; } $applicationId = trim((string) ($option['application_id'] ?? '')); $preset = trim((string) ($option['cloud_init_preset'] ?? '')); if ($applicationId !== '' || $preset !== '') { return true; } return array_key_exists('application_id', $option) || array_key_exists('cloud_init_preset', $option) ? false : true; } /** * @param mixed $families * @return array */ private function normalizedOsFamilies(mixed $families): array { $items = is_array($families) ? $families : []; return array_values(array_filter(array_map( static fn ($family): string => strtolower(trim((string) $family)), $items ))); } /** * @param array $option */ private function cloudInitForOption(array $option, string $defaultUser): ?string { $preset = (string) ($option['cloud_init_preset'] ?? ''); if ($preset === '') { return null; } return match ($preset) { 'webmin' => $this->contaboProvider->generateCloudInit([ 'packages' => ['curl', 'wget'], 'run_commands' => [ 'bash -lc \'if command -v apt-get >/dev/null 2>&1; then curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/setup-repos.sh | sh && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y webmin; elif command -v dnf >/dev/null 2>&1; then dnf install -y perl perl-Net-SSLeay openssl perl-Encode-Detect perl-Data-Dumper && curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/setup-repos.sh | sh && dnf install -y webmin; elif command -v yum >/dev/null 2>&1; then yum install -y perl perl-Net-SSLeay openssl perl-Encode-Detect perl-Data-Dumper && curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/setup-repos.sh | sh && yum install -y webmin; fi\'', ], ]), 'webmin_lamp' => $this->contaboProvider->generateCloudInit([ 'packages' => ['curl', 'wget'], 'run_commands' => [ 'bash -lc \'if command -v apt-get >/dev/null 2>&1; then apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y apache2 mariadb-server php libapache2-mod-php php-mysql curl wget && curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/setup-repos.sh | sh && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y webmin; elif command -v dnf >/dev/null 2>&1; then dnf install -y httpd mariadb-server php php-mysqlnd curl wget perl perl-Net-SSLeay openssl perl-Encode-Detect perl-Data-Dumper && curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/setup-repos.sh | sh && dnf install -y webmin; elif command -v yum >/dev/null 2>&1; then yum install -y httpd mariadb-server php php-mysqlnd curl wget perl perl-Net-SSLeay openssl perl-Encode-Detect perl-Data-Dumper && curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/setup-repos.sh | sh && yum install -y webmin; fi && (systemctl enable --now apache2 || systemctl enable --now httpd || true) && (systemctl enable --now mariadb || systemctl enable --now mysqld || true)\'', ], ]), default => $this->contaboProvider->generateCloudInit([ 'packages' => ['curl', 'wget'], 'run_commands' => [ sprintf('echo "Preset %s selected for %s"', $preset, $defaultUser), ], ]), }; } /** * @param array|null $option * @return array|null */ private function selectionSummaryItem(string $key, string $label, ?array $option, bool $priceOptional = false): ?array { if (! $option) { return null; } $amount = (float) ($option['prices'][CustomerHostingOrder::CYCLE_MONTHLY] ?? 0); if (! $priceOptional && ($option['label'] ?? null) === null) { return null; } return [ 'key' => $key, 'label' => $label, 'value' => (string) ($option['label'] ?? $option['value'] ?? ''), 'description' => (string) ($option['description'] ?? ''), 'automated' => (bool) ($option['automated'] ?? true), 'monthly_amount' => round($amount, 2), ]; } /** * @return array */ private function cyclePricesForRecurringUsd(HostingProduct $product, float $monthlyUsd): array { $monthlyGhs = $monthlyUsd > 0 ? $this->pricing->convertUsdRecurringOption($monthlyUsd, $product->category) : 0.0; return [ CustomerHostingOrder::CYCLE_MONTHLY => round($monthlyGhs, 2), CustomerHostingOrder::CYCLE_QUARTERLY => round($monthlyGhs * 3, 2), CustomerHostingOrder::CYCLE_SEMIANNUAL => round($monthlyGhs * 6, 2), CustomerHostingOrder::CYCLE_YEARLY => round($monthlyGhs * 12, 2), ]; } }