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>
663 lines
25 KiB
PHP
663 lines
25 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hosting;
|
|
|
|
use App\Models\HostingOrder;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ResellerClubHostingService
|
|
{
|
|
private const DEFAULT_API_URL = 'https://httpapi.com/api';
|
|
|
|
private ?string $lastSyncError = null;
|
|
|
|
private string $apiUrl;
|
|
|
|
private string $hostingEndpoint;
|
|
|
|
private string $authUserId;
|
|
|
|
private string $apiKey;
|
|
|
|
private string $customerId;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->apiUrl = rtrim($this->configuredApiUrl(), '/');
|
|
$this->hostingEndpoint = trim((string) config('mailinfra.hosting_endpoint', 'singledomainhosting/linux/us'), '/');
|
|
$this->authUserId = (string) config('mailinfra.registrar_auth_userid');
|
|
$this->apiKey = (string) config('mailinfra.registrar_api_key');
|
|
$this->customerId = (string) config('mailinfra.registrar_customer_id');
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->authUserId !== '' && $this->apiKey !== '';
|
|
}
|
|
|
|
public function lastSyncError(): ?string
|
|
{
|
|
return $this->lastSyncError;
|
|
}
|
|
|
|
/**
|
|
* Build a URL with all parameters as query string values, including auth.
|
|
*
|
|
* @param array<string, mixed> $params
|
|
*/
|
|
private function buildQueryUrl(string $endpoint, array $params = []): string
|
|
{
|
|
$all = array_merge([
|
|
'auth-userid' => $this->authUserId,
|
|
'api-key' => $this->apiKey,
|
|
], $params);
|
|
|
|
$separator = str_contains($endpoint, '?') ? '&' : '?';
|
|
|
|
return $endpoint.$separator.http_build_query($all);
|
|
}
|
|
|
|
/**
|
|
* Authenticate a ResellerClub customer by email and password.
|
|
* Returns the customer ID on success, or null on failure.
|
|
*
|
|
* @return array{success: bool, customer_id?: string, message?: string}
|
|
*/
|
|
public function authenticateCustomer(string $email, string $password): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(15)->post($this->buildQueryUrl($this->apiUrl.'/customers/authenticate.json', [
|
|
'username' => $email,
|
|
'passwd' => $password,
|
|
]));
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed()) {
|
|
$message = $data['message'] ?? $data['error'] ?? 'Authentication failed.';
|
|
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
// RC returns the customer ID directly as the response body on success
|
|
$customerId = is_array($data) ? (string) ($data['customerid'] ?? '') : (string) $data;
|
|
|
|
if ($customerId === '' || $customerId === 'false') {
|
|
return ['success' => false, 'message' => 'Invalid email or password.'];
|
|
}
|
|
|
|
return ['success' => true, 'customer_id' => $customerId];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: authenticateCustomer failed', ['error' => $e->getMessage()]);
|
|
|
|
return ['success' => false, 'message' => 'Could not verify ResellerClub credentials.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get customer details by customer ID.
|
|
*
|
|
* @return array{success: bool, customer?: array, message?: string}
|
|
*/
|
|
public function getCustomerDetails(string $customerId): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(15)->get($this->apiUrl.'/customers/details-by-id.json', [
|
|
'auth-userid' => $this->authUserId,
|
|
'api-key' => $this->apiKey,
|
|
'customer-id' => $customerId,
|
|
]);
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed() || ! is_array($data)) {
|
|
return ['success' => false, 'message' => 'Could not fetch customer details.'];
|
|
}
|
|
|
|
return ['success' => true, 'customer' => $data];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: getCustomerDetails failed', ['error' => $e->getMessage()]);
|
|
|
|
return ['success' => false, 'message' => 'Could not fetch customer details.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search hosting orders for a specific customer.
|
|
*
|
|
* @return array{success: bool, orders?: array, total?: int, message?: string}
|
|
*/
|
|
public function searchOrdersByCustomer(string $customerId, int $page = 1, int $perPage = 50): array
|
|
{
|
|
$this->lastSyncError = null;
|
|
|
|
try {
|
|
$params = [
|
|
'auth-userid' => $this->authUserId,
|
|
'api-key' => $this->apiKey,
|
|
'no-of-records' => $perPage,
|
|
'page-no' => $page,
|
|
'customer-id' => $customerId,
|
|
];
|
|
|
|
$response = Http::timeout(15)->get($this->endpoint('search'), $params);
|
|
$data = $response->json();
|
|
|
|
if ($response->failed() || ! is_array($data)) {
|
|
$message = $this->responseMessage($response, $data, 'Failed to search hosting orders.');
|
|
$this->lastSyncError = $message;
|
|
|
|
Log::warning('HostingService: hosting search unsuccessful response', [
|
|
'customer_id' => $customerId,
|
|
'status' => $response->status(),
|
|
'body' => $this->sanitizeErrorText((string) $response->body()),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
$total = (int) ($data['recsonpage'] ?? 0);
|
|
$orders = [];
|
|
$recordCount = (int) ($data['recsindb'] ?? 0);
|
|
|
|
for ($i = 1; $i <= $total; $i++) {
|
|
if (isset($data[(string) $i])) {
|
|
$orders[] = $this->normalizeOrderData($data[(string) $i]);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'orders' => $orders,
|
|
'total' => $recordCount,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: searchOrdersByCustomer failed', ['error' => $e->getMessage()]);
|
|
|
|
$message = $this->syncErrorMessage($e, 'Could not fetch hosting orders.');
|
|
$this->lastSyncError = $message;
|
|
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync all orders for a specific RC customer into a Ladill user account.
|
|
*
|
|
* @return int Number of orders synced.
|
|
*/
|
|
public function syncCustomerOrders(string $customerId, int $userId): int
|
|
{
|
|
$this->lastSyncError = null;
|
|
|
|
$page = 1;
|
|
$synced = 0;
|
|
|
|
do {
|
|
$result = $this->searchOrdersByCustomer($customerId, $page, 50);
|
|
|
|
if (! ($result['success'] ?? false) || empty($result['orders'])) {
|
|
break;
|
|
}
|
|
|
|
foreach ($result['orders'] as $order) {
|
|
$payload = is_array($order) ? ($order['raw'] ?? $order) : [];
|
|
$orderId = (string) ($payload['orderid'] ?? $payload['entityid'] ?? $order['order_id'] ?? '');
|
|
|
|
if ($orderId !== '') {
|
|
$details = $this->getOrderDetails($orderId);
|
|
if (($details['success'] ?? false) && is_array($details['order'] ?? null)) {
|
|
$payload = (array) ($details['order']['raw'] ?? $details['order']);
|
|
}
|
|
}
|
|
|
|
$this->syncOrderToLocal((array) $payload, $userId);
|
|
$synced++;
|
|
}
|
|
|
|
$page++;
|
|
} while ($synced < ($result['total'] ?? 0));
|
|
|
|
return $synced;
|
|
}
|
|
|
|
/**
|
|
* Search hosting orders under the reseller account.
|
|
*
|
|
* @return array{success: bool, orders?: array, total?: int, message?: string}
|
|
*/
|
|
public function searchOrders(int $page = 1, int $perPage = 25, ?string $domainName = null, ?string $status = null): array
|
|
{
|
|
try {
|
|
$params = [
|
|
'auth-userid' => $this->authUserId,
|
|
'api-key' => $this->apiKey,
|
|
'no-of-records' => $perPage,
|
|
'page-no' => $page,
|
|
];
|
|
|
|
if ($domainName) {
|
|
$params['domain-name'] = $domainName;
|
|
}
|
|
|
|
if ($status) {
|
|
$params['status'] = $status;
|
|
}
|
|
|
|
if ($this->customerId) {
|
|
$params['customer-id'] = $this->customerId;
|
|
}
|
|
|
|
$response = Http::timeout(15)->get($this->endpoint('search'), $params);
|
|
$data = $response->json();
|
|
|
|
if ($response->failed() || ! is_array($data)) {
|
|
$message = $this->responseMessage($response, $data, 'Failed to search hosting orders.');
|
|
|
|
Log::warning('HostingService: hosting search unsuccessful response', [
|
|
'status' => $response->status(),
|
|
'body' => $this->sanitizeErrorText((string) $response->body()),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
$total = (int) ($data['recsonpage'] ?? 0);
|
|
$orders = [];
|
|
|
|
// RC returns numbered keys for each record
|
|
$recordCount = (int) ($data['recsindb'] ?? 0);
|
|
for ($i = 1; $i <= $total; $i++) {
|
|
if (isset($data[(string) $i])) {
|
|
$orders[] = $this->normalizeOrderData($data[(string) $i]);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'orders' => $orders,
|
|
'total' => $recordCount,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: searchOrders failed', ['error' => $e->getMessage()]);
|
|
|
|
return ['success' => false, 'message' => 'Could not fetch hosting orders.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get details of a specific hosting order by order ID.
|
|
*
|
|
* @return array{success: bool, order?: array, message?: string}
|
|
*/
|
|
public function getOrderDetails(string $orderId): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(15)->get($this->endpoint('details'), [
|
|
'auth-userid' => $this->authUserId,
|
|
'api-key' => $this->apiKey,
|
|
'order-id' => $orderId,
|
|
]);
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed() || ! is_array($data)) {
|
|
return ['success' => false, 'message' => 'Failed to fetch order details.'];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'order' => $this->normalizeOrderData($data),
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: getOrderDetails failed', [
|
|
'order_id' => $orderId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => 'Could not fetch order details.'];
|
|
}
|
|
}
|
|
|
|
private function configuredApiUrl(): string
|
|
{
|
|
$value = trim((string) config('mailinfra.hosting_api_url'));
|
|
|
|
return $value !== '' ? $value : self::DEFAULT_API_URL;
|
|
}
|
|
|
|
private function endpoint(string $action): string
|
|
{
|
|
return sprintf('%s/%s/%s.json', $this->apiUrl, $this->hostingEndpoint, $action);
|
|
}
|
|
|
|
/**
|
|
* Order a new single-domain Linux hosting plan.
|
|
*
|
|
* @return array{success: bool, order_id?: string, message?: string}
|
|
*/
|
|
public function orderHosting(string $domainName, string $planId, string $customerId, int $months = 1): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(30)->post($this->buildQueryUrl($this->endpoint('add'), [
|
|
'domain-name' => $domainName,
|
|
'customer-id' => $customerId,
|
|
'months' => $months,
|
|
'plan-id' => $planId,
|
|
'invoice-option' => 'NoInvoice',
|
|
]));
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed()) {
|
|
$message = $data['message'] ?? $data['error'] ?? 'Hosting order failed.';
|
|
Log::warning('HostingService: orderHosting failed', [
|
|
'domain' => $domainName,
|
|
'status' => $response->status(),
|
|
'response' => $data,
|
|
]);
|
|
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
Log::info('HostingService: hosting ordered', [
|
|
'domain' => $domainName,
|
|
'order_id' => $data['entityid'] ?? null,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'order_id' => (string) ($data['entityid'] ?? ''),
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: orderHosting exception', [
|
|
'domain' => $domainName,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => 'Hosting order failed due to a network error.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renew a hosting order.
|
|
*
|
|
* @return array{success: bool, message?: string}
|
|
*/
|
|
public function renewOrder(string $orderId, int $months = 1, string $invoiceOption = 'NoInvoice'): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(30)->post($this->buildQueryUrl($this->endpoint('renew'), [
|
|
'order-id' => $orderId,
|
|
'months' => $months,
|
|
'invoice-option' => $invoiceOption,
|
|
]));
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed()) {
|
|
return ['success' => false, 'message' => $data['message'] ?? 'Renewal failed.'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: renewOrder failed', [
|
|
'order_id' => $orderId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => 'Renewal failed due to a network error.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array{success: bool, message?: string}
|
|
*/
|
|
public function changePanelPassword(string $orderId, string $password): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(15)->post($this->buildQueryUrl($this->endpoint('change-password'), [
|
|
'order-id' => $orderId,
|
|
'new-passwd' => $password,
|
|
]));
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed()) {
|
|
return ['success' => false, 'message' => $data['message'] ?? $data['error'] ?? 'Could not update the hosting panel password.'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: changePanelPassword failed', [
|
|
'order_id' => $orderId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => 'Could not update the hosting panel password.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete (cancel) a hosting order.
|
|
*
|
|
* @return array{success: bool, message?: string}
|
|
*/
|
|
public function deleteOrder(string $orderId): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(15)->post($this->buildQueryUrl($this->endpoint('delete'), [
|
|
'order-id' => $orderId,
|
|
]));
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->failed()) {
|
|
return ['success' => false, 'message' => $data['message'] ?? 'Cancellation failed.'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Throwable $e) {
|
|
Log::error('HostingService: deleteOrder failed', [
|
|
'order_id' => $orderId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ['success' => false, 'message' => 'Cancellation failed due to a network error.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync a hosting order from ResellerClub into the local database.
|
|
*/
|
|
public function syncOrderToLocal(array $rcOrder, int $userId, ?int $domainId = null): HostingOrder
|
|
{
|
|
$raw = is_array($rcOrder['raw'] ?? null) ? (array) $rcOrder['raw'] : [];
|
|
|
|
return HostingOrder::query()->updateOrCreate(
|
|
['rc_order_id' => (string) ($rcOrder['orderid'] ?? $rcOrder['order_id'] ?? $rcOrder['entityid'] ?? Arr::get($raw, 'orders.orderid') ?? Arr::get($raw, 'entity.entityid') ?? $raw['orderid'] ?? $raw['entityid'] ?? '')],
|
|
[
|
|
'user_id' => $userId,
|
|
'domain_id' => $domainId,
|
|
'domain_name' => $rcOrder['domain_name'] ?? $rcOrder['domainname'] ?? Arr::get($raw, 'entity.description') ?? Arr::get($raw, 'domainname') ?? Arr::get($raw, 'domain-name') ?? '',
|
|
'rc_customer_id' => (string) ($rcOrder['customerid'] ?? $rcOrder['customer_id'] ?? $rcOrder['customer-id'] ?? Arr::get($raw, 'entity.customerid') ?? $raw['customerid'] ?? $raw['customer-id'] ?? ''),
|
|
'plan_name' => $rcOrder['plan_name'] ?? $rcOrder['planname'] ?? $rcOrder['productcategory'] ?? Arr::get($raw, 'entitytype.entitytypename') ?? Arr::get($raw, 'entitytype.entitytypekey') ?? $raw['planname'] ?? $raw['productcategory'] ?? null,
|
|
'hosting_type' => HostingOrder::TYPE_SINGLE_DOMAIN,
|
|
'status' => $this->mapRcStatus($rcOrder['currentstatus'] ?? $rcOrder['orderstatus'] ?? $rcOrder['status'] ?? Arr::get($raw, 'entity.currentstatus') ?? Arr::get($raw, 'orders.orderstatus') ?? 'pending'),
|
|
'ip_address' => $rcOrder['ipaddress'] ?? $rcOrder['ip'] ?? $rcOrder['ip_address'] ?? Arr::get($raw, 'server.ipaddress') ?? Arr::get($raw, 'server.ip') ?? $raw['ipaddress'] ?? $raw['ip'] ?? null,
|
|
'cpanel_url' => $rcOrder['cpanelurl'] ?? $rcOrder['cpanel_url'] ?? $rcOrder['tempurl'] ?? Arr::get($raw, 'server.cpanelurl') ?? Arr::get($raw, 'server.controlpanelurl') ?? Arr::get($raw, 'server.tempurl') ?? $raw['cpanelurl'] ?? $raw['tempurl'] ?? null,
|
|
'cpanel_username' => $rcOrder['cpanelusername'] ?? $rcOrder['cpanel_username'] ?? Arr::get($raw, 'server.cpanelusername') ?? Arr::get($raw, 'server.username') ?? $raw['cpanelusername'] ?? null,
|
|
'bandwidth_mb' => $this->unsignedLimitOrNull($rcOrder['bandwidth'] ?? Arr::get($raw, 'plan.bandwidth') ?? $raw['bandwidth'] ?? null),
|
|
'disk_space_mb' => $this->unsignedLimitOrNull($rcOrder['diskspace'] ?? Arr::get($raw, 'plan.diskspace') ?? $raw['diskspace'] ?? null),
|
|
'expires_at' => $this->hostingTimestamp($rcOrder['endtime'] ?? $rcOrder['end_time'] ?? Arr::get($raw, 'orders.endtime') ?? $raw['endtime'] ?? null),
|
|
'provisioned_at' => $this->hostingTimestamp($rcOrder['creationtime'] ?? $rcOrder['creation_time'] ?? Arr::get($raw, 'orders.creationtime') ?? $raw['creationtime'] ?? null),
|
|
'meta' => $raw !== [] ? $raw : $rcOrder,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sync all orders from ResellerClub for a user.
|
|
* If user has a linked RC account, only syncs that customer's orders.
|
|
* Otherwise syncs all orders under the reseller account.
|
|
*
|
|
* @return int Number of orders synced.
|
|
*/
|
|
public function syncAllOrders(int $userId, ?string $rcCustomerId = null): int
|
|
{
|
|
if ($rcCustomerId) {
|
|
return $this->syncCustomerOrders($rcCustomerId, $userId);
|
|
}
|
|
|
|
$page = 1;
|
|
$synced = 0;
|
|
|
|
do {
|
|
$result = $this->searchOrders($page, 50);
|
|
|
|
if (! ($result['success'] ?? false) || empty($result['orders'])) {
|
|
break;
|
|
}
|
|
|
|
foreach ($result['orders'] as $order) {
|
|
$this->syncOrderToLocal($order, $userId);
|
|
$synced++;
|
|
}
|
|
|
|
$page++;
|
|
} while ($synced < ($result['total'] ?? 0));
|
|
|
|
return $synced;
|
|
}
|
|
|
|
private function normalizeOrderData(array $data): array
|
|
{
|
|
return [
|
|
'order_id' => (string) ($data['orderid'] ?? $data['entityid'] ?? $data['order-id'] ?? $data['entity-id'] ?? Arr::get($data, 'orders.orderid') ?? Arr::get($data, 'entity.entityid') ?? ''),
|
|
'domain_name' => $data['domainname'] ?? $data['domain-name'] ?? $data['domain_name'] ?? $data['domain'] ?? $data['hostname'] ?? $data['description'] ?? Arr::get($data, 'entity.description') ?? '',
|
|
'customer_id' => (string) ($data['customerid'] ?? $data['customer-id'] ?? $data['customer_id'] ?? Arr::get($data, 'entity.customerid') ?? ''),
|
|
'plan_name' => $data['planname'] ?? $data['plan_name'] ?? $data['productcategory'] ?? $data['productkey'] ?? $data['description'] ?? Arr::get($data, 'entitytype.entitytypename') ?? Arr::get($data, 'entitytype.entitytypekey') ?? '',
|
|
'status' => $data['currentstatus'] ?? $data['orderstatus'] ?? $data['status'] ?? Arr::get($data, 'entity.currentstatus') ?? Arr::get($data, 'orders.orderstatus') ?? 'Unknown',
|
|
'ip_address' => $data['ipaddress'] ?? $data['ip_address'] ?? $data['ip'] ?? Arr::get($data, 'server.ipaddress') ?? Arr::get($data, 'server.ip') ?? null,
|
|
'cpanel_url' => $data['cpanelurl'] ?? $data['cpanel_url'] ?? $data['controlpanelurl'] ?? $data['tempurl'] ?? Arr::get($data, 'server.cpanelurl') ?? Arr::get($data, 'server.controlpanelurl') ?? Arr::get($data, 'server.tempurl') ?? null,
|
|
'cpanel_username' => $data['cpanelusername'] ?? $data['cpanel_username'] ?? $data['username'] ?? Arr::get($data, 'server.cpanelusername') ?? Arr::get($data, 'server.username') ?? null,
|
|
'bandwidth' => $data['bandwidth'] ?? $data['bandwidth_mb'] ?? Arr::get($data, 'plan.bandwidth') ?? null,
|
|
'disk_space' => $data['diskspace'] ?? $data['disk_space_mb'] ?? Arr::get($data, 'plan.diskspace') ?? null,
|
|
'creation_time' => $data['creationtime'] ?? $data['creation_time'] ?? Arr::get($data, 'orders.creationtime') ?? null,
|
|
'end_time' => $data['endtime'] ?? $data['end_time'] ?? $data['expirytime'] ?? Arr::get($data, 'orders.endtime') ?? null,
|
|
'raw' => $data,
|
|
];
|
|
}
|
|
|
|
private function mapRcStatus(string $rcStatus): string
|
|
{
|
|
return match (strtolower($rcStatus)) {
|
|
'active', 'active (paid)' => HostingOrder::STATUS_ACTIVE,
|
|
'suspended' => HostingOrder::STATUS_SUSPENDED,
|
|
'deleted', 'cancelled' => HostingOrder::STATUS_CANCELLED,
|
|
'expired' => HostingOrder::STATUS_EXPIRED,
|
|
'pending', 'inactive' => HostingOrder::STATUS_PENDING,
|
|
default => HostingOrder::STATUS_PENDING,
|
|
};
|
|
}
|
|
|
|
private function syncErrorMessage(\Throwable $e, string $fallback): string
|
|
{
|
|
$message = $e->getMessage();
|
|
|
|
if (str_contains($message, 'Could not resolve host')) {
|
|
return 'Could not reach the ResellerClub API host. Check server DNS/network access to httpapi.com.';
|
|
}
|
|
|
|
return $fallback;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $data
|
|
*/
|
|
private function responseMessage(Response $response, mixed $data, string $fallback): string
|
|
{
|
|
if ($this->looksLikeHtmlError($response)) {
|
|
return 'The upstream provisioning service temporarily blocked or rejected the request. Please try again in a moment.';
|
|
}
|
|
|
|
if (is_array($data)) {
|
|
foreach (['message', 'error', 'description', 'msg'] as $key) {
|
|
$value = $this->sanitizeErrorText((string) ($data[$key] ?? ''));
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
$rawBody = $this->sanitizeErrorText((string) $response->body());
|
|
|
|
return $rawBody !== '' ? $rawBody : $fallback;
|
|
}
|
|
|
|
private function sanitizeErrorText(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
|
|
if ($this->looksLikeHtmlDocument($value)) {
|
|
return 'ResellerClub temporarily blocked or rejected the request. Please try again in a moment.';
|
|
}
|
|
|
|
return trim(preg_replace('/\s+/', ' ', strip_tags($value)) ?? '');
|
|
}
|
|
|
|
private function looksLikeHtmlError(Response $response): bool
|
|
{
|
|
$contentType = strtolower((string) $response->header('Content-Type'));
|
|
$body = trim((string) $response->body());
|
|
|
|
return str_contains($contentType, 'text/html') || $this->looksLikeHtmlDocument($body);
|
|
}
|
|
|
|
private function looksLikeHtmlDocument(string $value): bool
|
|
{
|
|
$normalized = strtolower(ltrim($value));
|
|
|
|
return str_starts_with($normalized, '<!doctype html')
|
|
|| str_starts_with($normalized, '<html')
|
|
|| str_contains($normalized, '<body')
|
|
|| str_contains($normalized, '<head')
|
|
|| str_contains($normalized, '<title>')
|
|
|| str_contains($normalized, 'cloudflare');
|
|
}
|
|
|
|
private function hostingTimestamp(mixed $value): ?\Carbon\Carbon
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_numeric($value)) {
|
|
return \Carbon\Carbon::createFromTimestamp((int) $value);
|
|
}
|
|
|
|
try {
|
|
return \Carbon\Carbon::parse((string) $value);
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function unsignedLimitOrNull(mixed $value): ?int
|
|
{
|
|
if (! is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
$normalized = (int) $value;
|
|
|
|
return $normalized >= 0 ? $normalized : null;
|
|
}
|
|
}
|