Phase F
This commit is contained in:
@@ -56,6 +56,10 @@ Handles global configuration:
|
||||
|
||||
**Phase D (SSO)** — implemented. See [docs/integrations/whmcs/sso.md](../../docs/integrations/whmcs/sso.md).
|
||||
|
||||
**Phase E (Sync & Reconciliation)** — implemented. See [docs/integrations/whmcs/reconciliation.md](../../docs/integrations/whmcs/reconciliation.md).
|
||||
|
||||
**Phase F (Usage Billing)** — implemented. See [docs/integrations/whmcs/usage-billing.md](../../docs/integrations/whmcs/usage-billing.md).
|
||||
|
||||
- Server module: `integrations/whmcs/modules/servers/hexagamecloud/`
|
||||
- Addon module: planned for global mappings and reconciliation UI
|
||||
|
||||
|
||||
@@ -134,6 +134,85 @@ function hexagamecloud_TestConnection(array $params): array
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_ListAccounts(array $params): array
|
||||
{
|
||||
try {
|
||||
$client = new HexaGameCloudApiClient($params);
|
||||
$response = $client->listAccounts();
|
||||
$accounts = [];
|
||||
|
||||
foreach ($response['accounts'] ?? [] as $account) {
|
||||
$accounts[] = [
|
||||
'uuid' => $account['externalServiceId'] ?? '',
|
||||
'domain' => $account['domain'] ?? '',
|
||||
'username' => $account['username'] ?? '',
|
||||
'status' => $account['status'] ?? 'Active',
|
||||
];
|
||||
}
|
||||
|
||||
return $accounts;
|
||||
} catch (Throwable $exception) {
|
||||
logModuleCall('hexagamecloud', __FUNCTION__, $params, $exception->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_MetricProvider(array $params)
|
||||
{
|
||||
try {
|
||||
$client = new HexaGameCloudApiClient($params);
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$usage = $client->getServiceUsage($serviceId, hexagamecloud_resolveUsageQuery($params));
|
||||
|
||||
return [
|
||||
'metrics' => $usage['metrics'] ?? [],
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
logModuleCall('hexagamecloud', __FUNCTION__, $params, $exception->getMessage());
|
||||
return ['metrics' => []];
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_UsageUpdate(array $params)
|
||||
{
|
||||
try {
|
||||
$client = new HexaGameCloudApiClient($params);
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$usage = $client->getServiceUsage($serviceId, hexagamecloud_resolveUsageQuery($params));
|
||||
$usageUpdate = $usage['usageUpdate'] ?? [];
|
||||
|
||||
return [
|
||||
'diskusage' => (float) ($usageUpdate['diskUsageGiB'] ?? 0),
|
||||
'bandwidth' => (float) ($usageUpdate['bandwidthGiB'] ?? 0),
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
logModuleCall('hexagamecloud', __FUNCTION__, $params, $exception->getMessage());
|
||||
return [
|
||||
'diskusage' => 0,
|
||||
'bandwidth' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_resolveUsageQuery(array $params): array
|
||||
{
|
||||
$query = [];
|
||||
|
||||
if (!empty($params['usageperiodstart'])) {
|
||||
$query['periodStart'] = gmdate('c', strtotime((string) $params['usageperiodstart']));
|
||||
}
|
||||
|
||||
if (!empty($params['usageperiodend'])) {
|
||||
$query['periodEnd'] = gmdate('c', strtotime((string) $params['usageperiodend']));
|
||||
}
|
||||
|
||||
if (!empty($params['testmode']) || !empty($params['configoption4'])) {
|
||||
$query['testMode'] = true;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
function hexagamecloud_lifecycleAction(array $params, string $action, array $body = []): string
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -95,8 +95,88 @@ final class HexaGameCloudApiClient
|
||||
);
|
||||
}
|
||||
|
||||
private function request(string $method, string $path, array $body = [], ?string $idempotencySuffix = null): array
|
||||
public function listAccounts(): array
|
||||
{
|
||||
return $this->request('GET', '/api/v1/integrations/whmcs/accounts');
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -105,7 +185,7 @@ final class HexaGameCloudApiClient
|
||||
$signatureBase = implode("\n", [
|
||||
strtoupper($method),
|
||||
$path,
|
||||
'',
|
||||
$canonicalQuery,
|
||||
hash('sha256', $bodyJson),
|
||||
$timestamp,
|
||||
$nonce,
|
||||
@@ -113,8 +193,9 @@ final class HexaGameCloudApiClient
|
||||
]);
|
||||
|
||||
$signature = hash_hmac('sha256', $signatureBase, $this->apiSecret);
|
||||
$requestUrl = $this->baseUrl . $path . ($canonicalQuery !== '' ? ('?' . $canonicalQuery) : '');
|
||||
|
||||
$ch = curl_init($this->baseUrl . $path);
|
||||
$ch = curl_init($requestUrl);
|
||||
if ($ch === false) {
|
||||
throw new RuntimeException('Unable to initialize HTTP client');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user