$imgFile) { if (str_starts_with($normImgKey, $key) || str_starts_with($key, $normImgKey)) { return $imgFile; } } } return ''; } $db = billing_get_db(); if (!($db instanceof mysqli)) { die("Database connection failed."); } include(__DIR__ . '/includes/top.php'); include(__DIR__ . '/includes/menu.php'); /* ----------------------------------------------------------------------- Auto-sync: keep billing_services in step with config_homes Source: one row per config_homes entry, keyed by home_cfg_id. Runs on every page load; INSERT and soft-disable only — never hard-delete. ----------------------------------------------------------------------- */ function sync_billing_services(mysqli $db, string $prefix): array { $messages = []; $tableName = $prefix . 'billing_services'; // Schema auto-repair: ensure all expected columns exist. // col_exists() is provided by bootstrap.php. $autoRepairCols = [ 'home_cfg_id' => "ADD COLUMN `home_cfg_id` INT(11) NOT NULL DEFAULT 0", 'description' => "ADD COLUMN `description` VARCHAR(1000) NOT NULL DEFAULT ''", 'img_url' => "ADD COLUMN `img_url` VARCHAR(255) NOT NULL DEFAULT ''", 'slot_min_qty' => "ADD COLUMN `slot_min_qty` INT(11) NOT NULL DEFAULT 1", 'slot_max_qty' => "ADD COLUMN `slot_max_qty` INT(11) NOT NULL DEFAULT 100", 'price_daily' => "ADD COLUMN `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0", 'price_monthly' => "ADD COLUMN `price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0", 'price_year' => "ADD COLUMN `price_year` FLOAT(15,4) NOT NULL DEFAULT 0", 'remote_server_id' => "ADD COLUMN `remote_server_id` VARCHAR(255) NOT NULL DEFAULT ''", ]; foreach ($autoRepairCols as $col => $alterFragment) { if (!col_exists($db, $tableName, $col)) { if ($db->query("ALTER TABLE `{$tableName}` {$alterFragment}")) { $messages[] = "✔ Auto-repaired: added column '{$col}' to {$tableName}."; } else { $messages[] = "✖ Could not add column '{$col}' to {$tableName}: " . $db->error; } } } // If critical columns are still absent after repair, abort to avoid SQL errors. foreach (['service_name', 'home_cfg_id', 'enabled'] as $critical) { if (!col_exists($db, $tableName, $critical)) { $messages[] = "⚠ Critical column '{$critical}' missing from {$tableName}; skipping sync."; return $messages; } } // Load all game configs from config_homes — one entry per game XML. $configHomes = []; $res = $db->query( "SELECT home_cfg_id, game_name, home_cfg_file FROM `{$prefix}config_homes` ORDER BY game_name" ); if ($res) { while ($row = $res->fetch_assoc()) { $configHomes[(int)$row['home_cfg_id']] = $row; } } if (empty($configHomes)) { // config_homes is empty or the table does not exist yet — nothing to sync. return $messages; } // Load existing billing_services indexed by home_cfg_id. $existing = []; $svcRes = $db->query( "SELECT service_id, home_cfg_id, enabled FROM `{$tableName}`" ); if ($svcRes) { while ($row = $svcRes->fetch_assoc()) { $hid = (int)$row['home_cfg_id']; if ($hid > 0) { $existing[$hid] = $row; } } } // Insert a new row for every config_homes entry not yet in billing_services. // Admin-editable fields (prices, slots, enabled, etc.) get safe defaults so // the service is visible to the admin but not yet live in the store. $availableImages = list_game_images(); foreach ($configHomes as $homeCfgId => $ch) { if (isset($existing[$homeCfgId])) { continue; } $svcName = $db->real_escape_string($ch['game_name']); $guessedImg = $db->real_escape_string( guess_game_image((string)$ch['game_name'], (string)($ch['home_cfg_file'] ?? ''), $availableImages) ); $db->query( "INSERT INTO `{$tableName}` (home_cfg_id, mod_cfg_id, service_name, description, remote_server_id, enabled, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty, img_url, ftp, install_method, manual_url, access_rights) VALUES ({$homeCfgId}, 0, '{$svcName}', '{$svcName}', '', 0, 0.00, 0.00, 0.00, 1, 100, '{$guessedImg}', '', 'steamcmd', '', '')" ); $msg = "Added new service: " . $ch['game_name']; if ($guessedImg !== '') { $msg .= " (image auto-set: {$guessedImg})"; } $messages[] = $msg; } // Soft-disable billing_services whose home_cfg_id no longer appears in config_homes. foreach ($existing as $homeCfgId => $svcRow) { if (!isset($configHomes[$homeCfgId])) { $sid = (int)$svcRow['service_id']; $db->query( "UPDATE `{$tableName}` SET enabled = 0 WHERE service_id = {$sid} AND enabled = 1" ); if ($db->affected_rows > 0) { $messages[] = "Service ID {$sid} disabled — game config no longer in config_homes."; } } } return $messages; } $syncMessages = sync_billing_services($db, $table_prefix); $flash = []; $flashType = 'ok'; /* ----------------------------------------------------------------------- SAVE: service configuration form submitted Only admin-editable fields are updated; service_name and home_cfg_id are never overwritten here. ----------------------------------------------------------------------- */ if (isset($_POST['save_services'])) { // Load valid remote server IDs for validation $validServerIds = []; $rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`"); while ($rsRes && ($rsRow = $rsRes->fetch_assoc())) { $validServerIds[] = (int)$rsRow['remote_server_id']; } $validSet = array_flip($validServerIds); $postedServices = $_POST['svc'] ?? []; $postedServers = $_POST['servers'] ?? []; foreach ((array)$postedServices as $sid => $svcData) { $sid = (int)$sid; $enabled = isset($svcData['enabled']) ? 1 : 0; $priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 2, '.', ''); $priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', ''); $priceYear = number_format((float)($svcData['price_year'] ?? 0), 2, '.', ''); $slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1)); $slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1)); if ($slotMax < $slotMin) { $slotMax = $slotMin; } $description = $db->real_escape_string(substr((string)($svcData['description'] ?? ''), 0, 1000)); // Merge dropdown and fallback text input: // - dropdown value "__other__" means use the text fallback field // - otherwise use the dropdown value (bare filename or '') $rawImgUrl = (string)($svcData['img_url'] ?? ''); if ($rawImgUrl === '__other__') { $rawImgUrl = (string)($svcData['img_url_other'] ?? ''); } $imgUrl = $db->real_escape_string(substr($rawImgUrl, 0, 255)); // Build comma-separated remote_server_id from checkboxes, validating each ID $checkedIds = []; foreach ((array)($postedServers[$sid] ?? []) as $rawId) { $rid = (int)$rawId; if (isset($validSet[$rid])) { $checkedIds[] = $rid; } } $remoteServerIdStr = $db->real_escape_string(implode(',', $checkedIds)); $db->query( "UPDATE `{$table_prefix}billing_services` SET enabled = {$enabled}, price_daily = '{$priceDaily}', price_monthly = '{$priceMonthly}', price_year = '{$priceYear}', slot_min_qty = {$slotMin}, slot_max_qty = {$slotMax}, description = '{$description}', img_url = '{$imgUrl}', remote_server_id = '{$remoteServerIdStr}' WHERE service_id = {$sid}" ); } $flash[] = "Services saved."; } /* ----------------------------------------------------------------------- Load data for display — join config_homes to show the config XML filename ----------------------------------------------------------------------- */ $remoteServers = []; $rsRes = $db->query( "SELECT remote_server_id, remote_server_name FROM `{$table_prefix}remote_servers` ORDER BY remote_server_name" ); while ($rsRes && ($row = $rsRes->fetch_assoc())) { $remoteServers[] = $row; } $services = []; $svcRes = $db->query( "SELECT bs.service_id, bs.service_name, bs.enabled, bs.price_daily, bs.price_monthly, bs.price_year, bs.slot_min_qty, bs.slot_max_qty, bs.remote_server_id, bs.description, bs.img_url, ch.home_cfg_file FROM `{$table_prefix}billing_services` bs LEFT JOIN `{$table_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id ORDER BY bs.service_name" ); while ($svcRes && ($row = $svcRes->fetch_assoc())) { $services[] = $row; } ?>

Service Configuration

Enable services, configure pricing and slot ranges, and select which remote servers each game can be installed on. The service list is automatically kept in sync with the panel game configuration (config_homes). Check one or more servers to make a game available for purchase; leaving all servers unchecked prevents the game from appearing in the store.

No billing services found. Ensure game configs are loaded in the panel (Home → Games configuration).

Game Name Config XML Enabled Min Slots Max Slots Price / Day ($) Price / Month ($) Price / Year ($) Description Image Available Servers
ID:
—'; ?> > No remote servers configured

Notes: