Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user