ALTER TABLE fragment to add it if missing.
$autoRepairCols = [
'description' => "ADD COLUMN `description` VARCHAR(1000) NOT NULL DEFAULT ''",
'img_url' => "ADD COLUMN `img_url` VARCHAR(255) NOT NULL DEFAULT ''",
'out_of_stock' => "ADD COLUMN `out_of_stock` VARCHAR(255) NOT NULL DEFAULT ''",
'slot_min_qty' => "ADD COLUMN `slot_min_qty` INT(11) NOT NULL DEFAULT 0",
'slot_max_qty' => "ADD COLUMN `slot_max_qty` INT(11) NOT NULL DEFAULT 0",
'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 ''",
'install_method' => "ADD COLUMN `install_method` VARCHAR(255) NOT NULL DEFAULT 'steamcmd'",
];
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 missing after repair, skip the sync to avoid SQL errors.
foreach (['service_name', 'mod_cfg_id', 'enabled'] as $critical) {
if (!col_exists($db, $tableName, $critical)) {
$messages[] = "⚠ Critical column '{$critical}' missing from {$tableName}; skipping sync.";
return $messages;
}
}
// Load all games/mods from panel config tables
$gameMods = [];
$res = $db->query(
"SELECT cm.mod_cfg_id, cm.home_cfg_id, cm.mod_name, ch.game_name
FROM `{$prefix}config_mods` cm
JOIN `{$prefix}config_homes` ch ON ch.home_cfg_id = cm.home_cfg_id
ORDER BY ch.game_name, cm.mod_name"
);
if ($res) {
while ($row = $res->fetch_assoc()) {
$gameMods[(int)$row['mod_cfg_id']] = $row;
}
}
if (empty($gameMods)) {
// config_mods is empty or tables don't exist yet — nothing to sync
return $messages;
}
// Load existing billing_services indexed by mod_cfg_id
$existing = [];
$svcRes = $db->query(
"SELECT service_id, mod_cfg_id, enabled, out_of_stock
FROM `{$prefix}billing_services`"
);
if ($svcRes) {
while ($row = $svcRes->fetch_assoc()) {
$existing[(int)$row['mod_cfg_id']] = $row;
}
}
// Insert new rows for game/mods not yet in billing_services
foreach ($gameMods as $modCfgId => $gm) {
if (isset($existing[$modCfgId])) {
continue;
}
$svcName = $db->real_escape_string($gm['mod_name'] ?: $gm['game_name']);
$homeCfgId = (int)$gm['home_cfg_id'];
// remote_server_id is intentionally empty: no servers are assigned until
// an admin reviews and enables the service on the adminserverlist page.
$db->query(
"INSERT INTO `{$prefix}billing_services`
(home_cfg_id, mod_cfg_id, service_name, description,
remote_server_id, enabled, out_of_stock,
price_daily, price_monthly, price_year,
slot_min_qty, slot_max_qty, install_method)
VALUES
({$homeCfgId}, {$modCfgId}, '{$svcName}', '{$svcName}',
'', 0, 0,
0, 0, 0,
1, 100, 'steamcmd')"
);
$messages[] = "Added new service: " . ($gm['mod_name'] ?: $gm['game_name']);
}
// Soft-disable billing_services whose mod_cfg_id no longer appears in config_mods
foreach ($existing as $modCfgId => $svcRow) {
if ($modCfgId > 0 && !isset($gameMods[$modCfgId])) {
$sid = (int)$svcRow['service_id'];
$db->query(
"UPDATE `{$prefix}billing_services`
SET enabled = 0, out_of_stock = 1
WHERE service_id = {$sid} AND enabled = 1"
);
if ($db->affected_rows > 0) {
$messages[] = "Service ID {$sid} disabled — game mod no longer in config.";
}
}
}
return $messages;
}
$syncMessages = sync_billing_services($db, $table_prefix);
$flash = [];
$flashType = 'ok';
/* -----------------------------------------------------------------------
SAVE: service configuration form submitted
----------------------------------------------------------------------- */
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), 4, '.', '');
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 4, '.', '');
$priceYear = number_format((float)($svcData['price_year'] ?? 0), 4, '.', '');
$slotMin = max(0, (int)($svcData['slot_min_qty'] ?? 0));
$slotMax = max(0, (int)($svcData['slot_max_qty'] ?? 0));
if ($slotMax < $slotMin) { $slotMax = $slotMin; }
// 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},
remote_server_id = '{$remoteServerIdStr}'
WHERE service_id = {$sid}"
);
}
$flash[] = "Services saved.";
}
/* -----------------------------------------------------------------------
Load data for display
----------------------------------------------------------------------- */
$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 service_id, service_name, enabled, price_daily, price_monthly, price_year,
slot_min_qty, slot_max_qty, remote_server_id
FROM `{$table_prefix}billing_services`
ORDER BY 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/mod configuration. 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).
Notes:
- A service will only appear in the store when Enabled is checked
and at least one server is selected.
- Available servers are stored as a comma-separated list of server IDs in
.
- The service list is automatically synced with the panel game/mod configuration on
every page load. New games are added with Enabled = off so they do not
appear in the store until you configure and enable them.
- Games removed from the panel configuration are disabled automatically; they are
never deleted while orders may reference them.