fix: billing DB safety, remove mapping table, sync services from game config
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/0b960e6d-bdf7-4b5b-8114-6c63e6b11a8d Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
6465632097
commit
3219ed335e
8 changed files with 332 additions and 235 deletions
|
|
@ -1,3 +1,13 @@
|
|||
-- DEPRECATED: This file is no longer needed.
|
||||
--
|
||||
-- The gsp_billing_service_remote_servers mapping table has been removed.
|
||||
-- Server availability per game/service is now stored in gsp_billing_services.remote_server_id
|
||||
-- as a comma-separated list of numeric server IDs (e.g. "1,3,7").
|
||||
-- The module migration (db_version 4) drops the mapping table automatically.
|
||||
--
|
||||
-- The original content of this file is kept below for historical reference only.
|
||||
-- Do NOT run this script on new installations.
|
||||
--
|
||||
-- Migration: add override_price to billing_service_remote_servers
|
||||
-- Run once on existing installs that already have the mapping table (db_version 2)
|
||||
-- but are missing the override_price column (added in db_version 3 / module v3.1).
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
-- DEPRECATED: This file is no longer needed.
|
||||
--
|
||||
-- The billing module no longer references an `enabled` column on gsp_remote_servers.
|
||||
-- gsp_remote_servers is the server inventory table only.
|
||||
-- Server availability per game/service is stored in gsp_billing_services.remote_server_id
|
||||
-- as a comma-separated list of numeric server IDs (e.g. "1,3,7").
|
||||
--
|
||||
-- The original content of this file is kept below for historical reference only.
|
||||
-- Do NOT run this script on new installations.
|
||||
--
|
||||
-- Migration: add `enabled` column to gsp_remote_servers
|
||||
--
|
||||
-- The original panel schema (panel.sql / ogp_remote_servers) includes an `enabled`
|
||||
|
|
|
|||
|
|
@ -3,210 +3,258 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Server / Game Matrix - GameServers.World</title>
|
||||
<title>Admin Service Configuration - GSP</title>
|
||||
<style>
|
||||
.matrix-table { border-collapse: collapse; width: 100%; }
|
||||
.matrix-table th, .matrix-table td { border: 1px solid #ddd; padding: 6px 8px; vertical-align: middle; text-align: center; }
|
||||
.matrix-table th { background: #f5f5f5; white-space: nowrap; }
|
||||
.matrix-table td.game-name { text-align: left; white-space: nowrap; }
|
||||
.override-input { width: 72px; margin-top: 4px; }
|
||||
.svc-table { border-collapse: collapse; width: 100%; }
|
||||
.svc-table th, .svc-table td { border: 1px solid #ddd; padding: 6px 8px; vertical-align: middle; }
|
||||
.svc-table th { background: #f5f5f5; white-space: nowrap; text-align: center; }
|
||||
.svc-table td.game-name { text-align: left; white-space: nowrap; }
|
||||
.price-input { width: 80px; }
|
||||
.slot-input { width: 60px; }
|
||||
.muted { color: #999; font-size: 0.85em; }
|
||||
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; }
|
||||
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; }
|
||||
.servers-cell { text-align: left; }
|
||||
.server-cb-label { display: block; white-space: nowrap; margin: 2px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
// Admin matrix page: game × server availability + price overrides
|
||||
/**
|
||||
* Admin service configuration page.
|
||||
*
|
||||
* On every load this page syncs gsp_billing_services with the panel's game/mod
|
||||
* config list (config_mods joined with config_homes). It provides a table UI
|
||||
* where admins can enable/disable services, set prices, configure slot ranges,
|
||||
* and choose which remote servers each game can be installed on.
|
||||
*
|
||||
* remote_server_id in gsp_billing_services stores a comma-separated list of
|
||||
* numeric remote server IDs, e.g. "1,3,7". The deprecated
|
||||
* gsp_billing_service_remote_servers mapping table is never referenced here.
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/bootstrap.php');
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function h(mixed $s): string
|
||||
{
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
|
||||
if (!$db) {
|
||||
die("Connection failed: " . mysqli_connect_error());
|
||||
$db = billing_get_db();
|
||||
if (!($db instanceof mysqli)) {
|
||||
die("Database connection failed.");
|
||||
}
|
||||
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
|
||||
// Ensure the mapping table exists with the override_price column
|
||||
$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$table_prefix}billing_service_remote_servers` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`service_id` INT(11) NOT NULL,
|
||||
`remote_server_id` INT(11) NOT NULL,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`override_price` DECIMAL(10,2) NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `svc_rs` (`service_id`, `remote_server_id`),
|
||||
KEY `service_id` (`service_id`),
|
||||
KEY `remote_server_id` (`remote_server_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
// Add override_price if this is an older install that already has the table without it
|
||||
$chk = $db->query("SHOW COLUMNS FROM `{$table_prefix}billing_service_remote_servers` LIKE 'override_price'");
|
||||
if ($chk && $chk->num_rows === 0) {
|
||||
$db->query("ALTER TABLE `{$table_prefix}billing_service_remote_servers` ADD COLUMN `override_price` DECIMAL(10,2) NULL");
|
||||
/* -----------------------------------------------------------------------
|
||||
Auto-sync: keep billing_services in step with game/mod config list
|
||||
Runs on every page load; INSERT and soft-disable only — never hard-delete.
|
||||
----------------------------------------------------------------------- */
|
||||
function sync_billing_services(mysqli $db, string $prefix): array
|
||||
{
|
||||
$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'];
|
||||
$db->query(
|
||||
"INSERT INTO `{$prefix}billing_services`
|
||||
(home_cfg_id, mod_cfg_id, service_name, 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}', '',
|
||||
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;
|
||||
}
|
||||
|
||||
$flash = [];
|
||||
$syncMessages = sync_billing_services($db, $table_prefix);
|
||||
|
||||
$flash = [];
|
||||
$flashType = 'ok';
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
SAVE: matrix form submitted
|
||||
SAVE: service configuration form submitted
|
||||
----------------------------------------------------------------------- */
|
||||
if (isset($_POST['save_matrix'])) {
|
||||
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'] ?? [];
|
||||
$postedMappings = $_POST['map'] ?? [];
|
||||
$postedServers = $_POST['servers'] ?? [];
|
||||
|
||||
foreach ((array)$postedServices as $sid => $svcData) {
|
||||
$sid = (int)$sid;
|
||||
$enabled = isset($svcData['enabled']) ? 1 : 0;
|
||||
$base_price = number_format((float)($svcData['base_price'] ?? 0), 2, '.', '');
|
||||
$period = in_array($svcData['period'] ?? 'monthly', ['daily','monthly','yearly'], true)
|
||||
? $svcData['period'] : 'monthly';
|
||||
$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; }
|
||||
|
||||
$price_col = $period === 'daily' ? 'price_daily' : ($period === 'yearly' ? 'price_year' : 'price_monthly');
|
||||
$base_esc = $db->real_escape_string($base_price);
|
||||
// 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_col}` = '{$base_esc}'
|
||||
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}"
|
||||
);
|
||||
}
|
||||
|
||||
// Upsert mappings: for every service x server pair post data received
|
||||
$allServerIds = [];
|
||||
$rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`");
|
||||
while ($rsRes && ($rsRow = $rsRes->fetch_assoc())) {
|
||||
$allServerIds[] = (int)$rsRow['remote_server_id'];
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO `{$table_prefix}billing_service_remote_servers`
|
||||
(service_id, remote_server_id, enabled, override_price)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
enabled = VALUES(enabled),
|
||||
override_price = VALUES(override_price)"
|
||||
);
|
||||
foreach ((array)$postedServices as $sid => $ignored) {
|
||||
$sid = (int)$sid;
|
||||
foreach ($allServerIds as $rid) {
|
||||
$mapEnabled = isset($postedMappings[$sid][$rid]['enabled']) ? 1 : 0;
|
||||
$ovRaw = $postedMappings[$sid][$rid]['override_price'] ?? '';
|
||||
$ovPrice = (trim($ovRaw) === '') ? null : number_format((float)$ovRaw, 2, '.', '');
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('iisd', $sid, $rid, $mapEnabled, $ovPrice);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($stmt) {
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$flash[] = "Matrix saved successfully.";
|
||||
$flash[] = "Services saved.";
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
Remove a service
|
||||
----------------------------------------------------------------------- */
|
||||
if (isset($_POST['remove_service'], $_POST['service_id_remove'])) {
|
||||
$sid = (int)$_POST['service_id_remove'];
|
||||
$db->query("DELETE FROM `{$table_prefix}billing_service_remote_servers` WHERE service_id = {$sid}");
|
||||
$db->query("DELETE FROM `{$table_prefix}billing_services` WHERE service_id = {$sid}");
|
||||
$flash[] = "Service #{$sid} removed.";
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
Load data
|
||||
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");
|
||||
$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
|
||||
"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;
|
||||
}
|
||||
|
||||
// Load existing mappings into a lookup: $mappings[$service_id][$remote_server_id] = ['enabled'=>..,'override_price'=>..]
|
||||
$mappings = [];
|
||||
$mapRes = $db->query(
|
||||
"SELECT service_id, remote_server_id, enabled, override_price
|
||||
FROM `{$table_prefix}billing_service_remote_servers`"
|
||||
);
|
||||
while ($mapRes && ($row = $mapRes->fetch_assoc())) {
|
||||
$mappings[(int)$row['service_id']][(int)$row['remote_server_id']] = [
|
||||
'enabled' => (int)$row['enabled'],
|
||||
'override_price' => $row['override_price'],
|
||||
];
|
||||
}
|
||||
?>
|
||||
|
||||
<?php foreach ((array)$flash as $msg): ?>
|
||||
<?php foreach (array_merge((array)$syncMessages, (array)$flash) as $idx => $msg):
|
||||
$type = ($flashType === 'err' || $idx < count((array)$syncMessages) && count($syncMessages) > 0 && strpos($msg, 'disabled') !== false) ? 'ok' : $flashType;
|
||||
?>
|
||||
<div class="flash-<?php echo $flashType; ?>"><?php echo h($msg); ?></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<h2>Game × Server Matrix</h2>
|
||||
<h2>Service Configuration</h2>
|
||||
<p class="muted">
|
||||
Enable or disable each game for billing, set its base price and billing period, then
|
||||
toggle availability per server and optionally override the price for that location.
|
||||
Leave override blank to use the base price.
|
||||
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.
|
||||
</p>
|
||||
|
||||
<?php if (empty($services)): ?>
|
||||
<p>No billing services found. Add services first via the database or the panel.</p>
|
||||
<p>No billing services found. Ensure game configs are loaded in the panel (Home → Games configuration).</p>
|
||||
<?php else: ?>
|
||||
|
||||
<form method="post" action="">
|
||||
<input type="hidden" name="save_matrix" value="1">
|
||||
<input type="hidden" name="save_services" value="1">
|
||||
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="matrix-table">
|
||||
<table class="svc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="game-name">Game</th>
|
||||
<th class="game-name">Game / Service</th>
|
||||
<th>Enabled</th>
|
||||
<th>Base Price ($)</th>
|
||||
<th>Period</th>
|
||||
<?php foreach ((array)$remoteServers as $rs): ?>
|
||||
<th><?php echo h($rs['remote_server_name']); ?><br>
|
||||
<span class="muted">#<?php echo (int)$rs['remote_server_id']; ?></span>
|
||||
</th>
|
||||
<?php endforeach; ?>
|
||||
<th>Price / Day ($)</th>
|
||||
<th>Price / Month ($)</th>
|
||||
<th>Price / Year ($)</th>
|
||||
<th>Min Slots</th>
|
||||
<th>Max Slots</th>
|
||||
<th>Available Servers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ((array)$services as $svc):
|
||||
$sid = (int)$svc['service_id'];
|
||||
$sid = (int)$svc['service_id'];
|
||||
$svcEnabled = (int)$svc['enabled'];
|
||||
// Determine current base price and period from existing columns
|
||||
if ((float)$svc['price_monthly'] > 0) {
|
||||
$basePrice = number_format((float)$svc['price_monthly'], 2, '.', '');
|
||||
$period = 'monthly';
|
||||
} elseif ((float)$svc['price_daily'] > 0) {
|
||||
$basePrice = number_format((float)$svc['price_daily'], 2, '.', '');
|
||||
$period = 'daily';
|
||||
} elseif ((float)$svc['price_year'] > 0) {
|
||||
$basePrice = number_format((float)$svc['price_year'], 2, '.', '');
|
||||
$period = 'yearly';
|
||||
} else {
|
||||
$basePrice = '0.00';
|
||||
$period = 'monthly';
|
||||
|
||||
// Parse existing remote_server_id CSV into a set for fast checkbox lookup
|
||||
$savedIds = [];
|
||||
foreach (explode(',', (string)$svc['remote_server_id']) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '' && ctype_digit($part)) {
|
||||
$savedIds[(int)$part] = true;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
|
|
@ -215,44 +263,61 @@ while ($mapRes && ($row = $mapRes->fetch_assoc())) {
|
|||
<div class="muted">ID: <?php echo $sid; ?></div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td style="text-align:center;">
|
||||
<input type="hidden" name="svc[<?php echo $sid; ?>][enabled]" value="0">
|
||||
<input type="checkbox" name="svc[<?php echo $sid; ?>][enabled]" value="1"
|
||||
<?php echo $svcEnabled ? 'checked' : ''; ?>>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="svc[<?php echo $sid; ?>][base_price]"
|
||||
value="<?php echo h($basePrice); ?>"
|
||||
style="width:90px;">
|
||||
<input type="number" step="0.0001" min="0" class="price-input"
|
||||
name="svc[<?php echo $sid; ?>][price_daily]"
|
||||
value="<?php echo h(number_format((float)$svc['price_daily'], 4, '.', '')); ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<select name="svc[<?php echo $sid; ?>][period]">
|
||||
<option value="monthly" <?php echo $period === 'monthly' ? 'selected' : ''; ?>>Monthly</option>
|
||||
<option value="daily" <?php echo $period === 'daily' ? 'selected' : ''; ?>>Daily</option>
|
||||
<option value="yearly" <?php echo $period === 'yearly' ? 'selected' : ''; ?>>Yearly</option>
|
||||
</select>
|
||||
<input type="number" step="0.0001" min="0" class="price-input"
|
||||
name="svc[<?php echo $sid; ?>][price_monthly]"
|
||||
value="<?php echo h(number_format((float)$svc['price_monthly'], 4, '.', '')); ?>">
|
||||
</td>
|
||||
|
||||
<?php foreach ((array)$remoteServers as $rs):
|
||||
$rid = (int)$rs['remote_server_id'];
|
||||
$mapEntry = $mappings[$sid][$rid] ?? ['enabled' => 0, 'override_price' => null];
|
||||
$mapEnabled = (int)$mapEntry['enabled'];
|
||||
$ovPrice = $mapEntry['override_price'];
|
||||
?>
|
||||
<td>
|
||||
<input type="hidden" name="map[<?php echo $sid; ?>][<?php echo $rid; ?>][enabled]" value="0">
|
||||
<input type="checkbox" name="map[<?php echo $sid; ?>][<?php echo $rid; ?>][enabled]" value="1"
|
||||
<?php echo $mapEnabled ? 'checked' : ''; ?>>
|
||||
<br>
|
||||
<input type="number" step="0.01" min="0" placeholder="override"
|
||||
class="override-input"
|
||||
name="map[<?php echo $sid; ?>][<?php echo $rid; ?>][override_price]"
|
||||
value="<?php echo ($ovPrice !== null ? h(number_format((float)$ovPrice, 2, '.', '')) : ''); ?>">
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
<td>
|
||||
<input type="number" step="0.0001" min="0" class="price-input"
|
||||
name="svc[<?php echo $sid; ?>][price_year]"
|
||||
value="<?php echo h(number_format((float)$svc['price_year'], 4, '.', '')); ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" min="0" class="slot-input"
|
||||
name="svc[<?php echo $sid; ?>][slot_min_qty]"
|
||||
value="<?php echo (int)$svc['slot_min_qty']; ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" min="0" class="slot-input"
|
||||
name="svc[<?php echo $sid; ?>][slot_max_qty]"
|
||||
value="<?php echo (int)$svc['slot_max_qty']; ?>">
|
||||
</td>
|
||||
|
||||
<td class="servers-cell">
|
||||
<?php if (empty($remoteServers)): ?>
|
||||
<span class="muted">No remote servers configured</span>
|
||||
<?php else: ?>
|
||||
<?php foreach ((array)$remoteServers as $rs):
|
||||
$rid = (int)$rs['remote_server_id'];
|
||||
$checked = isset($savedIds[$rid]) ? 'checked' : '';
|
||||
?>
|
||||
<label class="server-cb-label">
|
||||
<input type="checkbox"
|
||||
name="servers[<?php echo $sid; ?>][]"
|
||||
value="<?php echo $rid; ?>"
|
||||
<?php echo $checked; ?>>
|
||||
<?php echo h($rs['remote_server_name']); ?>
|
||||
<span class="muted">(#<?php echo $rid; ?>)</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
@ -260,33 +325,25 @@ while ($mapRes && ($row = $mapRes->fetch_assoc())) {
|
|||
</div>
|
||||
|
||||
<div style="margin-top:14px;">
|
||||
<button type="submit">Save Matrix</button>
|
||||
<button type="submit">Save Services</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h3 style="margin-top:28px;">Remove a Service</h3>
|
||||
<form method="post" action="" style="display:flex;gap:8px;align-items:center;">
|
||||
<input type="hidden" name="remove_service" value="1">
|
||||
<select name="service_id_remove">
|
||||
<?php foreach ((array)$services as $s): ?>
|
||||
<option value="<?php echo (int)$s['service_id']; ?>">
|
||||
<?php echo h($s['service_name']); ?> (ID: <?php echo (int)$s['service_id']; ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" onclick="return confirm('Remove this service and all its server mappings? This cannot be undone.')">Remove</button>
|
||||
</form>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel" style="margin-top:20px;">
|
||||
<p><strong>Legend:</strong> Checkbox = server is available for this game.
|
||||
Override price = customer pays this amount instead of the base price for that location.
|
||||
Leave override blank to use the game base price.</p>
|
||||
<p class="muted">
|
||||
Availability is controlled entirely by <code><?php echo h("{$table_prefix}billing_service_remote_servers"); ?></code>.
|
||||
No entry or <code>enabled = 0</code> means the server is not offered for that game.
|
||||
</p>
|
||||
<div style="margin-top:20px;" class="panel">
|
||||
<p><strong>Notes:</strong></p>
|
||||
<ul>
|
||||
<li>A service will only appear in the store when <strong>Enabled</strong> is checked
|
||||
<em>and</em> at least one server is selected.</li>
|
||||
<li>Available servers are stored as a comma-separated list of server IDs in
|
||||
<code><?php echo h("{$table_prefix}billing_services.remote_server_id"); ?></code>.</li>
|
||||
<li>The service list is automatically synced with the panel game/mod configuration on
|
||||
every page load. New games are added with <em>Enabled = off</em> so they do not
|
||||
appear in the store until you configure and enable them.</li>
|
||||
<li>Games removed from the panel configuration are disabled automatically; they are
|
||||
never deleted while orders may reference them.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php billing_maybe_close_db($db); ?>
|
||||
|
|
|
|||
|
|
@ -2,36 +2,25 @@
|
|||
/**
|
||||
* Billing config loader
|
||||
*
|
||||
* Priority order (standalone-first):
|
||||
* 1. modules/billing/includes/config.inc.php (local billing config — always wins when present)
|
||||
* 2. <panel_root>/includes/config.inc.php (panel config — fallback when no local config)
|
||||
* Priority order (panel-first):
|
||||
* 1. <panel_root>/includes/config.inc.php (active panel config — always wins when present)
|
||||
* 2. modules/billing/includes/config.inc.php (local billing config — standalone fallback only)
|
||||
*
|
||||
* This ensures that copying modules/billing/ to any web root works correctly
|
||||
* after editing its own config.inc.php, without being overridden by a parent
|
||||
* panel installation that may have a different database name.
|
||||
* The panel config is preferred so that every billing page, migration, and schema
|
||||
* check automatically uses the database from the active installation. This prevents
|
||||
* a testing install from accidentally writing to a production database when the local
|
||||
* billing config.inc.php still contains hard-coded production credentials.
|
||||
*
|
||||
* Standalone deployments (billing module deployed without the panel) should place
|
||||
* their own config.inc.php in modules/billing/includes/ as a fallback.
|
||||
*/
|
||||
if (defined('BILLING_CONFIG_LOADED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$localConfig = __DIR__ . '/config.inc.php';
|
||||
$attempted = [];
|
||||
|
||||
// Always prefer the local billing config so the module is self-contained.
|
||||
if (is_readable($localConfig)) {
|
||||
$attempted[] = $localConfig;
|
||||
require_once $localConfig;
|
||||
if (!defined('BILLING_CONFIG_PATH')) {
|
||||
define('BILLING_CONFIG_PATH', $localConfig);
|
||||
}
|
||||
define('BILLING_CONFIG_LOADED', true);
|
||||
return;
|
||||
}
|
||||
|
||||
$attempted[] = $localConfig;
|
||||
|
||||
// Fallback: try to load the panel's config (useful when running embedded inside the panel
|
||||
// and no local copy has been made yet).
|
||||
// Prefer the panel config so the billing module always uses the active installation's DB.
|
||||
$panelConfig = null;
|
||||
$projectRoot = realpath(__DIR__ . '/../../..');
|
||||
if ($projectRoot !== false) {
|
||||
|
|
@ -52,6 +41,21 @@ if ($panelConfig && is_readable($panelConfig)) {
|
|||
|
||||
$attempted[] = $panelConfig;
|
||||
|
||||
// Fallback: local billing config (useful for standalone deployments where no panel is present).
|
||||
$localConfig = __DIR__ . '/config.inc.php';
|
||||
|
||||
if (is_readable($localConfig)) {
|
||||
$attempted[] = $localConfig;
|
||||
require_once $localConfig;
|
||||
if (!defined('BILLING_CONFIG_PATH')) {
|
||||
define('BILLING_CONFIG_PATH', $localConfig);
|
||||
}
|
||||
define('BILLING_CONFIG_LOADED', true);
|
||||
return;
|
||||
}
|
||||
|
||||
$attempted[] = $localConfig;
|
||||
|
||||
$message = "GSP Billing module cannot find config.inc.php.\n";
|
||||
$message .= "Looked in:\n";
|
||||
foreach ((array)$attempted as $path) {
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
// Module general information
|
||||
$module_title = "billing";
|
||||
$module_version = "3.1";
|
||||
$db_version = 3;
|
||||
$module_version = "3.2";
|
||||
$db_version = 4;
|
||||
$module_required = FALSE;
|
||||
// Module description
|
||||
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
|
||||
|
|
@ -44,25 +44,26 @@ $install_queries[0] = array(
|
|||
// Billing Services - Available game server packages
|
||||
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_services` (
|
||||
`service_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`home_cfg_id` INT(11) NOT NULL,
|
||||
`mod_cfg_id` INT(11) NOT NULL,
|
||||
`home_cfg_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`mod_cfg_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`service_name` VARCHAR(255) NOT NULL,
|
||||
`remote_server_id` VARCHAR(255) NOT NULL,
|
||||
`remote_server_id` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`out_of_stock` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`slot_max_qty` INT(11) NOT NULL,
|
||||
`slot_min_qty` INT(11) NOT NULL,
|
||||
`slot_max_qty` INT(11) NOT NULL DEFAULT 0,
|
||||
`slot_min_qty` INT(11) NOT NULL DEFAULT 0,
|
||||
`price_daily` FLOAT(15,4) NOT NULL DEFAULT 0,
|
||||
`price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0,
|
||||
`price_year` FLOAT(15,4) NOT NULL DEFAULT 0,
|
||||
`description` VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
`img_url` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`ftp` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`install_method` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`install_method` VARCHAR(255) NOT NULL DEFAULT 'steamcmd',
|
||||
`manual_url` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`access_rights` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` INT(11) NOT NULL DEFAULT 1,
|
||||
`enabled` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`service_id`),
|
||||
KEY `enabled` (`enabled`)
|
||||
KEY `enabled` (`enabled`),
|
||||
KEY `mod_cfg_id` (`mod_cfg_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;",
|
||||
|
||||
// Billing Orders - Actual game server instances (ongoing services)
|
||||
|
|
@ -179,4 +180,11 @@ $install_queries[2] = array(
|
|||
"ALTER TABLE `".OGP_DB_PREFIX."billing_service_remote_servers` ADD COLUMN `override_price` DECIMAL(10,2) NULL AFTER `enabled`"
|
||||
);
|
||||
|
||||
// Version 4 (array index 3): Remove the separate service-to-server mapping table.
|
||||
// remote_server_id on billing_services now stores a comma-separated list of server IDs.
|
||||
// The mapping table is no longer used; drop it if it still exists from older installs.
|
||||
$install_queries[3] = array(
|
||||
"DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_service_remote_servers`"
|
||||
);
|
||||
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -189,29 +189,37 @@ if ($row['price_monthly'] == 0.0) {
|
|||
<td align="right"><b>Location</b></td>
|
||||
<td align="left">
|
||||
<?php
|
||||
// Fetch servers enabled for this game via the billing_service_remote_servers mapping table.
|
||||
// Only servers with an enabled mapping row are shown; remote_servers.enabled is not used.
|
||||
// Fetch servers available for this game from billing_services.remote_server_id
|
||||
// (a comma-separated list of numeric remote server IDs, e.g. "1,3,7").
|
||||
$available_server = false;
|
||||
$sid_order = (int)$row['service_id'];
|
||||
$mappedQuery = "SELECT m.remote_server_id, m.override_price, r.remote_server_name
|
||||
FROM {$table_prefix}billing_service_remote_servers m
|
||||
JOIN {$table_prefix}remote_servers r
|
||||
ON r.remote_server_id = m.remote_server_id
|
||||
WHERE m.service_id = {$sid_order} AND m.enabled = 1
|
||||
ORDER BY r.remote_server_name";
|
||||
$mappedResult = $db->query($mappedQuery);
|
||||
if ($mappedResult) {
|
||||
$firstServer = true;
|
||||
while ($rs = $mappedResult->fetch_assoc()) {
|
||||
$rsID = (int)$rs['remote_server_id'];
|
||||
$rsNAME = htmlspecialchars((string)$rs['remote_server_name'], ENT_QUOTES, 'UTF-8');
|
||||
$checked = $firstServer ? ' checked' : '';
|
||||
$available_server = true;
|
||||
$firstServer = false;
|
||||
echo "<div>\n"
|
||||
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' required{$checked}>\n"
|
||||
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
|
||||
. "</div>\n";
|
||||
$remoteIdsCsv = (string)($row['remote_server_id'] ?? '');
|
||||
$allowedIds = [];
|
||||
foreach (explode(',', $remoteIdsCsv) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '' && ctype_digit($part)) {
|
||||
$allowedIds[] = (int)$part;
|
||||
}
|
||||
}
|
||||
if (!empty($allowedIds)) {
|
||||
$inList = implode(',', $allowedIds);
|
||||
$rsQuery = "SELECT remote_server_id, remote_server_name
|
||||
FROM {$table_prefix}remote_servers
|
||||
WHERE remote_server_id IN ({$inList})
|
||||
ORDER BY remote_server_name";
|
||||
$rsResult = $db->query($rsQuery);
|
||||
if ($rsResult) {
|
||||
$firstServer = true;
|
||||
while ($rs = $rsResult->fetch_assoc()) {
|
||||
$rsID = (int)$rs['remote_server_id'];
|
||||
$rsNAME = htmlspecialchars((string)$rs['remote_server_name'], ENT_QUOTES, 'UTF-8');
|
||||
$checked = $firstServer ? ' checked' : '';
|
||||
$available_server = true;
|
||||
$firstServer = false;
|
||||
echo "<div>\n"
|
||||
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' required{$checked}>\n"
|
||||
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
|
||||
. "</div>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ if (isset($_POST['save']) && !empty($_POST['description'])) {
|
|||
|
||||
// Fetch services
|
||||
$service_id = isset($_REQUEST['service_id']) ? intval($_REQUEST['service_id']) : 0;
|
||||
$where_service_id = $service_id !== 0 ? "WHERE enabled = 1 AND service_id = $service_id" : "WHERE enabled = 1";
|
||||
$where_service_id = $service_id !== 0 ? "WHERE enabled = 1 AND service_id = $service_id AND remote_server_id != ''" : "WHERE enabled = 1 AND remote_server_id != ''";
|
||||
$qry_services = "SELECT * FROM {$table_prefix}billing_services $where_service_id ORDER BY service_name";
|
||||
$services = $db->query($qry_services);
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Last Updated at 1:15pm on 2026-02-05
|
||||
Last Updated at 3:21pm on 2026-02-05
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue