feat: simplify billing status to Active/Invoiced/Expired with new SQL migration and cron rewrite

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/5dbd58e1-7aa0-41e2-8dd3-c56b69ede05e

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-01 22:17:22 +00:00 committed by GitHub
parent b99cd45db9
commit b03d9b2171
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 738 additions and 499 deletions

View file

@ -1,7 +1,7 @@
<?php
/*
*
* OGP - Open Game Panel
* OGP / GSP - Open Game Panel / Game Server Panel
* Copyright (C) 2008 - 2017 The OGP Development Team
*
* http://www.opengamepanel.org/
@ -20,462 +20,416 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*
* INVOICE-BASED BILLING SYSTEM
* =============================
*
* Status Flow for billing_orders:
* - in-cart: User added to cart, not yet paid
* - paid: Payment received, awaiting server provisioning
* - installed: Active/Running (server provisioned and operational)
* - suspended: Server stopped, payment overdue (has unpaid invoice)
* - deleted: Server permanently removed
* - expired: Order has expired
*
* Invoice Status (billing_invoices):
* - unpaid: Invoice created, awaiting payment
* - paid: Invoice paid, service extended
*
* BILLING CRON - Three-Status Lifecycle
* ========================================
*
* Operates on server_homes.billing_status (separate from game-server runtime state).
*
* Status values:
* Active - Server is current; no unpaid renewal invoice.
* Invoiced - Renewal invoice generated; payment due.
* Expired - Invoice not paid by due date; server awaiting deletion.
*
* Steps run each night:
* A. Active -> Invoiced : next_invoice_date has arrived -> create {prefix}invoices record.
* B. Invoiced -> Expired : server_expiration_date passed and invoice unpaid.
* C. Expired -> Deleted : past delete_after_expired_days grace window -> remove server.
* D. Paid invoices (safety net): set server and invoice back to Active.
*
* Prerequisites (run once):
* sql/update_billing_status_active_invoiced_expired.sql
*/
chdir(realpath(dirname(__FILE__))); /* Change to the current file path */
chdir("../.."); /* Base path to ogp web files */
// Report all PHP errors
chdir(realpath(dirname(__FILE__))); /* Change to the billing module directory */
chdir("../.."); /* Step back to the OGP/GSP web root */
error_reporting(E_ALL);
// Path definitions
define("CONFIG_FILE","includes/config.inc.php");
//Require
ini_set('display_errors', '1');
define("CONFIG_FILE", "includes/config.inc.php");
require_once("includes/functions.php");
require_once("includes/helpers.php");
require_once("includes/html_functions.php");
require_once("modules/config_games/server_config_parser.php");
require_once("includes/lib_remote.php");
require_once CONFIG_FILE;
// Connect to the database server and select database.
$db = createDatabaseConnection($db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix, isset($db_port) ? $db_port : NULL);
require_once(CONFIG_FILE);
// Connect using the panel's DB helper (provides $db with logger(), resultQuery(), etc.)
$db = createDatabaseConnection(
$db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix,
isset($db_port) ? $db_port : null
);
$panel_settings = $db->getSettings();
if( isset($panel_settings['time_zone']) && $panel_settings['time_zone'] != "" )
date_default_timezone_set($panel_settings['time_zone']);
// Date calculations
$today = time();
$invoice_date = strtotime('+ 7 days'); // Create invoice 7 days before expiration
$suspend_date = $today; // Suspend immediately when overdue
$removal_date = strtotime('- 7 days'); // Remove 7 days after suspension
$rundate = date('Y-m-d H:i:s', is_numeric($today) ? (int)$today : strtotime($today));
$db->logger("BILLING-CRON: Server lifecycle automation running at " . $rundate);
// ==================================================================================
// STEP 1: CREATE RENEWAL INVOICES FOR SERVERS EXPIRING IN 7 DAYS
// ==================================================================================
// Find all ACTIVE servers (installed) that expire within 7 days and don't have an unpaid invoice
$upcoming_expirations = $db->resultQuery("
SELECT o.*, u.users_email, u.users_fname, u.users_lname
FROM " . $table_prefix . "billing_orders o
LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id
WHERE o.status = 'installed'
AND o.end_date IS NOT NULL
AND UNIX_TIMESTAMP(o.end_date) < {$invoice_date}
AND UNIX_TIMESTAMP(o.end_date) > {$today}
AND NOT EXISTS (
SELECT 1 FROM " . $table_prefix . "billing_invoices i
WHERE i.order_id = o.order_id AND i.status = 'unpaid'
)
");
if (is_array($upcoming_expirations)) {
foreach ((array)$upcoming_expirations as $order) {
$user_id = $order['user_id'];
$order_id = $order['order_id'];
$home_id = $order['home_id'];
$customer_name = trim(($order['users_fname'] ?? '') . ' ' . ($order['users_lname'] ?? ''));
$customer_email = $order['users_email'] ?? '';
// Create renewal invoice
$invoice_desc = "Renewal for " . $order['home_name'];
$due_date = date('Y-m-d H:i:s', strtotime($order['end_date']));
$db->query("INSERT INTO " . $table_prefix . "billing_invoices
(order_id, user_id, customer_name, customer_email, amount, currency, status,
invoice_date, due_date, description, invoice_duration, qty)
VALUES (
{$order_id},
{$user_id},
'" . $db->realEscapeSingle($customer_name) . "',
'" . $db->realEscapeSingle($customer_email) . "',
" . floatval($order['price']) . ",
'USD',
'unpaid',
NOW(),
'" . $db->realEscapeSingle($due_date) . "',
'" . $db->realEscapeSingle($invoice_desc) . "',
'" . $db->realEscapeSingle($order['invoice_duration']) . "',
" . intval($order['qty']) . "
)");
// Mark order status as 'renew' to indicate renewal invoice was created
$db->query("UPDATE " . $table_prefix . "billing_orders
SET status='renew'
WHERE order_id={$order_id}");
// Send renewal notice email
$settings = $db->getSettings();
$subject = "Renewal Invoice for " . $order['home_name'] . " - " . $panel_settings['panel_name'];
$message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) will expire on " .
date('F j, Y', strtotime($order['end_date'])) .
".<br><br>A renewal invoice has been created. Please log in to your account and pay the invoice to continue your service." .
"<br><br>Amount Due: $" . number_format($order['price'], 2) .
"<br>Due Date: " . date('F j, Y', strtotime($order['end_date'])) .
"<br><br>Thank you for your business!<br>";
$mail = mymail($customer_email, $subject, $message, $settings);
$db->logger("BILLING-CRON: Created renewal invoice for order {$order_id}, home {$home_id}");
if (!$mail) {
$db->logger("BILLING-CRON: Email FAILED - Renewal invoice for order {$order_id}");
}
}
if (!empty($panel_settings['time_zone'])) {
date_default_timezone_set($panel_settings['time_zone']);
}
// ==================================================================================
// STEP 2: SUSPEND SERVERS THAT ARE EXPIRED AND HAVE UNPAID INVOICES
// ==================================================================================
// Find servers that:
// - Are currently installed or renew (active)
// - Have passed their end_date
// - Have at least one unpaid invoice
$servers_to_suspend = $db->resultQuery("
SELECT DISTINCT o.*, u.users_email
FROM " . $table_prefix . "billing_orders o
LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id
INNER JOIN " . $table_prefix . "billing_invoices i ON o.order_id = i.order_id
WHERE o.status IN ('installed', 'renew')
AND o.end_date IS NOT NULL
AND UNIX_TIMESTAMP(o.end_date) < {$suspend_date}
AND i.status = 'unpaid'
$rundate = date('Y-m-d H:i:s');
$db->logger("BILLING-CRON: ===== Lifecycle automation started at {$rundate} =====");
// ----------------------------------------------------------------
// Load global billing config (grace_days, delete_after_expired_days)
// Falls back to safe defaults when {prefix}billing_config is empty.
// ----------------------------------------------------------------
$cfg_rows = $db->resultQuery(
"SELECT * FROM {$table_prefix}billing_config WHERE game_key IS NULL AND enabled = 1 ORDER BY config_id ASC LIMIT 1"
);
$global_cfg = is_array($cfg_rows) && !empty($cfg_rows) ? $cfg_rows[0] : [];
$grace_days = intval($global_cfg['grace_days'] ?? 0);
$delete_after_days = intval($global_cfg['delete_after_expired_days'] ?? 7);
$default_rate_type = $global_cfg['rate_type'] ?? 'monthly';
$default_price_player = floatval($global_cfg['price_per_player'] ?? 0.00);
$db->logger("BILLING-CRON: Config => grace_days={$grace_days}, delete_after={$delete_after_days}, rate={$default_rate_type}");
// ======================================================================
// STEP A - Active -> Invoiced
// Find billing-enabled servers whose next_invoice_date has arrived
// and that do not already have an open 'Invoiced' renewal invoice.
// ======================================================================
$db->logger("BILLING-CRON: --- Step A: Active -> Invoiced ---");
$due_for_invoice = $db->resultQuery("
SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id,
sh.next_invoice_date, sh.server_expiration_date,
bo.price, bo.invoice_duration, bo.qty, bo.order_id,
COALESCE(bs.price_monthly, 0) AS svc_price_monthly,
u.users_email,
CONCAT(COALESCE(u.users_fname,''), ' ', COALESCE(u.users_lname,'')) AS customer_name
FROM {$table_prefix}server_homes sh
LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main
LEFT JOIN {$table_prefix}billing_orders bo
ON bo.home_id = sh.home_id AND bo.status = 'Active'
LEFT JOIN {$table_prefix}billing_services bs ON bs.service_id = bo.service_id
WHERE sh.billing_enabled = 1
AND sh.billing_status = 'Active'
AND sh.next_invoice_date IS NOT NULL
AND sh.next_invoice_date <= NOW()
AND NOT EXISTS (
SELECT 1 FROM {$table_prefix}invoices inv
WHERE inv.home_id = sh.home_id AND inv.billing_status = 'Invoiced'
)
ORDER BY sh.home_id ASC
");
if (is_array($servers_to_suspend)) {
foreach ((array)$servers_to_suspend as $order) {
$user_id = $order['user_id'];
$home_id = $order['home_id'];
$order_id = $order['order_id'];
// Get home and server info
$home_info = $db->getGameHomeWithoutMods($home_id);
if (!$home_info) {
$db->logger("BILLING-CRON: WARNING - Home {$home_id} not found for order {$order_id}, marking suspended anyway");
$db->query("UPDATE " . $table_prefix . "billing_orders SET status='suspended' WHERE order_id={$order_id}");
if (is_array($due_for_invoice)) {
foreach ($due_for_invoice as $srv) {
$home_id = intval($srv['home_id']);
$user_id = intval($srv['user_id']);
$home_name = $srv['home_name'] ?? 'Server #' . $home_id;
$qty = max(1, intval($srv['qty'] ?? 1));
// Normalise rate_type to the ENUM values used in {prefix}invoices
$raw_rate = strtolower($srv['invoice_duration'] ?? $default_rate_type);
$rate_map = ['day' => 'daily', 'daily' => 'daily',
'month' => 'monthly', 'monthly' => 'monthly',
'year' => 'yearly', 'yearly' => 'yearly'];
$rate_type = $rate_map[$raw_rate] ?? 'monthly';
// Pricing: billing_config > billing_orders flat price
$price_per_player = $default_price_player;
$player_slots = max(0, intval($srv['qty'] ?? 0));
$subtotal = $price_per_player * max(1, $player_slots);
if ($subtotal == 0.00 && floatval($srv['price'] ?? 0) > 0) {
$subtotal = floatval($srv['price']);
}
$total_due = $subtotal;
// Calculate due_date: now + 1 billing period
$period_map = ['daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year'];
$due_date_ts = strtotime($period_map[$rate_type], time());
$due_date = date('Y-m-d H:i:s', $due_date_ts);
// Guard: skip if an invoice for this exact period already exists
$exists = $db->resultQuery("
SELECT invoice_id FROM {$table_prefix}invoices
WHERE home_id = {$home_id}
AND due_date = '" . $db->realEscapeSingle($due_date) . "'
LIMIT 1
");
if (is_array($exists) && !empty($exists)) {
$db->logger("BILLING-CRON: Step A - SKIP home {$home_id}: invoice for this period already exists");
continue;
}
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
$remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'],
$server_info['encryption_key'], $server_info['timeout']);
// Disable FTP
$ftp_login = isset($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id;
$remote->ftp_mgr("userdel", $ftp_login);
$db->changeFtpStatus('disabled', $home_id);
// Stop the server
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
$control_type = isset($server_xml->control_protocol_type) ? $server_xml->control_protocol_type : "";
$addresses = $db->getHomeIpPorts($home_id);
foreach ((array)$addresses as $address) {
$remote->remote_stop_server($home_id, $address['ip'], $address['port'],
$server_xml->control_protocol, $home_info['control_password'],
$control_type, $home_info['home_path']);
}
// Unassign from user
$db->unassignHomeFrom("user", $user_id, $home_id);
// Update order status
$db->query("UPDATE " . $table_prefix . "billing_orders SET status='suspended' WHERE order_id={$order_id}");
$db->logger("BILLING-CRON: SUSPENDED server {$home_id} for order {$order_id} due to unpaid invoice");
// Send suspension email
$settings = $db->getSettings();
$subject = "Server Suspended - " . $order['home_name'] . " - " . $panel_settings['panel_name'];
$message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) has been suspended due to non-payment." .
"<br><br>Your server has been stopped and will be permanently deleted in 7 days if payment is not received." .
"<br><br>Please log in to your account and pay your outstanding invoice to restore your server." .
"<br><br>Thank you.";
$mail = mymail($order['users_email'], $subject, $message, $settings);
if (!$mail) {
$db->logger("BILLING-CRON: Email FAILED - Suspension notice for order {$order_id}");
// Create renewal invoice in {prefix}invoices
$db->query("
INSERT INTO {$table_prefix}invoices
(home_id, user_id, due_date, billing_status, rate_type,
price_per_player, player_slots, quantity, subtotal, total_due)
VALUES (
{$home_id}, {$user_id},
'" . $db->realEscapeSingle($due_date) . "',
'Invoiced',
'" . $db->realEscapeSingle($rate_type) . "',
" . number_format($price_per_player, 2, '.', '') . ",
{$player_slots},
{$qty},
" . number_format($subtotal, 2, '.', '') . ",
" . number_format($total_due, 2, '.', '') . "
)
");
$new_invoice_id = $db->lastInsertId();
// Update server_homes: set Invoiced, store invoice id and expiration date
$db->query("
UPDATE {$table_prefix}server_homes
SET billing_status = 'Invoiced',
server_expiration_date = '" . $db->realEscapeSingle($due_date) . "',
last_invoice_id = " . intval($new_invoice_id) . "
WHERE home_id = {$home_id}
");
$db->logger("BILLING-CRON: Step A - INVOICED home {$home_id} (invoice #{$new_invoice_id}, due {$due_date})");
// Send renewal notice
if (!empty($srv['users_email'])) {
$settings = $db->getSettings();
$subject = "Renewal Invoice for {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel');
$message = "Your server '{$home_name}' (ID: {$home_id}) has a renewal invoice due on "
. date('F j, Y', $due_date_ts) . "."
. "<br><br>Amount Due: \$" . number_format($total_due, 2)
. "<br>Due Date: " . date('F j, Y', $due_date_ts)
. "<br><br>Please log in to pay your invoice and keep your server active."
. "<br><br>Thank you!";
if (!mymail($srv['users_email'], $subject, $message, $settings)) {
$db->logger("BILLING-CRON: Step A - Email FAILED for home {$home_id}");
}
}
}
}
// ==================================================================================
// STEP 3: DELETE SERVERS THAT HAVE BEEN SUSPENDED FOR 7+ DAYS
// ==================================================================================
// Find servers that:
// - Are currently suspended
// - Have been suspended for at least 7 days (end_date + 7 days has passed)
// - Still have unpaid invoices
$servers_to_delete = $db->resultQuery("
SELECT DISTINCT o.*, u.users_email
FROM " . $table_prefix . "billing_orders o
LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id
INNER JOIN " . $table_prefix . "billing_invoices i ON o.order_id = i.order_id
WHERE o.status = 'suspended'
AND o.end_date IS NOT NULL
AND UNIX_TIMESTAMP(o.end_date) < {$removal_date}
AND i.status = 'unpaid'
// ======================================================================
// STEP B - Invoiced -> Expired
// Servers whose expiration date has passed and whose last invoice
// is still unpaid.
// ======================================================================
$db->logger("BILLING-CRON: --- Step B: Invoiced -> Expired ---");
$past_due = $db->resultQuery("
SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id,
sh.last_invoice_id, sh.server_expiration_date,
u.users_email
FROM {$table_prefix}server_homes sh
LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main
WHERE sh.billing_enabled = 1
AND sh.billing_status = 'Invoiced'
AND sh.server_expiration_date IS NOT NULL
AND sh.server_expiration_date < NOW()
AND (
sh.last_invoice_id IS NULL
OR EXISTS (
SELECT 1 FROM {$table_prefix}invoices inv
WHERE inv.invoice_id = sh.last_invoice_id
AND inv.billing_status = 'Invoiced'
AND inv.paid_at IS NULL
)
)
ORDER BY sh.home_id ASC
");
if (is_array($servers_to_delete)) {
foreach ((array)$servers_to_delete as $order) {
$user_id = $order['user_id'];
$home_id = $order['home_id'];
$order_id = $order['order_id'];
// Get home and server info
if (is_array($past_due)) {
foreach ($past_due as $srv) {
$home_id = intval($srv['home_id']);
$last_invoice_id = intval($srv['last_invoice_id'] ?? 0);
// Mark server Expired
$db->query("
UPDATE {$table_prefix}server_homes
SET billing_status = 'Expired'
WHERE home_id = {$home_id}
");
// Mark matching invoice Expired (if still unpaid)
if ($last_invoice_id > 0) {
$db->query("
UPDATE {$table_prefix}invoices
SET billing_status = 'Expired'
WHERE invoice_id = {$last_invoice_id}
AND billing_status = 'Invoiced'
AND paid_at IS NULL
");
}
$db->logger("BILLING-CRON: Step B - EXPIRED home {$home_id}");
// Notify user
if (!empty($srv['users_email'])) {
$settings = $db->getSettings();
$home_name = $srv['home_name'] ?? 'Server #' . $home_id;
$subject = "Server Expired - {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel');
$message = "Your server '{$home_name}' (ID: {$home_id}) has expired due to non-payment."
. "<br><br>The server will be permanently deleted in {$delete_after_days} day(s) if payment is not received."
. "<br><br>Please log in and pay your outstanding invoice to restore service."
. "<br><br>Thank you.";
if (!mymail($srv['users_email'], $subject, $message, $settings)) {
$db->logger("BILLING-CRON: Step B - Email FAILED for home {$home_id}");
}
}
}
}
// ======================================================================
// STEP C - Expired -> Deleted
// Servers that have been Expired longer than delete_after_expired_days.
// ======================================================================
$db->logger("BILLING-CRON: --- Step C: Expired -> Deleted (window={$delete_after_days}d) ---");
$to_delete = $db->resultQuery("
SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id,
sh.server_expiration_date,
u.users_email
FROM {$table_prefix}server_homes sh
LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main
WHERE sh.billing_enabled = 1
AND sh.billing_status = 'Expired'
AND sh.server_expiration_date IS NOT NULL
AND sh.server_expiration_date < DATE_SUB(NOW(), INTERVAL {$delete_after_days} DAY)
ORDER BY sh.home_id ASC
");
if (is_array($to_delete)) {
foreach ($to_delete as $srv) {
$home_id = intval($srv['home_id']);
$user_id = intval($srv['user_id']);
$home_name = $srv['home_name'] ?? 'Server #' . $home_id;
// Fetch home info for remote deletion
$home_info = $db->getGameHomeWithoutMods($home_id);
if ($home_info) {
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
$remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'],
$server_info['encryption_key'], $server_info['timeout']);
// Remove the game home from db
$db->deleteGameHome($home_id);
// Remove the game home files from remote server
$remote->remove_home($home_info['home_path']);
// Drop database and user if they exist (both user_#### and server_#### formats)
@$db->query("DROP USER 'user_" . $home_id . "'@'%'");
@$db->query("DROP USER 'user_" . $home_id . "'@'localhost'");
@$db->query("DROP USER 'server_" . $home_id . "'@'%'");
@$db->query("DROP USER 'server_" . $home_id . "'@'localhost'");
@$db->query("DROP DATABASE IF EXISTS user_" . $home_id);
@$db->query("DROP DATABASE IF EXISTS server_" . $home_id);
if ($server_info) {
$remote = new OGPRemoteLibrary(
$server_info['agent_ip'],
$server_info['agent_port'],
$server_info['encryption_key'],
$server_info['timeout']
);
// Stop the running server process
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
$control_type = isset($server_xml->control_protocol_type)
? (string)$server_xml->control_protocol_type : "";
$addresses = $db->getHomeIpPorts($home_id);
foreach ((array)$addresses as $addr) {
$remote->remote_stop_server(
$home_id, $addr['ip'], $addr['port'],
$server_xml->control_protocol,
$home_info['control_password'],
$control_type,
$home_info['home_path']
);
}
// Disable FTP
$ftp_login = !empty($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id;
$remote->ftp_mgr("userdel", $ftp_login);
$db->changeFtpStatus('disabled', $home_id);
// Unassign from user
$db->unassignHomeFrom("user", $user_id, $home_id);
// Delete home record from panel DB
$db->deleteGameHome($home_id);
// Remove server files on remote agent
$remote->remove_home($home_info['home_path']);
// Drop any per-server database/user accounts
@$db->query("DROP USER 'user_{$home_id}'@'%'");
@$db->query("DROP USER 'user_{$home_id}'@'localhost'");
@$db->query("DROP USER 'server_{$home_id}'@'%'");
@$db->query("DROP USER 'server_{$home_id}'@'localhost'");
@$db->query("DROP DATABASE IF EXISTS user_{$home_id}");
@$db->query("DROP DATABASE IF EXISTS server_{$home_id}");
} else {
$db->logger("BILLING-CRON: Step C - WARNING: no remote server info for home {$home_id}; removing panel record only");
$db->deleteGameHome($home_id);
}
} else {
$db->logger("BILLING-CRON: Step C - WARNING: home {$home_id} not found in panel DB (already removed)");
}
// Update order status and clear home_id
$db->query("UPDATE " . $table_prefix . "billing_orders
SET status='deleted', home_id='0'
WHERE order_id={$order_id}");
// Mark all unpaid invoices for this order as deleted
$db->query("UPDATE " . $table_prefix . "billing_invoices
SET status='deleted'
WHERE order_id={$order_id} AND status='unpaid'");
$db->logger("BILLING-CRON: DELETED server {$home_id} for order {$order_id} after 7 days suspended");
// Send deletion email
$settings = $db->getSettings();
$subject = "Server Permanently Deleted - " . $order['home_name'] . " - " . $panel_settings['panel_name'];
$message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) has been permanently deleted." .
"<br><br>The server was suspended 7 days ago due to non-payment and has now been removed." .
"<br><br>If this was an error and you contact us immediately, we may be able to restore your server from backups." .
"<br><br>Thank you for being a customer. We hope to serve you again in the future.";
$mail = mymail($order['users_email'], $subject, $message, $settings);
if (!$mail) {
$db->logger("BILLING-CRON: Email FAILED - Deletion notice for order {$order_id}");
// Mark billing_orders record as Expired and clear home_id reference
$db->query("
UPDATE {$table_prefix}billing_orders
SET status = 'Expired',
home_id = '0'
WHERE home_id = '{$home_id}'
");
// Mark any open gsp_invoices for this home as Expired
$db->query("
UPDATE {$table_prefix}invoices
SET billing_status = 'Expired'
WHERE home_id = {$home_id}
AND billing_status = 'Invoiced'
");
$db->logger("BILLING-CRON: Step C - DELETED home {$home_id}");
// Notify user
if (!empty($srv['users_email'])) {
$settings = $db->getSettings();
$subject = "Server Permanently Deleted - {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel');
$message = "Your server '{$home_name}' (ID: {$home_id}) has been permanently deleted."
. "<br><br>The server expired and was removed after the grace period."
. "<br><br>If this was an error, contact us immediately - we may be able to restore from backup."
. "<br><br>Thank you for being a customer. We hope to serve you again.";
if (!mymail($srv['users_email'], $subject, $message, $settings)) {
$db->logger("BILLING-CRON: Step C - Email FAILED for home {$home_id}");
}
}
}
}
$db->logger("BILLING-CRON: Server lifecycle automation completed");
?>
// ======================================================================
// STEP D - Paid invoice safety net
// If a payment was recorded on a {prefix}invoices row but the
// server_home was not updated (e.g. race condition at capture time),
// correct it here so the server is restored to Active.
// ======================================================================
$db->logger("BILLING-CRON: --- Step D: Paid invoice safety-net ---");
$paid_invoices = $db->resultQuery("
SELECT inv.invoice_id, inv.home_id, inv.rate_type,
sh.billing_status
FROM {$table_prefix}invoices inv
INNER JOIN {$table_prefix}server_homes sh ON sh.home_id = inv.home_id
WHERE inv.billing_status = 'Invoiced'
AND sh.billing_status = 'Invoiced'
AND (inv.paid_at IS NOT NULL OR inv.payment_id IS NOT NULL)
ORDER BY inv.invoice_id ASC
");
//THESE SERVERS HAVE REACHED THE DATE FOR INVOICE, END_DATE - 7 (OR WHAT IS IN SETTINGS)
//SET STATUS 'invoiced' MEANING INVOICE SHOULD BE CREATED
//LOOP THROUGH ALL SERVERS WITH STATUS = 'paid' OR 'installed' (ACTIVE) -----------------------------------------------------------
$user_homes = $db->resultQuery( "SELECT *
FROM " . $table_prefix . "billing_orders
WHERE status IN ('paid', 'installed') AND end_date <" . $invoice_date);
if (is_array($paid_invoices)) {
foreach ($paid_invoices as $inv) {
$home_id = intval($inv['home_id']);
$invoice_id = intval($inv['invoice_id']);
$rate_type = $inv['rate_type'] ?? 'monthly';
if (!is_array($user_homes))
{
}
else
{
foreach ((array)$user_homes as $user_home)
{
// Calculate next_invoice_date based on rate_type
$period_map = ['daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year'];
$next_invoice_date = date('Y-m-d H:i:s', strtotime($period_map[$rate_type] ?? '+1 month'));
// Developer note:
// In future we may want to change the renewal/invoice strategy so that a
// new order record is created for the renewal (leaving the original order
// intact) instead of mutating the existing order's status/end_date.
// Creating a separate renewal order gives a clearer, immutable purchase
// history and simplifies auditing. For now this cron job continues to
// update the existing order (change status/end_date) as implemented
// below.
$db->query("
UPDATE {$table_prefix}invoices
SET billing_status = 'Active'
WHERE invoice_id = {$invoice_id}
");
$user_id = $user_home['user_id'];
$home_id = $user_home['home_id'];
// Reset the STATUS 'invoiced' so cart.php will create an invoice
$db->query( "UPDATE " . $table_prefix . "billing_orders
SET status='invoiced'
WHERE order_id=".$db->realEscapeSingle($user_home['order_id']));
$db->query("
UPDATE {$table_prefix}server_homes
SET billing_status = 'Active',
next_invoice_date = '" . $db->realEscapeSingle($next_invoice_date) . "',
server_expiration_date = NULL
WHERE home_id = {$home_id}
");
// SEND EMAIL
$settings = $db->getSettings();
$subject = "You have an INVOICE at ". $panel_settings['panel_name'];
$email = $db->resultQuery(" SELECT DISTINCT users_email
FROM " . $table_prefix . "users, " . $table_prefix . "billing_orders
WHERE " . $table_prefix . "users.user_id = $user_id")[0]["users_email"];
$message = "Your server with ID ". $home_id . " will expire soon. Please log in and VIEW INVOICES on the Dashboard to renew your server.<br><br><br>~<br>Thanks!<br>";
$mail = mymail($email, $subject, $message, $settings);
//logger
$db->logger( "AUTO-CLEAN: INVOICE created for server " . $home_id);
if (!$mail)
$db->logger( "AUTO-CLEAN: Email FAILED - Server Invoiced " . $home_id);
// END EMAIL
}
$db->logger("BILLING-CRON: Step D - RESTORED home {$home_id} to Active via paid invoice #{$invoice_id}");
}
}
//THESE ARE THE SERVERS THAT HAVE NOT BEEN PAID AND THE END_DATE IS TODAY
//THESE SERVERS GET SUSPENDED
//LOOP THROUGH ALL ORDERS WITH STATUS 'invoiced' OR 'in-cart' OR 'unknown' (INACTIVE OR INVOICED)
$user_homes = $db->resultQuery( "SELECT *
FROM " . $table_prefix . "billing_orders
WHERE status IN ('invoiced', 'in-cart', 'unknown') AND end_date < ".$today);
if (!is_array($user_homes))
{
}
else
{
foreach ((array)$user_homes as $user_home)
{
$user_id = $user_home['user_id'];
$home_id = $user_home['home_id'];
$home_info = $db->getGameHomeWithoutMods($home_id);
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
$remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'], $server_info['encryption_key'],$server_info['timeout']);
$ftp_login = isset($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id;
$remote->ftp_mgr("userdel", $ftp_login);
$db->changeFtpStatus('disabled',$home_id);
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
if(isset($server_xml->control_protocol_type))$control_type = $server_xml->control_protocol_type; else $control_type = "";
$addresses = $db->getHomeIpPorts($home_id);
foreach ((array)$addresses as $address)
{
$remote->remote_stop_server($home_id,$address['ip'],$address['port'],$server_xml->control_protocol,$home_info['control_password'],$control_type,$home_info['home_path']);
}
$db->unassignHomeFrom("user", $user_id, $home_id);
// Reset the invoice end date to 'suspended'
// User can still RENEW server
$db->query( "UPDATE " . $table_prefix . "billing_orders
SET status='suspended'
WHERE order_id=".$db->realEscapeSingle($user_home['order_id']));
//logger
$db->logger( "AUTO-CLEAN: SUSPENDED server " . $home_id);
// SEND EMAIL
$settings = $db->getSettings();
$subject = "GameServer Suspended at ". $panel_settings['panel_name'];
$email = $db->resultQuery(" SELECT DISTINCT users_email
FROM " . $table_prefix . "users, " . $table_prefix . "billing_orders
WHERE " . $table_prefix . "users.user_id = $user_id")[0]["users_email"];
$message = "Your server with ID ". $home_id . " has expired and has been suspended. Please log in and VIEW INVOICES on the Dashboard to renew your server.<br>~<br>Thanks!<br>";
$mail = mymail($email, $subject, $message, $settings);
if (!$mail)
$db->logger( "AUTO-CLEAN: Email FAILED - Server Suspended " . $home_id);
// END EMAIL
}
}
// end date = 'suspended' (suspended) and its been suspended for $removal_date days
//set removed servers as 'deleted'
$user_homes = $db->resultQuery( "SELECT *
FROM " . $table_prefix . "billing_orders
WHERE status = 'suspended' AND end_date < ".$removal_date );
if (!is_array($user_homes))
{
}
else
{
foreach ((array)$user_homes as $user_home)
{
$user_id = $user_home['user_id'];
$home_id = $user_home['home_id'];
$home_info = $db->getGameHomeWithoutMods($home_id);
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
$remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'], $server_info['encryption_key'],$server_info['timeout']);
// Remove the game home from db
$db->deleteGameHome($home_id);
// Remove the game home files from remote server
$remote->remove_home($home_info['home_path']);
// Reset the invoice end date
$db->query( "UPDATE " . $table_prefix . "billing_orders
SET status='deleted'
WHERE order_id=".$db->realEscapeSingle($user_home['order_id']));
// Set order as not installed
$db->query( "UPDATE " . $table_prefix . "billing_orders
SET home_id=0
WHERE order_id=".$db->realEscapeSingle($user_home['order_id']));
// Mark all unpaid invoices for this order as deleted
$db->query("UPDATE " . $table_prefix . "billing_invoices
SET status='deleted'
WHERE order_id=".$db->realEscapeSingle($user_home['order_id'])." AND status='unpaid'");
// remove userid and table from database (both user_#### and server_#### formats)
@$db->query( "DROP USER 'user_" .$home_id ."'@'%'");
@$db->query( "DROP USER 'user_" .$home_id ."'@'localhost'");
@$db->query( "DROP USER 'server_" .$home_id ."'@'%'");
@$db->query( "DROP USER 'server_" .$home_id ."'@'localhost'");
@$db->query( "DROP DATABASE IF EXISTS user_" .$home_id);
@$db->query( "DROP DATABASE IF EXISTS server_" .$home_id);
//logger
$db->logger( "AUTO-CLEAN: DELETED server " . $home_id);
// SEND EMAIL
$settings = $db->getSettings();
$settings = $db->getSettings();
$subject = "GameServer DELETED at ". $panel_settings['panel_name'];
$email = $db->resultQuery(" SELECT DISTINCT users_email
FROM " . $table_prefix . "users, " . $table_prefix . "billing_orders
WHERE " . $table_prefix . "users.user_id = $user_id")[0]["users_email"];
$message = "Your server with ID ". $home_id . " has been deleted<br><br>You did not renew the service and it was PERMANENTLY REMOVED today. If this was an error, if you contact us immediately we may be able to restore your server.<br>Thanks for being a customer and we hope we can provide a server for you again.<br><br>";
$mail = mymail($email, $subject, $message, $settings);
if (!$mail)
$db->logger( "AUTO-CLEAN: Email FAILED - Server Deleted " . $home_id);
// END EMAIL
}
}
?>
$db->logger("BILLING-CRON: ===== Lifecycle automation completed at " . date('Y-m-d H:i:s') . " =====");