conn->query($query)->fetch_assoc(); $this->inserve_url = $result['source_url']; $this->inserve_token = $result['source_auth_token']; $this->inserve_source_uuid = $result['source_uuid']; $this->inserve_custom_data = json_decode($result['source_custom_data'], 1)['data']; } public function execCurl() { $this->response = curl_exec($this->ch); $this->httpCode = curl_getinfo($this->ch, CURLINFO_HTTP_CODE); curl_close($this->ch); } public function returnResponse() { $this->apiOutput($this->httpCode, json_decode($this->response, true)); } public function authMe() { $this->ch = curl_init($this->inserve_url . 'auth/me'); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); } public function getLinkedCompanies() { # init the clouddistrubutor name if (!isset($this->inserve_custom_data['clouddistrubutor']) || $this->inserve_custom_data['clouddistrubutor'] === '') { $this->apiOutput(400, ['error' => 'missing clouddistrubutor in custom source data']); } else { $this->cloudDistrubutor = $this->inserve_custom_data['clouddistrubutor']; } $this->ch = curl_init($this->inserve_url . 'cloud-distributors/' . $this->cloudDistrubutor . '/companies'); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); } public function getProducts() { $this->ch = curl_init($this->inserve_url . 'products/'); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); } public function companies($page) { // Build array the way the API expects $params = [ 'b' => [ ['orderBy' => ['name', 'ASC']], ['orderBy' => ['id', 'DESC']], ['with' => ['operator', 'country']], ['paginate' => 300], ], 'page' => $page ]; $query = http_build_query($params); $this->ch = curl_init($this->inserve_url . 'companies?' . $query); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); return json_decode($this->response, true); } public function syncCompaniesFromSentri() { # init the clouddistrubutor name if (!isset($this->inserve_custom_data['clouddistrubutor']) || $this->inserve_custom_data['clouddistrubutor'] === '') { $this->apiOutput(400, ['error' => 'missing clouddistrubutor in custom source data']); } else { $this->cloudDistrubutor = $this->inserve_custom_data['clouddistrubutor']; } # First retrieve all the active companies to sync to the Inserve cloud distributor $companies = []; $sql = "SELECT company_source_id FROM companies WHERE company_state = 'active'"; $stmt = $this->conn->query($sql); while ($row = $stmt->fetch_assoc()) { $id = (int)$row['company_source_id']; $companies[] = [ 'cloud_distribution_id' => (string)$id, 'company_id' => $id ]; } $url = $this->inserve_url . 'cloud-distributors/' . $this->cloudDistrubutor . '/companies'; $this->ch = curl_init($url); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($companies), CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json", "Content-Type: application/json" ], ]); $this->execCurl(); } public function getCloudSubscriptions() { $this->ch = curl_init($this->inserve_url . 'cloud-distribution-subscriptions/'); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); } public function getSubscriptions() { $this->ch = curl_init($this->inserve_url . 'subscriptions/'); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); } public function updateCloudSubscription($subscriptionId = false, $payload = false) { $url = $this->inserve_url . 'cloud-distribution-subscriptions/' . $subscriptionId; $this->ch = curl_init($url); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json", "Content-Type: application/json" ], ]); $this->execCurl(); } public function updateSubscription($subscriptionId = false, $payload = false) { $url = $this->inserve_url . 'subscriptions/' . $subscriptionId; $this->ch = curl_init($url); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json", "Content-Type: application/json" ], ]); $this->execCurl(); } private function getAllTypes($type) { $allowedColumns = [ 'server_licenses', 'server_backup' ]; if (!in_array($type, $allowedColumns, true)) { throw new Exception('Invalid column name'); } $query = "SELECT `$type` FROM servers"; $stmt = $this->prepareStatement($query); $this->executeStatement($stmt); $result = $stmt->get_result(); $servers = []; while ($row = $result->fetch_assoc()) { array_push($servers, $row); } $allTypes = []; foreach ($servers as $server) { if (!empty($server[$type])) { $types = json_decode($server[$type], true); if (is_array($types)) { foreach ($types as $item) { foreach ($item as $key => $value) { $allTypes[$key . '.' . $value] = 0; } } } } } return $allTypes; } private function calculateTotalDiskUsage($diskJson) { $disks = json_decode($diskJson, true); $server_disks_count = 0; if (is_array($disks)) { foreach ($disks as $disk) { $server_disks_count += $disk['disk_space']; } } if (is_array($disks) && count($disks) > 0) { $sizes = array_column($disks, 'disk_space'); $server_disks_count = array_sum($sizes); } return $server_disks_count; } private function buildCountObject(string $serverUuid, string $key): array { return [ 'countSentri' => 0, 'countInserve' => 0, 'sentriCompanyId' => 0, 'SentriStatus' => 0, 'subscriptionInserveExists' => false, 'subscriptionInserveId' => false, 'subscriptionInserveCompanyId' => false, 'subscriptionInserveName' => false, 'subscriptionInserveStatus' => 0, 'md5' => md5($serverUuid . ':' . $key), ]; } private function transformTypes(array $types, string $serverUuid): array { $result = []; foreach ($types as $key => $value) { $result[$key] = $this->buildCountObject($serverUuid, $key); } return $result; } private function buildCountArray($serverUuid) { $allBackupTypes = $this->getAllTypes('server_backup'); $allLicenseTypes = $this->getAllTypes('server_licenses'); $backupCounts = $this->transformTypes($allBackupTypes, $serverUuid); $licenseCounts = $this->transformTypes($allLicenseTypes, $serverUuid); return array_merge( [ "server_CPU_count" => $this->buildCountObject($serverUuid, 'server_cpu_count'), "server_Memory_count" => $this->buildCountObject($serverUuid, 'server_memory_count'), "server_Disk_space_count" => $this->buildCountObject($serverUuid, 'server_disks_count'), ], $licenseCounts, $backupCounts ); } public function syncServerLicencesToInserve() { # Get all the linked companies $this->getLinkedCompanies(); $allCompanies = json_decode($this->response, true); $allCompaniesIds = array_column($allCompanies['matched'], 'id', 'company_id'); # first get the current subscriptions $this->getCloudSubscriptions(); $allInserveCloudSubscriptions = json_decode($this->response, true); # Filter out all the none Sentri posted subscriptions based on the name for performance $allInserveCloudSubscriptions = array_filter($allInserveCloudSubscriptions, function ($subscription) { return isset($subscription['cloud_subscription_id']) && $subscription['cloud_subscription_id'] === 'sentri-servers'; }); # Build lookup of existing Inserve subscriptions by cloud_distribution_id # this will be used later to lookup $inserveLookup = []; foreach ($allInserveCloudSubscriptions as $subscription) { if (!empty($subscription['cloud_distribution_id'])) { $inserveLookup[$subscription['cloud_distribution_id']] = [ 'id' => (int)$subscription['id'], 'quantity' => (int)$subscription['quantity'], 'status' => (int)$subscription['status'], 'cloud_distribution_company_id' => (int)$subscription['cloud_distribution_company_id'], 'name' => $subscription['name'], ]; } } # get all the servers from Sentri $sql = "SELECT * FROM servers INNER JOIN companies ON servers.company_uuid = companies.company_uuid WHERE company_state = 'active' AND server_state != 'new' AND server_state != 'disabled' "; $stmt = $this->conn->query($sql); while ($row = $stmt->fetch_assoc()) { # Create a count of all the Subscriptions possible with every count on 0 $subscriptionCounts = $this->buildCountArray($row['server_uuid']); $totalDiskSpace = $this->calculateTotalDiskUsage($row['server_disks']); # Inserve status codes are: # 0 = active, 1 = cancelled, 2 = pending, 3 = trial, 4 = on hold, 5 = removed $statusMap = [ 'active' => 0, 'trial' => 3, 'deleted' => 5, ]; // if no states matched there is something terrifying wrong, call the ambulance! if (!isset($statusMap[$row['server_state']])) { exit; } $sentriStatus = $statusMap[$row['server_state']]; # Set all the server resource counts from Sentri into the $subscriptionCounts $subscriptionCounts['server_CPU_count']['countSentri'] = $row['server_cpu']; $subscriptionCounts['server_Memory_count']['countSentri'] = (int)ceil($row['server_memory'] / 1024); $subscriptionCounts['server_Disk_space_count']['countSentri'] = $totalDiskSpace; $licenses = json_decode($row['server_licenses'], true); if (is_array($licenses)) { foreach ($licenses as $license) { foreach ($license as $key => $LicenseType) { $subscriptionCounts[$key . '.' . $LicenseType]['countSentri']++; } } } $backups = json_decode($row['server_backup'], true); foreach ($backups as $backup) { foreach ($backup as $key => $BackupType) { $subscriptionCounts[$key . '.' . $BackupType]['countSentri'] = $totalDiskSpace; } } # Mark subscriptions that already exist in Inserve foreach ($subscriptionCounts as $key => &$item) { if (!is_array($item) || !isset($item['md5'])) { continue; } $md5 = (string)$item['md5']; if (isset($inserveLookup[$md5])) { # Subscription already exists in Inserve $item['SentriStatus'] = $sentriStatus; $item['sentriCompanyId'] = (int)$allCompaniesIds[$row['company_source_id']] ?? 0; $item['subscriptionInserveExists'] = true; $item['subscriptionInserveId'] = $inserveLookup[$item['md5']]['id']; $item['countInserve'] = $inserveLookup[$item['md5']]['quantity']; $item['subscriptionInserveCompanyId'] = $inserveLookup[$item['md5']]['cloud_distribution_company_id']; $item['subscriptionInserveName'] = $inserveLookup[$item['md5']]['name']; //$item['subscriptionInserveStatus'] = $inserveLookup[$item['md5']]['status']; } else { # Subscription does not exists in Inserve $item['sentriCompanyId'] = (int)$allCompaniesIds[$row['company_source_id']] ?? 0; $item['subscriptionInserveExists'] = false; $item['subscriptionInserveId'] = false; $item['countInserve'] = 0; $item['subscriptionInserveCompanyId'] = false; } } unset($item); // Make the subscriptions names look nice and dandy. foreach ($subscriptionCounts as $key => &$item) { // Set server name $serverName = $row['server_hostname'] ?? $row['server_vm_host_name'] ?? 'Unknown'; // remove server_ prefix and _count suffix $namePart = $key; if (str_starts_with($key, 'server_') && str_ends_with($key, '_count')) { $namePart = substr($key, 7, -6); $namePart = ucfirst($namePart); } // Handle keys with "." elseif (strpos($key, '.') !== false) { [$first, $second] = explode('.', $key, 2); if ($first === $second || strtolower($second) === 'yes') { $namePart = ucfirst($first); } else { $namePart = ucfirst($first) . ' - ' . $second; } } //Handle keys without . but with a space (expmale directadmin.Standard Discounted) elseif (strpos($key, ' ') !== false) { // explode on first . $parts = explode('.', $key, 2); if (count($parts) === 2) { $namePart = ucfirst($parts[0]) . ' - ' . $parts[1]; } else { // Cap first word before first space $spacePos = strpos($key, ' '); $first = ucfirst(substr($key, 0, $spacePos)); $rest = substr($key, $spacePos + 1); $namePart = $first . ' - ' . $rest; } } $item['subscriptionSentriName'] = $serverName . ' - ' . $namePart; } unset($item); foreach ($subscriptionCounts as $key => $item) { // if subscriptionInserveExists but the countInserve is null skip creation if ($item['subscriptionInserveExists'] === false && (int)$item['countSentri'] === 0) { continue; } // if subscriptionInserveExists is false create a new subscription if ($item['subscriptionInserveExists'] === false) { $payload = [ "cloud_distribution_id" => $item['md5'], #md5 hash based on the server_uuid from sentri and the subscription name (eg. server_cpu_count) "cloud_subscription_id" => "sentri-servers", # Mark all the sentri-servers subscriptions so we can filter the subscriptions better "name" => $item['subscriptionInserveName'], "quantity" => ($row['server_state'] === 'deleted') ? 0 : $item['countSentri'], "cloud_distribution_company_id" => $item['sentriCompanyId'], # this is generated by inserve "status" => $item['SentriStatus'], "period_type" => 0, # 0 = monthly, 1 = anual, 2 = one time cost "start_date" => date('Y-m-d') ]; $this->createCloudSubscription($payload); continue; } // update the subscription if the countInserve and countSentri dont match // Or when sentriCompanyId and subscriptionInserveCompanyId dont match if (( (int)$item['countInserve'] !== (int)$item['countSentri'] || (int)$item['sentriCompanyId'] !== (int)$item['subscriptionInserveCompanyId'] || (int)$item['SentriStatus'] !== (int)$item['subscriptionInserveStatus'] || $item['subscriptionSentriName'] !== $item['subscriptionInserveName'] ) && $item['subscriptionInserveExists'] !== false ) { $payload = [ "quantity" => (int)$item['countSentri'], "cloud_distribution_company_id" => (int)$item['sentriCompanyId'], "name" => $item['subscriptionSentriName'], "status" => $item['SentriStatus'], "quantity" => ($row['server_state'] === 'deleted') ? 0 : $item['countSentri'] ]; $this->updateCloudSubscription($item['subscriptionInserveId'], $payload); continue; } } } } public function getSubscriptionsLines($subscriptionId) { $params = ['b' => [['where' => [0 => 'subscription_lines.subscription_id', 1 => $subscriptionId]]]]; $query = http_build_query($params); $this->ch = curl_init($this->inserve_url . 'subscription-lines?' . $query); curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json" ] ]); $this->execCurl(); } private function buildSubscriptionLine($subscription_description, $productCode, $allInserveProducts) { return [ 'quantity' => 0, 'product_code' => $productCode, 'price' => $allInserveProducts[$productCode]['price_excl'], 'description' => $subscription_description . ' ' . $allInserveProducts[$productCode]['product_name'], ]; } public function syncServerSubscriptionsToInserve() { $SentriCustomData = $this->inserve_custom_data; # Get all the Product IDs from Inserve $this->getProducts(); $allInserveProducts = array_column(json_decode($this->response, true), null, 'product_code'); # Get all the current subscriptions $this->getSubscriptions(); $allInserveSubscriptions = json_decode($this->response, true); # Filter out all the none Sentri posted subscriptions based on the name for performance $allInserveVPSSubscriptions = array_filter($allInserveSubscriptions, function ($subscription) { return isset($subscription['description']) && str_contains($subscription['description'], 'VPS - '); }); # get all the servers from Sentri $sql = "SELECT * FROM servers INNER JOIN companies ON servers.company_uuid = companies.company_uuid WHERE company_state = 'active' AND server_state != 'new' AND server_state != 'disabled' AND server_state != 'deleted' "; $stmt = $this->conn->query($sql); while ($row = $stmt->fetch_assoc()) { # init server item loop $subscription_description = 'VPS - ' . (!empty($row['server_vm_host_name']) ? $row['server_vm_host_name'] : $row['server_hostname']); $firstDayCurrentMonth = date('Y-m-01'); $firstDayPreviousMonth = date('Y-m-d', strtotime('first day of last month')); $missing = []; $invalidProductCode = []; $subscriptionLines = [ 'lines' => [] ]; $subscriptionFound = false; # standard server components with flat values foreach (['server_cpu', 'server_memory', 'server_disks'] as $serverComponent) { if ($row[$serverComponent] != NULL) { if (!isset($SentriCustomData['subscriptions'][$serverComponent])) { $missing[] = $serverComponent; } $productCode = $SentriCustomData['subscriptions'][$serverComponent]['product_code'] ?? null; #if (!isset($allInserveProducts[$productCode])) { # $invalidProductCode[] = $serverComponent; #} $subscriptionLines['lines'][] = $this->buildSubscriptionLine($subscription_description, $productCode, $allInserveProducts); } } # server backups if ($row['server_backup'] != NULL) { $server_backup = json_decode($row['server_backup'], true); if (!empty($server_backup)) { foreach ($server_backup as $backupItem) { foreach ($backupItem as $key => $value) { if (!isset($SentriCustomData['subscriptions']['server_backup'][$key])) { $missing[] = $key; } $productCode = $SentriCustomData['subscriptions']['server_backup'][$key]['product_code'] ?? null; $subscriptionLines['lines'][] = $this->buildSubscriptionLine($subscription_description, $productCode, $allInserveProducts); } } } } # server licenses if ($row['server_licenses'] != NULL) { $server_licenses = json_decode($row['server_licenses'], true); if (!empty($server_licenses)) { foreach ($server_licenses as $LicenseItem) { foreach ($LicenseItem as $key => $value) { if (!isset($SentriCustomData['subscriptions']['server_licenses'][$key][$value])) { $missing[] = $key; } $productCode = $SentriCustomData['subscriptions']['server_licenses'][$key][$value]['product_code'] ?? null; $subscriptionLines['lines'][] = $this->buildSubscriptionLine($subscription_description, $productCode, $allInserveProducts); } } } } if (!empty($missing)) { $this->apiOutput(400, [ 'error' => 'Missing subscription data for: ' . implode(', ', $missing) ]); } if (!empty($invalidProductCode)) { $this->apiOutput(400, [ 'error' => 'Invalid product code for: ' . implode(', ', $invalidProductCode) ]); } # Check if the subscription already exists to decide if we need to update or create the subscription foreach ($allInserveVPSSubscriptions as $subscription) { if (isset($subscription['description']) && str_contains($subscription['description'], $subscription_description)) { $subscriptionFound = $subscription['id']; break; } } if ($subscriptionFound) { # subscription already exists, update information that is collected by Sentri (company and server usages) $subscriptionId = $subscriptionFound; $existingLines = []; $missingLines = []; $subscriptionLines = []; # Get the subscription lines from the specific subscriptionId $this->getSubscriptionsLines($subscriptionId); $InserveSubscriptionsLines = json_decode($this->response, true); foreach ($InserveSubscriptionsLines as $item) { $existingLines[$item['product_code']] = $item; } # Check if there are products that dont exists yet or have different descriptions foreach ($subscriptionLines['lines'] as $subscriptionLine) { $product_code = $subscriptionLine['product_code']; if (!isset($existingLines[$product_code])) { $missingLines[] = $subscriptionLine; continue; } # Update the description and label if they differ. if ($existingLines[$product_code]['description'] !== $subscriptionLine['description']) { $existingLines[$product_code]['description'] = $subscriptionLine['description']; $existingLines[$product_code]['_label'] = $subscriptionLine['description']; } } $subscriptionPayload = [ 'company_id' => (int)$row['company_source_id'] ?? 0, 'relationships' => [ 'lines' => array_values(array_merge($existingLines, $missingLines)) ] ]; $this->updateSubscription($subscriptionId, $subscriptionPayload); } else { # create a new subscription $subscriptionPayload = [ 'description' => $subscription_description, 'company_id' => (int)$row['company_source_id'] ?? 0, 'periods' => 1, 'period' => 'month', 'start_period' => $firstDayCurrentMonth, 'direct_debit' => 0, 'next_billing_date' => $firstDayPreviousMonth, 'billing_start_date' => $firstDayPreviousMonth, 'invoice_periods' => 1, 'invoice_separately' => true, 'invoice_setting_on_change' => 'system', 'relationships' => $subscriptionLines, ]; $this->createSubscription($subscriptionPayload); } } } public function createSubscription($payload) { $url = $this->inserve_url . 'subscriptions'; $this->ch = curl_init($url); # I need to make this pay load: curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json", "Content-Type: application/json" ], ]); $this->execCurl(); } public function createCloudSubscription($payload) { $url = $this->inserve_url . 'cloud-distribution-subscriptions'; $this->ch = curl_init($url); # I need to make this pay load: curl_setopt_array($this->ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ "X-Api-Key: $this->inserve_token", "Accept: application/json", "Content-Type: application/json" ], ]); $this->execCurl(); } }