exchangeRateService = $exchangeRateService; $this->contaboProvider = $contaboProvider; $this->margins = config('hosting.pricing.margins', [ 'vps' => 30, 'dedicated' => 30, ]); $this->contaboBasePrices = config('hosting.pricing.contabo_base_prices', []); } public function getExchangeRate(): float { return $this->exchangeRateService->getUsdToGhs(); } public function getMargin(string $category): float { return (float) ($this->margins[$category] ?? 30); } public function getContaboBasePrice(?string $productId): ?array { if ($productId === null) { return null; } $productId = trim($productId); if ($productId === '') { return null; } $cacheKey = "contabo_product_price_{$productId}"; return Cache::remember($cacheKey, (int) config('hosting.pricing.contabo_price_cache_ttl', 3600), function () use ($productId) { $liveProduct = $this->getLiveContaboProduct($productId); if ($liveProduct !== null) { return $liveProduct + ['source' => 'contabo_api']; } $fallback = $this->contaboBasePrices[$productId] ?? null; return is_array($fallback) ? $fallback + ['source' => 'local_fallback'] : null; }); } public function calculatePrice(float $basePriceUsd, string $category): float { $margin = $this->getMargin($category); $exchangeRate = $this->getExchangeRate(); $convertedPrice = $basePriceUsd * $exchangeRate; $priceWithMargin = $convertedPrice * (1 + ($margin / 100)); // Round to nearest 5 for cleaner pricing return round($priceWithMargin / 5) * 5; } public function calculateQuarterlyPrice(float $monthlyPrice): float { return $this->calculateTermPrice($monthlyPrice, 3, 'quarterly'); } public function calculateSemiannualPrice(float $monthlyPrice): float { return $this->calculateTermPrice($monthlyPrice, 6, 'semiannual'); } public function calculateYearlyPrice(float $monthlyPrice): float { return $this->calculateTermPrice($monthlyPrice, 12, 'yearly'); } public function convertUsdRecurringOption(float $monthlyUsd, string $category): float { $margin = $this->getMargin($category); $exchangeRate = $this->getExchangeRate(); return round($monthlyUsd * $exchangeRate * (1 + ($margin / 100)), 2); } private function calculateTermPrice(float $monthlyPrice, int $months, string $cycle): float { $discountPercent = (float) config("hosting.pricing.term_discounts.{$cycle}", 0); $total = $monthlyPrice * $months * (1 - ($discountPercent / 100)); return round($total / 5) * 5; } public function getDynamicPriceForProduct(HostingProduct $product): array { // Only calculate dynamic pricing for VPS and Dedicated if (!in_array($product->category, ['vps', 'dedicated'])) { return [ 'monthly' => (float) $product->price_monthly, 'quarterly' => (float) $product->price_quarterly, 'semiannual' => null, 'yearly' => (float) $product->price_yearly, 'currency' => $product->display_currency, 'is_dynamic' => false, ]; } $exchangeRate = $this->getExchangeRate(); $cacheKey = "hosting_price_{$product->slug}_{$product->contabo_product_id}_{$exchangeRate}"; return Cache::remember($cacheKey, 3600, function () use ($product, $exchangeRate) { $contaboProduct = $this->getContaboBasePrice($product->contabo_product_id); if (!$contaboProduct) { // Fallback to stored prices if no Contabo mapping return [ 'monthly' => (float) $product->price_monthly, 'quarterly' => (float) $product->price_quarterly, 'semiannual' => null, 'yearly' => (float) $product->price_yearly, 'currency' => $product->display_currency, 'is_dynamic' => false, ]; } $basePriceUsd = $contaboProduct['monthly']; $monthlyPrice = $this->calculatePrice($basePriceUsd, $product->category); return [ 'monthly' => $monthlyPrice, 'quarterly' => $this->calculateQuarterlyPrice($monthlyPrice), 'semiannual' => $this->calculateSemiannualPrice($monthlyPrice), 'yearly' => $this->calculateYearlyPrice($monthlyPrice), 'currency' => 'GHS', 'is_dynamic' => true, 'base_price_usd' => $basePriceUsd, 'exchange_rate' => $exchangeRate, 'margin_percent' => $this->getMargin($product->category), ]; }); } public function getPricingBreakdown(HostingProduct $product): array { if (!in_array($product->category, ['vps', 'dedicated'])) { return []; } $contaboProduct = $this->getContaboBasePrice($product->contabo_product_id); if (!$contaboProduct) { return []; } $basePriceUsd = $contaboProduct['monthly']; $margin = $this->getMargin($product->category); $exchangeRate = $this->getExchangeRate(); $convertedPrice = $basePriceUsd * $exchangeRate; $marginAmount = $convertedPrice * ($margin / 100); $finalPrice = $this->calculatePrice($basePriceUsd, $product->category); return [ 'contabo_price_usd' => $basePriceUsd, 'price_source' => (string) ($contaboProduct['source'] ?? 'unknown'), 'exchange_rate' => $exchangeRate, 'converted_ghs' => round($convertedPrice, 2), 'margin_percent' => $margin, 'margin_amount_ghs' => round($marginAmount, 2), 'final_price_ghs' => $finalPrice, 'profit_per_month_ghs' => round($finalPrice - $convertedPrice, 2), ]; } public function getExchangeRateInfo(): array { return $this->exchangeRateService->getRateInfo(); } public function refreshExchangeRate(): float { return $this->exchangeRateService->refreshRate(); } private function getLiveContaboProduct(string $productId): ?array { try { foreach ($this->contaboProvider->getAvailableProducts() as $product) { if ((string) ($product['id'] ?? '') !== $productId) { continue; } $monthlyUsd = $this->extractMonthlyUsdPrice($product); if ($monthlyUsd === null || $monthlyUsd <= 0) { return null; } return [ 'monthly' => $monthlyUsd, 'name' => (string) ($product['name'] ?? $productId), 'cpu' => isset($product['cpu']) ? (int) $product['cpu'] : null, 'ram_gb' => isset($product['ram_gb']) ? (float) $product['ram_gb'] : null, 'disk_gb' => isset($product['disk_gb']) ? (float) $product['disk_gb'] : null, ]; } } catch (\Throwable $exception) { Log::warning('Contabo product price lookup failed; falling back to configured prices.', [ 'product_id' => $productId, 'message' => $exception->getMessage(), ]); } return null; } private function extractMonthlyUsdPrice(array $product): ?float { foreach (['price_monthly', 'monthly', 'monthly_usd', 'monthlyUsd', 'monthlyPrice', 'price'] as $key) { if (! isset($product[$key]) || ! is_numeric($product[$key])) { continue; } return (float) $product[$key]; } return null; } }