diff --git a/src/Domains/Registrar.php b/src/Domains/Registrar.php index 394164e..54897e1 100644 --- a/src/Domains/Registrar.php +++ b/src/Domains/Registrar.php @@ -81,11 +81,12 @@ public function available(string $domain): bool * @param array|Contact $contacts * @param array $nameservers * @param bool $autorenewEnabled + * @param float|null $purchasePrice Required if domain is premium * @return string Order ID */ - public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false): string + public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false, ?float $purchasePrice = null): string { - return $this->adapter->purchase($domain, $contacts, $periodYears, $nameservers, $autorenewEnabled); + return $this->adapter->purchase($domain, $contacts, $periodYears, $nameservers, $autorenewEnabled, $purchasePrice); } /** diff --git a/src/Domains/Registrar/Adapter.php b/src/Domains/Registrar/Adapter.php index 4ec8f19..59d395d 100644 --- a/src/Domains/Registrar/Adapter.php +++ b/src/Domains/Registrar/Adapter.php @@ -95,9 +95,10 @@ abstract public function available(string $domain): bool; * @param int $periodYears * @param array $nameservers * @param bool $autorenewEnabled + * @param float|null $purchasePrice Required if domain is premium * @return string Order ID */ - abstract public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false): string; + abstract public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false, ?float $purchasePrice = null): string; /** * Suggest domain names diff --git a/src/Domains/Registrar/Adapter/Mock.php b/src/Domains/Registrar/Adapter/Mock.php index 1759235..c9eeb67 100644 --- a/src/Domains/Registrar/Adapter/Mock.php +++ b/src/Domains/Registrar/Adapter/Mock.php @@ -130,11 +130,12 @@ public function available(string $domain): bool * @param int $periodYears * @param array $nameservers * @param bool $autorenewEnabled + * @param float|null $purchasePrice Required if domain is premium * @return string Order ID * @throws DomainTakenException * @throws InvalidContactException */ - public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false): string + public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false, ?float $purchasePrice = null): string { if (!$this->available($domain)) { throw new DomainTakenException("Domain {$domain} is not available for registration", self::RESPONSE_CODE_DOMAIN_TAKEN); diff --git a/src/Domains/Registrar/Adapter/NameCom.php b/src/Domains/Registrar/Adapter/NameCom.php index 3a6a09e..744415b 100644 --- a/src/Domains/Registrar/Adapter/NameCom.php +++ b/src/Domains/Registrar/Adapter/NameCom.php @@ -170,9 +170,10 @@ public function updateNameservers(string $domain, array $nameservers): array * @param int $periodYears Registration period in years * @param array $nameservers Nameservers to use * @param bool $autorenewEnabled Whether autorenew should be enabled + * @param float|null $purchasePrice Required if domain is premium * @return string Order ID */ - public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false): string + public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false, ?float $purchasePrice = null): string { try { $contacts = \is_array($contacts) ? $contacts : [$contacts]; @@ -190,6 +191,10 @@ public function purchase(string $domain, array|Contact $contacts, int $periodYea 'years' => $periodYears, ]; + if ($purchasePrice !== null) { + $data['purchasePrice'] = $purchasePrice; + } + $result = $this->send('POST', '/core/v1/domains', $data); return (string) ($result['order'] ?? ''); @@ -320,7 +325,13 @@ public function suggest(array|string $query, array $tlds = [], int|null $limit = $purchasable = $domainResult['purchasable'] ?? false; $price = isset($domainResult['purchasePrice']) ? (float) $domainResult['purchasePrice'] : null; - $isPremium = isset($domainResult['premium']) && $domainResult['premium'] === true; + $renewalPrice = isset($domainResult['renewalPrice']) ? (float) $domainResult['renewalPrice'] : null; + $purchaseType = $domainResult['purchaseType'] ?? 'registration'; + + // Aftermarket listings (purchaseType other than 'registration') + // are premium even when the premium flag is not set + $isPremium = (isset($domainResult['premium']) && $domainResult['premium'] === true) + || ($purchaseType !== '' && $purchaseType !== 'registration'); // Apply price filters if ($price !== null) { @@ -343,6 +354,8 @@ public function suggest(array|string $query, array $tlds = [], int|null $limit = $items[$domain] = [ 'available' => $purchasable, 'price' => $price, + 'renewalPrice' => $renewalPrice, + 'purchaseType' => $purchaseType, 'type' => $isPremium ? 'premium' : 'suggestion', ]; @@ -371,6 +384,9 @@ public function getPrice(string $domain, int $periodYears = 1, string $regType = if ($this->cache) { $cached = $this->cache->load($cacheKey, $ttl); if (\is_array($cached[$regType] ?? null)) { + if (($cached[$regType]['price'] ?? null) === null) { + throw new PriceNotFoundException("Price not found for domain: {$domain}", 400); + } return new Price($cached[$regType]['price'], $cached[$regType]['premium']); } } @@ -385,13 +401,48 @@ public function getPrice(string $domain, int $periodYears = 1, string $regType = Registrar::REG_TYPE_TRANSFER => $result['transferPrice'] ?? null, ]; + // getPricing only covers standard registry registrations. Premium + // aftermarket listings are priced by the availability endpoint, so + // without this merge a premium domain is quoted at the base TLD price. + $availability = null; + $availabilityFailed = false; + try { + $availabilityResult = $this->send('POST', '/core/v1/domains:checkAvailability', [ + 'domainNames' => [$domain], + ]); + $availability = $availabilityResult['results'][0] ?? null; + } catch (RateLimitException $e) { + throw $e; + } catch (Exception $e) { + // Registry pricing is still usable for standard domains; skip + // the premium override and skip caching so the merge is + // retried on the next request + $availabilityFailed = true; + } + + $purchaseType = $availability['purchaseType'] ?? 'registration'; + if ( + !empty($availability['purchasable']) + && (($availability['premium'] ?? false) === true || ($purchaseType !== '' && $purchaseType !== 'registration')) + ) { + $isPremium = true; + if (isset($availability['purchasePrice'])) { + $priceMap[Registrar::REG_TYPE_NEW] = (float) $availability['purchasePrice']; + } + // A renewal price of 0 means name.com has no renewal data for + // the listing, so keep the registry renewal price in that case + if (!empty($availability['renewalPrice'])) { + $priceMap[Registrar::REG_TYPE_RENEWAL] = (float) $availability['renewalPrice']; + } + } + if (!array_filter($priceMap, fn ($p) => $p !== null)) { throw new PriceNotFoundException("Price not found for domain: {$domain}", 400); } - if ($this->cache) { + if ($this->cache && !$availabilityFailed) { $cacheData = array_map( - fn ($price) => ['price' => (float) ($price ?? 0), 'premium' => $isPremium], + fn ($price) => ['price' => $price !== null ? (float) $price : null, 'premium' => $isPremium], $priceMap ); $this->cache->save($cacheKey, $cacheData); diff --git a/src/Domains/Registrar/Adapter/OpenSRS.php b/src/Domains/Registrar/Adapter/OpenSRS.php index 338aeb3..e8aa082 100644 --- a/src/Domains/Registrar/Adapter/OpenSRS.php +++ b/src/Domains/Registrar/Adapter/OpenSRS.php @@ -177,7 +177,7 @@ private function register(string $domain, string $regType, array $user, array $c return $result; } - public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false): string + public function purchase(string $domain, array|Contact $contacts, int $periodYears = 1, array $nameservers = [], bool $autorenewEnabled = false, ?float $purchasePrice = null): string { try { $contacts = \is_array($contacts) ? $contacts : [$contacts]; @@ -191,7 +191,7 @@ public function purchase(string $domain, array|Contact $contacts, int $periodYea $regType = Registrar::REG_TYPE_NEW; - $result = $this->register($domain, $regType, $this->user, $contacts, $nameservers, $periodYears, null, null, $autorenewEnabled); + $result = $this->register($domain, $regType, $this->user, $contacts, $nameservers, $periodYears, null, $purchasePrice, $autorenewEnabled); $result = $this->response($result); return $result['id'];