Phase F
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 10s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 18s

This commit is contained in:
smueller
2026-06-26 15:05:01 +02:00
parent 333ad1cc7d
commit 15cf302afc
23 changed files with 2116 additions and 8 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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');
}