baseUrl = $host; $this->integrationId = (string) ($params['serverusername'] ?? ''); $this->apiSecret = (string) ($params['serverpassword'] ?? ''); if ($this->baseUrl === '' || $this->integrationId === '' || $this->apiSecret === '') { throw new RuntimeException('GameCloud server hostname, integration ID and API secret are required'); } } public function health(): array { return $this->request('GET', '/api/v1/integrations/whmcs/health'); } public function upsertClient(string $externalClientId, array $payload): array { return $this->request( 'PUT', '/api/v1/integrations/whmcs/clients/' . rawurlencode($externalClientId), $payload, 'client:' . $externalClientId ); } public function provisionService(array $payload): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services', $payload, 'service:provision:' . $payload['externalServiceId'] ); } public function suspendService(string $externalServiceId, array $payload = []): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/suspend', $payload, 'service:suspend:' . $externalServiceId ); } public function unsuspendService(string $externalServiceId): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/unsuspend', [], 'service:unsuspend:' . $externalServiceId ); } public function terminateService(string $externalServiceId): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/terminate', [], 'service:terminate:' . $externalServiceId ); } public function changePackage(string $externalServiceId, array $payload): array { return $this->request( 'PATCH', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/change-package', $payload, 'service:change-package:' . $externalServiceId ); } public function createServiceSso(string $externalServiceId, array $payload): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/sso', $payload, 'service:sso:' . $externalServiceId . ':' . ($payload['kind'] ?? 'service') ); } public function listAccounts(): array { return $this->request('GET', '/api/v1/integrations/whmcs/accounts'); } public function dashboardStats(): array { return $this->request('GET', '/api/v1/integrations/whmcs/dashboard/stats'); } public function reconcileService(string $externalServiceId, array $payload = []): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/reconcile', $payload, 'service:reconcile:' . $externalServiceId . ':' . date('Y-m-d') ); } public function reconcileAll(array $payload = []): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/reconcile', $payload, 'reconcile:all:' . date('Y-m-d') ); } public function importService(array $payload): array { return $this->request( 'POST', '/api/v1/integrations/whmcs/services/import', $payload, 'service:import:' . $payload['externalServiceId'] ); } public function listEvents(?string $cursor = null, int $limit = 50): array { $query = $cursor ? ('?cursor=' . rawurlencode($cursor) . '&limit=' . $limit) : ('?limit=' . $limit); return $this->request('GET', '/api/v1/integrations/whmcs/events' . $query); } public function acknowledgeEvents(array $eventIds, bool $deadLetter = false, ?string $reason = null): array { $payload = ['eventIds' => $eventIds, 'deadLetter' => $deadLetter]; if ($reason !== null) { $payload['deadLetterReason'] = $reason; } return $this->request('POST', '/api/v1/integrations/whmcs/events/ack', $payload); } public function getServiceUsage(string $externalServiceId, array $query = []): array { $params = http_build_query(array_filter([ 'periodStart' => $query['periodStart'] ?? null, 'periodEnd' => $query['periodEnd'] ?? null, 'testMode' => isset($query['testMode']) ? ($query['testMode'] ? 'true' : 'false') : null, ])); $suffix = $params !== '' ? ('?' . $params) : ''; return $this->request( 'GET', '/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/usage' . $suffix ); } private function request(string $method, string $pathWithQuery, array $body = [], ?string $idempotencySuffix = null): array { $pathParts = explode('?', $pathWithQuery, 2); $path = $pathParts[0]; $canonicalQuery = ''; if (isset($pathParts[1]) && $pathParts[1] !== '') { parse_str($pathParts[1], $queryParams); ksort($queryParams); $pairs = []; foreach ($queryParams as $key => $value) { $pairs[] = rawurlencode((string) $key) . '=' . rawurlencode((string) $value); } $canonicalQuery = implode('&', $pairs); } $bodyJson = $body === [] ? '' : json_encode($body, JSON_THROW_ON_ERROR); $timestamp = (string) (int) (microtime(true) * 1000); $nonce = bin2hex(random_bytes(16)); $idempotencyKey = $idempotencySuffix ?: $method . ':' . $path . ':' . $timestamp; $signatureBase = implode("\n", [ strtoupper($method), $path, $canonicalQuery, hash('sha256', $bodyJson), $timestamp, $nonce, $idempotencyKey, ]); $signature = hash_hmac('sha256', $signatureBase, $this->apiSecret); $requestUrl = $this->baseUrl . $path . ($canonicalQuery !== '' ? ('?' . $canonicalQuery) : ''); $ch = curl_init($requestUrl); if ($ch === false) { throw new RuntimeException('Unable to initialize HTTP client'); } curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', 'X-HGC-Integration-ID: ' . $this->integrationId, 'X-HGC-Timestamp: ' . $timestamp, 'X-HGC-Nonce: ' . $nonce, 'X-HGC-Idempotency-Key: ' . $idempotencyKey, 'X-HGC-Signature: ' . $signature, ], CURLOPT_POSTFIELDS => $bodyJson, ]); $responseBody = curl_exec($ch); $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($responseBody === false) { throw new RuntimeException('GameCloud request failed: ' . $curlError); } $decoded = json_decode($responseBody, true); if ($statusCode >= 400) { $message = is_array($decoded) ? ($decoded['detail'] ?? $decoded['title'] ?? $responseBody) : $responseBody; throw new RuntimeException('GameCloud API error (' . $statusCode . '): ' . $message); } return is_array($decoded) ? $decoded : []; } }