Files
isaaccladandCursor b6c8ac343f Extract Ladill Servers as standalone app at servers.ladill.com.
VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 19:18:30 +00:00

286 lines
8.8 KiB
PHP

<?php
namespace App\Services\Domain;
use App\Models\DomainPricing;
use App\Services\Currency\CurrencyConverterService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class DomainPricingService
{
private const TLD_PRICING_CACHE_KEY = 'dynadot:tld_pricing';
private const LIVE_TLD_PRICING_CACHE_KEY = 'dynadot:tld_pricing:live';
private const TLD_PRICING_CACHE_TTL_HOURS = 6;
public function __construct(
private DynadotService $dynadot,
private CurrencyConverterService $currency,
) {}
/**
* Get the markup percentage from config.
*/
public function getMarkupPercent(): float
{
return (float) config('mailinfra.dynadot_price_markup_percent', 35);
}
/**
* Get pricing for a specific TLD in GHS with markup.
*
* @return array{register: int, renew: int, transfer: int, currency: string, usd_rate: float}|null
*/
public function getPricingForTld(string $tld): ?array
{
$tld = ltrim(strtolower(trim($tld)), '.');
$allPricing = $this->getAllTldPricing();
if (! isset($allPricing[$tld])) {
return null;
}
return $allPricing[$tld];
}
/**
* Get pricing for multiple TLDs.
*
* @return array<string, array{register: int, renew: int, transfer: int, currency: string}>
*/
public function getPricingForTlds(array $tlds): array
{
$allPricing = $this->getAllTldPricing();
$result = [];
foreach ($tlds as $tld) {
$tld = ltrim(strtolower(trim($tld)), '.');
if (isset($allPricing[$tld])) {
$result[$tld] = $allPricing[$tld];
}
}
return $result;
}
/**
* Get only TLDs that currently have live Dynadot pricing.
*
* @return list<string>
*/
public function getLivePricedTlds(): array
{
return array_keys($this->getLiveDynadotPricing());
}
/**
* Get all TLD pricing with conversion and markup applied.
* Price overrides from DomainPricing model take precedence.
*
* @return array<string, array{register: int, renew: int, transfer: int, currency: string, usd_rate: float, is_override: bool}>
*/
public function getAllTldPricing(): array
{
$cacheKey = self::TLD_PRICING_CACHE_KEY . ':ghs';
return Cache::remember($cacheKey, now()->addHours(self::TLD_PRICING_CACHE_TTL_HOURS), function () {
$usdPricing = $this->fetchDynadotPricing();
$overrides = DomainPricing::getOverrides();
if (empty($usdPricing) && empty($overrides)) {
return [];
}
$markup = $this->getMarkupPercent();
$usdRate = $this->currency->getUsdToGhsRate();
$converted = [];
// First, add all Dynadot prices with conversion
foreach ($usdPricing as $tld => $prices) {
$converted[$tld] = [
'register' => $this->convertPrice($prices['register'] ?? 0, $markup),
'renew' => $this->convertPrice($prices['renew'] ?? 0, $markup),
'transfer' => $this->convertPrice($prices['transfer'] ?? 0, $markup),
'currency' => 'GHS',
'usd_rate' => $usdRate,
'usd_register' => $prices['register'] ?? 0,
'usd_renew' => $prices['renew'] ?? 0,
'usd_transfer' => $prices['transfer'] ?? 0,
'is_override' => false,
];
}
// Apply overrides (manual GHS prices take precedence)
foreach ($overrides as $tld => $override) {
$converted[$tld] = [
'register' => $override['register'],
'renew' => $override['renew'],
'transfer' => $override['transfer'],
'currency' => 'GHS',
'usd_rate' => $usdRate,
'usd_register' => null,
'usd_renew' => null,
'usd_transfer' => null,
'is_override' => true,
];
}
return $converted;
});
}
/**
* Convert USD cents to GHS pesewas with markup.
*/
private function convertPrice(int $usdCents, float $markupPercent): int
{
return $this->currency->convertCentsTopesewaswithMarkup($usdCents, $markupPercent);
}
/**
* Fetch TLD pricing from Dynadot API.
*
* @return array<string, array{register: int, renew: int, transfer: int}>
*/
private function fetchDynadotPricing(): array
{
$livePricing = $this->getLiveDynadotPricing();
if ($livePricing !== []) {
return $livePricing;
}
return $this->getFallbackPricing();
}
/**
* Fallback pricing in USD cents for common TLDs.
*/
private function getFallbackPricing(): array
{
return [
'com' => ['register' => 1099, 'renew' => 1099, 'transfer' => 1099],
'net' => ['register' => 1199, 'renew' => 1199, 'transfer' => 1199],
'org' => ['register' => 1099, 'renew' => 1099, 'transfer' => 1099],
'io' => ['register' => 3999, 'renew' => 3999, 'transfer' => 3999],
'co' => ['register' => 2999, 'renew' => 2999, 'transfer' => 2999],
'info' => ['register' => 1899, 'renew' => 1899, 'transfer' => 1899],
'biz' => ['register' => 1699, 'renew' => 1699, 'transfer' => 1699],
'xyz' => ['register' => 1299, 'renew' => 1299, 'transfer' => 1299],
'online' => ['register' => 2999, 'renew' => 2999, 'transfer' => 2999],
'store' => ['register' => 4999, 'renew' => 4999, 'transfer' => 4999],
'tech' => ['register' => 4999, 'renew' => 4999, 'transfer' => 4999],
'site' => ['register' => 2999, 'renew' => 2999, 'transfer' => 2999],
];
}
/**
* Fetch raw live TLD pricing from Dynadot without fallback expansion.
*
* @return array<string, array{register: int, renew: int, transfer: int}>
*/
private function getLiveDynadotPricing(): array
{
return Cache::remember(self::LIVE_TLD_PRICING_CACHE_KEY, now()->addHours(self::TLD_PRICING_CACHE_TTL_HOURS), function () {
if (! $this->dynadot->isConfigured()) {
Log::warning('DomainPricingService: Dynadot not configured');
return [];
}
try {
$result = $this->dynadot->getTldPricing();
if (! ($result['success'] ?? false) || empty($result['pricing'])) {
Log::warning('DomainPricingService: Failed to fetch Dynadot pricing', $result);
return [];
}
return $result['pricing'];
} catch (\Throwable $e) {
Log::error('DomainPricingService: Exception fetching pricing', ['error' => $e->getMessage()]);
return [];
}
});
}
/**
* Format price for display.
*/
public function formatPrice(int $pesewas): string
{
return 'GH₵ ' . number_format($pesewas / 100, 2);
}
public function convertRegistrarAmountToGhsMinor(int $amountMinor, string $currency): ?int
{
$currency = strtoupper(trim($currency));
if ($currency === 'GHS') {
return $amountMinor;
}
if ($currency === 'USD') {
return $this->convertPrice($amountMinor, $this->getMarkupPercent());
}
return null;
}
/**
* Get price info for a domain (for checkout/cart).
*/
public function getDomainPrice(string $domain, string $action = 'register'): ?array
{
$parts = explode('.', strtolower($domain), 2);
if (count($parts) < 2) {
return null;
}
$tld = $parts[1];
$pricing = $this->getPricingForTld($tld);
if (! $pricing) {
return null;
}
$priceKey = match ($action) {
'renew' => 'renew',
'transfer' => 'transfer',
default => 'register',
};
return [
'domain' => $domain,
'tld' => $tld,
'action' => $action,
'amount_minor' => $pricing[$priceKey],
'currency' => 'GHS',
'price_label' => $this->formatPrice($pricing[$priceKey]),
];
}
/**
* Clear cached pricing.
*/
public function clearCache(): void
{
Cache::forget(self::TLD_PRICING_CACHE_KEY . ':ghs');
Cache::forget(self::LIVE_TLD_PRICING_CACHE_KEY);
}
/**
* Get current exchange rate info.
*/
public function getExchangeRateInfo(): array
{
return [
...$this->currency->getRateInfo(),
'markup_percent' => $this->getMarkupPercent(),
];
}
}