Merge pull request #89 from GameServerPanel/copilot/update-billing-status-system
This commit is contained in:
commit
1d5a090b01
11 changed files with 740 additions and 500 deletions
|
|
@ -44,12 +44,9 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
|||
.btn-save { background: #28a745; color: white; border: none; padding: 5px 12px; border-radius: 3px; cursor: pointer; }
|
||||
.btn-save:hover { background: #218838; }
|
||||
.status-badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: 600; }
|
||||
.status-paid { background: #d4edda; color: #155724; }
|
||||
.status-pending { background: #fff3cd; color: #856404; }
|
||||
.status-in-cart { background: #d1ecf1; color: #0c5460; }
|
||||
.status-expired { background: #f8d7da; color: #721c24; }
|
||||
.status-renew { background: #cce5ff; color: #004085; }
|
||||
.status-installed { background: #d4edda; color: #155724; }
|
||||
.status-Active { background: #d4edda; color: #155724; }
|
||||
.status-Invoiced { background: #fff3cd; color: #856404; }
|
||||
.status-Expired { background: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -128,12 +125,9 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
|||
<label style="margin-right: 15px;">
|
||||
<strong>Status:</strong>
|
||||
<select name="status" class="edit-select" required>
|
||||
<option value="in-cart" <?php echo $row['status'] === 'in-cart' ? 'selected' : ''; ?>>IN-CART</option>
|
||||
<option value="paid" <?php echo $row['status'] === 'paid' ? 'selected' : ''; ?>>PAID</option>
|
||||
<option value="installed" <?php echo $row['status'] === 'installed' ? 'selected' : ''; ?>>INSTALLED</option>
|
||||
<option value="renew" <?php echo $row['status'] === 'renew' ? 'selected' : ''; ?>>RENEW</option>
|
||||
<option value="pending" <?php echo $row['status'] === 'pending' ? 'selected' : ''; ?>>PENDING</option>
|
||||
<option value="expired" <?php echo $row['status'] === 'expired' ? 'selected' : ''; ?>>EXPIRED</option>
|
||||
<option value="Active" <?php echo $row['status'] === 'Active' ? 'selected' : ''; ?>>ACTIVE</option>
|
||||
<option value="Invoiced" <?php echo $row['status'] === 'Invoiced' ? 'selected' : ''; ?>>INVOICED</option>
|
||||
<option value="Expired" <?php echo $row['status'] === 'Expired' ? 'selected' : ''; ?>>EXPIRED</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" name="update_invoice" class="btn-save">Save Changes</button>
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ function exec_ogp_module()
|
|||
header("Location: home.php?m=billing&p=provision_servers&order_id=".$order_id);
|
||||
exit;
|
||||
break;
|
||||
case 'suspend':
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='suspended' WHERE order_id=".$order_id);
|
||||
case 'expire':
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Expired' WHERE order_id=".$order_id);
|
||||
break;
|
||||
case 'activate':
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='paid' WHERE order_id=".$order_id);
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Active' WHERE order_id=".$order_id);
|
||||
break;
|
||||
case 'delete':
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='deleted' WHERE order_id=".$order_id);
|
||||
case 'invoice':
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Invoiced' WHERE order_id=".$order_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -56,12 +56,9 @@ function exec_ogp_module()
|
|||
echo "<input type='hidden' name='p' value='admin_orders'>";
|
||||
echo "Status: <select name='status' onchange='this.form.submit()'>";
|
||||
echo "<option value='all' ".($status_filter == 'all' ? 'selected' : '').">All Orders</option>";
|
||||
echo "<option value='in-cart' ".($status_filter == 'in-cart' ? 'selected' : '').">In Cart</option>";
|
||||
echo "<option value='paid' ".($status_filter == 'paid' ? 'selected' : '').">Paid (Awaiting Provision)</option>";
|
||||
echo "<option value='installed' ".($status_filter == 'installed' ? 'selected' : '').">Installed (Active)</option>";
|
||||
echo "<option value='invoiced' ".($status_filter == 'invoiced' ? 'selected' : '').">Renewal Invoiced</option>";
|
||||
echo "<option value='suspended' ".($status_filter == 'suspended' ? 'selected' : '').">Suspended</option>";
|
||||
echo "<option value='deleted' ".($status_filter == 'deleted' ? 'selected' : '').">Deleted</option>";
|
||||
echo "<option value='Active' ".($status_filter == 'Active' ? 'selected' : '').">Active</option>";
|
||||
echo "<option value='Invoiced' ".($status_filter == 'Invoiced' ? 'selected' : '').">Invoiced</option>";
|
||||
echo "<option value='Expired' ".($status_filter == 'Expired' ? 'selected' : '').">Expired</option>";
|
||||
echo "</select> ";
|
||||
echo "Search: <input type='text' name='search' value='".$search."' placeholder='Order ID, username, server name...'> ";
|
||||
echo "<button type='submit' class='btn'>Filter</button>";
|
||||
|
|
@ -101,9 +98,9 @@ function exec_ogp_module()
|
|||
echo "<select name='bulk_action'>";
|
||||
echo "<option value=''>-- Choose Action --</option>";
|
||||
echo "<option value='provision'>Provision Servers</option>";
|
||||
echo "<option value='activate'>Set to Paid (Activate)</option>";
|
||||
echo "<option value='suspend'>Suspend</option>";
|
||||
echo "<option value='delete'>Delete</option>";
|
||||
echo "<option value='activate'>Set Active</option>";
|
||||
echo "<option value='invoice'>Set Invoiced</option>";
|
||||
echo "<option value='expire'>Set Expired</option>";
|
||||
echo "</select> ";
|
||||
echo "<button type='submit' class='btn'>Apply</button>";
|
||||
echo "</div>";
|
||||
|
|
@ -128,11 +125,10 @@ function exec_ogp_module()
|
|||
foreach ((array)$orders as $order) {
|
||||
$status_class = '';
|
||||
switch ($order['status']) {
|
||||
case 'paid': $status_class = 'label-warning'; break;
|
||||
case 'installed': $status_class = 'label-success'; break;
|
||||
case 'suspended': $status_class = 'label-danger'; break;
|
||||
case 'deleted': $status_class = 'label-default'; break;
|
||||
default: $status_class = 'label-info';
|
||||
case 'Active': $status_class = 'label-success'; break;
|
||||
case 'Invoiced': $status_class = 'label-warning'; break;
|
||||
case 'Expired': $status_class = 'label-danger'; break;
|
||||
default: $status_class = 'label-info';
|
||||
}
|
||||
|
||||
echo "<tr>";
|
||||
|
|
@ -150,11 +146,11 @@ function exec_ogp_module()
|
|||
echo "<td>".($order['home_id'] ? $order['home_id'] : 'N/A')."</td>";
|
||||
echo "<td>";
|
||||
|
||||
if ($order['status'] == 'paid') {
|
||||
if ($order['status'] == 'Active' && !$order['home_id']) {
|
||||
echo "<a href='home.php?m=billing&p=provision_servers&order_id=".$order['order_id']."' class='btn btn-sm'>Provision</a> ";
|
||||
}
|
||||
|
||||
if ($order['status'] == 'installed' && $order['home_id']) {
|
||||
if ($order['status'] == 'Active' && $order['home_id']) {
|
||||
echo "<a href='home.php?m=gamemanager&p=game_monitor&home_id-mod_id-ip=".$order['home_id']."' class='btn btn-sm'>View Server</a> ";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,12 +210,13 @@ if ($coupon_id > 0) {
|
|||
}
|
||||
}
|
||||
|
||||
// Mark all due invoices for this user as paid
|
||||
// Mark all due invoices for this user as paid.
|
||||
// Note: billing_invoices is the pre-purchase cart table and uses its own
|
||||
// status vocabulary (due -> paid). This is separate from gsp_invoices
|
||||
// (renewal invoices) and server_homes.billing_status (Active/Invoiced/Expired).
|
||||
$updateInvoicesSql = "UPDATE {$table_prefix}billing_invoices
|
||||
SET status='paid', paid_date='$now', payment_txid='$esc_txid', payment_method='paypal'
|
||||
WHERE user_id=$user_id AND status='due'";
|
||||
|
||||
log_payment('UPDATE_INVOICES_SQL', $updateInvoicesSql);
|
||||
$updateResult = mysqli_query($mysqli, $updateInvoicesSql);
|
||||
|
||||
if (!$updateResult) {
|
||||
|
|
@ -265,7 +266,7 @@ while ($inv = mysqli_fetch_assoc($invoicesResult)) {
|
|||
}
|
||||
$newEndDate = date('Y-m-d H:i:s', strtotime("+$qty $durationUnit", $baseTs));
|
||||
$renewSql = "UPDATE {$table_prefix}billing_orders
|
||||
SET status='installed', end_date='$newEndDate', paid_ts='$now', payment_txid='$esc_txid'
|
||||
SET status='Active', end_date='$newEndDate', paid_ts='$now', payment_txid='$esc_txid'
|
||||
WHERE order_id=$existing_order_id LIMIT 1";
|
||||
if (mysqli_query($mysqli, $renewSql)) {
|
||||
$renewedOrders++;
|
||||
|
|
@ -274,6 +275,28 @@ while ($inv = mysqli_fetch_assoc($invoicesResult)) {
|
|||
'invoice_id' => $invoice_id,
|
||||
'new_end_date' => $newEndDate
|
||||
]);
|
||||
|
||||
// Also update server_homes.billing_status and next_invoice_date
|
||||
$homeIdRow = mysqli_query($mysqli, "SELECT home_id FROM {$table_prefix}billing_orders WHERE order_id=$existing_order_id LIMIT 1");
|
||||
if ($homeIdRow && mysqli_num_rows($homeIdRow) === 1) {
|
||||
$homeData = mysqli_fetch_assoc($homeIdRow);
|
||||
$home_id_upd = intval($homeData['home_id'] ?? 0);
|
||||
if ($home_id_upd > 0) {
|
||||
$next_inv_date = mysqli_real_escape_string($mysqli, $newEndDate);
|
||||
mysqli_query($mysqli, "UPDATE {$table_prefix}server_homes
|
||||
SET billing_status = 'Active',
|
||||
next_invoice_date = '$next_inv_date',
|
||||
server_expiration_date = NULL
|
||||
WHERE home_id = $home_id_upd");
|
||||
// Mark the matching gsp_invoices renewal invoice as Active
|
||||
mysqli_query($mysqli, "UPDATE {$table_prefix}invoices
|
||||
SET billing_status = 'Active',
|
||||
paid_at = '$now',
|
||||
payment_id = '$esc_txid'
|
||||
WHERE home_id = $home_id_upd AND billing_status = 'Invoiced'");
|
||||
log_payment('SERVER_HOME_ACTIVATED', ['home_id' => $home_id_upd]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_payment('ORDER_RENEWAL_FAILED', [
|
||||
'order_id' => $existing_order_id,
|
||||
|
|
@ -298,14 +321,14 @@ while ($inv = mysqli_fetch_assoc($invoicesResult)) {
|
|||
// Calculate end_date
|
||||
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
|
||||
|
||||
// Insert order with status='paid' (panel will provision and change to 'active')
|
||||
// Insert order with status='Active' (server will be provisioned automatically)
|
||||
$insertOrderSql = "INSERT INTO {$table_prefix}billing_orders (
|
||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||
price, remote_control_password, ftp_password, status, order_date, end_date,
|
||||
payment_txid, paid_ts, paypal_data
|
||||
) VALUES (
|
||||
$user_id, $service_id, '$home_name', $ip, $max_players, $qty, '$duration',
|
||||
$amount, '$rcon_pw', '$ftp_pw', 'paid', '$now', '$end_date',
|
||||
$amount, '$rcon_pw', '$ftp_pw', 'Active', '$now', '$end_date',
|
||||
'$esc_txid', '$now', '$esc_paypal_json'
|
||||
)";
|
||||
|
||||
|
|
|
|||
|
|
@ -45,12 +45,12 @@ function exec_ogp_module()
|
|||
}
|
||||
}
|
||||
|
||||
// Handle provision_all request - provision all paid orders for this user
|
||||
// Handle provision_all request - provision all Active (paid) orders for this user
|
||||
if ($provision_all) {
|
||||
if ( $isAdmin ){
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE status='paid' ORDER BY order_id" );
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" );
|
||||
} else {
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='paid' ORDER BY order_id" );
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" );
|
||||
}
|
||||
}
|
||||
// Handle provision_single or order_id parameter - provision specific order
|
||||
|
|
@ -62,9 +62,9 @@ function exec_ogp_module()
|
|||
}
|
||||
$idList = implode(',', array_map('intval', $orderIds));
|
||||
if ( $isAdmin ){
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND status='paid'" );
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND status='Active'" );
|
||||
} else {
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='paid'" );
|
||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='Active'" );
|
||||
}
|
||||
}
|
||||
$processed_orders = array();
|
||||
|
|
@ -338,16 +338,10 @@ function exec_ogp_module()
|
|||
|
||||
|
||||
}
|
||||
// Set expiration date in ogp database
|
||||
//status is: in-cart, paid, installed, invoiced, suspended, deleted
|
||||
// 'paid' - order has been paid but server not yet created
|
||||
// 'installed' - server created and active
|
||||
// 'invoiced' - invoice created for renewal
|
||||
// 'suspended' - server suspended for non-payment
|
||||
// 'deleted' - server deleted after extended suspension
|
||||
//end_date the server will be suspended
|
||||
//in cron_shop the end_date is used to delete the server
|
||||
//several days after being suspended
|
||||
// Set expiration date in panel database
|
||||
// Status values: Active (provisioned & current), Invoiced (renewal invoice open),
|
||||
// Expired (past due and awaiting deletion)
|
||||
// end_date / next_invoice_date: when the next renewal invoice should be generated
|
||||
if ($order['invoice_duration'] == "day")
|
||||
{
|
||||
|
||||
|
|
@ -397,19 +391,28 @@ function exec_ogp_module()
|
|||
}
|
||||
|
||||
}
|
||||
// set order status to 'installed' to indicate server has been provisioned
|
||||
$end_date_str = date('Y-m-d H:i:s', $end_date);
|
||||
|
||||
// Set order status to 'Active' (server provisioned and current)
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
||||
SET status='installed'
|
||||
SET status='Active'
|
||||
WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||
|
||||
// set the order expiration
|
||||
// Set the order expiration / next renewal date
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
||||
SET end_date='" . $db->realEscapeSingle($end_date) . "'
|
||||
SET end_date='" . $db->realEscapeSingle($end_date_str) . "'
|
||||
WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||
|
||||
// Save home id created by this order
|
||||
// Save home_id created by this order
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
||||
SET home_id='" . $db->realEscapeSingle($home_id) . "' WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||
|
||||
// Set billing_status and next_invoice_date on server_homes
|
||||
$db->query("UPDATE OGP_DB_PREFIXserver_homes
|
||||
SET billing_status = 'Active',
|
||||
next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "',
|
||||
billing_enabled = 1
|
||||
WHERE home_id = " . $db->realEscapeSingle($home_id));
|
||||
|
||||
$provisioned_count++;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,414 @@
|
|||
* 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', 'month' => 'monthly', 'year' => 'yearly'];
|
||||
$rate_type = $rate_map[$raw_rate] ?? $raw_rate;
|
||||
|
||||
// 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 (grace_days={$grace_days}) ---");
|
||||
|
||||
$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 < DATE_SUB(NOW(), INTERVAL {$grace_days} DAY)
|
||||
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') . " =====");
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ function process_payment_record(array $record) {
|
|||
$dt = new DateTime($extend_from);
|
||||
if ($months > 0) $dt->modify('+' . intval($months) . ' months');
|
||||
$new_end = $dt->format('Y-m-d H:i:s');
|
||||
$update = "UPDATE `" . $TABLE_PREFIX . "billing_orders` SET end_date = ?, status='paid', payment_txid = ?, paid_ts = ? WHERE order_id = ?";
|
||||
$update = "UPDATE `" . $TABLE_PREFIX . "billing_orders` SET end_date = ?, status='Active', payment_txid = ?, paid_ts = ? WHERE order_id = ?";
|
||||
if ($u = mysqli_prepare($db, $update)) {
|
||||
mysqli_stmt_bind_param($u, 'sssi', $new_end, $esc_txid, $now, $order_id);
|
||||
mysqli_stmt_execute($u);
|
||||
|
|
@ -189,7 +189,7 @@ function process_payment_record(array $record) {
|
|||
$price = number_format($invoice_amount, 2, '.', '');
|
||||
|
||||
$insert2 = sprintf(
|
||||
"INSERT INTO `%s` (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, order_date, end_date, payment_txid, paid_ts) VALUES (%d, %d, '%s', %d, %d, %d, '%s', %s, '%s', '%s', 'paid', '%s', '%s', '%s', '%s')",
|
||||
"INSERT INTO `%s` (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, order_date, end_date, payment_txid, paid_ts) VALUES (%d, %d, '%s', %d, %d, %d, '%s', %s, '%s', '%s', 'Active', '%s', '%s', '%s', '%s')",
|
||||
$TABLE_PREFIX . 'billing_orders',
|
||||
$user_id, $service_id, $esc_home, $ip, $max_players, $qty, $esc_duration, $price, $esc_rcon, $esc_ftp, $now, $end_date, $esc_txid, $now
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,20 +12,22 @@ function exec_ogp_module()
|
|||
|
||||
echo "<h2>My Server Orders</h2>";
|
||||
|
||||
// Get paid but not installed orders for this user
|
||||
// Get Active (paid) but not yet provisioned orders for this user
|
||||
if ($isAdmin) {
|
||||
$orders = $db->resultQuery("SELECT o.*, s.service_name, u.users_login
|
||||
FROM OGP_DB_PREFIXbilling_orders o
|
||||
LEFT JOIN OGP_DB_PREFIXbilling_services s ON o.service_id = s.service_id
|
||||
LEFT JOIN OGP_DB_PREFIXusers u ON o.user_id = u.user_id
|
||||
WHERE o.status IN ('paid')
|
||||
WHERE o.status = 'Active'
|
||||
AND (o.home_id = '0' OR o.home_id = '')
|
||||
ORDER BY o.order_date DESC");
|
||||
} else {
|
||||
$orders = $db->resultQuery("SELECT o.*, s.service_name
|
||||
FROM OGP_DB_PREFIXbilling_orders o
|
||||
LEFT JOIN OGP_DB_PREFIXbilling_services s ON o.service_id = s.service_id
|
||||
WHERE o.user_id = ".$db->realEscapeSingle($user_id)."
|
||||
AND o.status IN ('paid')
|
||||
AND o.status = 'Active'
|
||||
AND (o.home_id = '0' OR o.home_id = '')
|
||||
ORDER BY o.order_date DESC");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,12 +42,13 @@ $query = "SELECT
|
|||
h.home_id,
|
||||
h.home_name,
|
||||
h.enabled,
|
||||
h.billing_status,
|
||||
h.server_expiration_date,
|
||||
rs.remote_server_name,
|
||||
gc.game_name,
|
||||
o.order_id,
|
||||
o.status,
|
||||
o.invoice_duration,
|
||||
-- use end_date as the expiration marker (set when order is paid/created)
|
||||
o.end_date AS expiration_date,
|
||||
bs.service_name,
|
||||
bs.price_monthly,
|
||||
|
|
@ -88,22 +89,31 @@ $result = mysqli_query($db, $query);
|
|||
<tbody>
|
||||
<?php while ($server = mysqli_fetch_assoc($result)): ?>
|
||||
<?php
|
||||
$is_active = $server['enabled'] == 1;
|
||||
$is_expired = strtotime($server['expiration_date']) < time();
|
||||
$status_class = $is_active ? 'text-success' : 'text-danger';
|
||||
$status_text = $is_active ? 'Active' : 'Inactive';
|
||||
|
||||
if ($is_expired) {
|
||||
$status_text = 'Expired';
|
||||
$status_class = 'text-danger';
|
||||
// Use billing_status from server_homes (set by migration + cron).
|
||||
// Falls back to computing from expiration date for legacy rows.
|
||||
$billing_status = $server['billing_status'] ?? null;
|
||||
if (!$billing_status) {
|
||||
$exp_ts = !empty($server['server_expiration_date']) ? strtotime($server['server_expiration_date']) : false;
|
||||
if ($exp_ts && $exp_ts < time()) {
|
||||
$billing_status = 'Expired';
|
||||
} else {
|
||||
$billing_status = 'Active';
|
||||
}
|
||||
}
|
||||
$status_class_map = [
|
||||
'Active' => 'text-success',
|
||||
'Invoiced' => 'text-warning',
|
||||
'Expired' => 'text-danger',
|
||||
];
|
||||
$status_class = $status_class_map[$billing_status] ?? 'text-secondary';
|
||||
$exp_date = $server['server_expiration_date'] ?? $server['expiration_date'];
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($server['home_name'] ?? 'Unknown'); ?></td>
|
||||
<td><?php echo htmlspecialchars($server['game_name'] ?? $server['service_name'] ?? 'Unknown'); ?></td>
|
||||
<td><?php echo htmlspecialchars($server['remote_server_name'] ?? 'Unknown'); ?></td>
|
||||
<td class="<?php echo $status_class; ?>"><?php echo $status_text; ?></td>
|
||||
<td><?php echo $server['expiration_date'] ? date('M d, Y', strtotime($server['expiration_date'])) : 'N/A'; ?></td>
|
||||
<td class="<?php echo $status_class; ?>"><?php echo htmlspecialchars($billing_status); ?></td>
|
||||
<td><?php echo $exp_date ? date('M d, Y', strtotime($exp_date)) : 'N/A'; ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$price = $server['price'] ?? $server['price_monthly'];
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ if ($db && $user_id > 0) {
|
|||
FROM {$table_prefix}billing_orders o
|
||||
LEFT JOIN {$table_prefix}billing_services s ON o.service_id = s.service_id
|
||||
WHERE o.user_id = $user_id
|
||||
AND o.status = 'paid'
|
||||
AND o.status = 'Active'
|
||||
ORDER BY o.order_date DESC
|
||||
LIMIT 10";
|
||||
|
||||
|
|
|
|||
|
|
@ -210,9 +210,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_renewal'])) {
|
|||
// Determine price based on duration (fall back to monthly if missing)
|
||||
$price = ($duration === 'year' && !empty($order['price_year']) && floatval($order['price_year']) > 0) ? floatval($order['price_year']) : floatval($order['price_monthly']);
|
||||
|
||||
// Prepare update to set this order into renew state
|
||||
// Prepare update to set this order into Invoiced state (renewal requested)
|
||||
if ($upd = $db->prepare("UPDATE {$table_prefix}billing_orders SET status = ?, invoice_duration = ?, qty = ?, price = ? WHERE order_id = ? AND user_id = ? LIMIT 1")) {
|
||||
$new_status = 'renew';
|
||||
$new_status = 'Invoiced';
|
||||
$orderIdInt = intval($order_id);
|
||||
$userIdInt = intval($user_id);
|
||||
$price_val = number_format($price, 2, '.', '');
|
||||
|
|
|
|||
260
sql/update_billing_status_active_invoiced_expired.sql
Normal file
260
sql/update_billing_status_active_invoiced_expired.sql
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
-- ============================================================
|
||||
-- GSP Billing Status Simplification Migration
|
||||
-- Simplifies server billing lifecycle to: Active | Invoiced | Expired
|
||||
--
|
||||
-- Run manually ONCE on an existing installation.
|
||||
-- Safe to re-run: every ALTER uses IF NOT EXISTS / PREPARE guards.
|
||||
-- Table prefix: gsp_ (matches modules/billing/includes/config.inc.php)
|
||||
--
|
||||
-- BACK UP YOUR DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
-- ============================================================
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 1: Fix gsp_server_homes.server_expiration_date
|
||||
-- Convert from VARCHAR(21) with 'X' default to DATETIME NULL
|
||||
-- ============================================================
|
||||
|
||||
-- Clear placeholder 'X' and empty-string values so the column
|
||||
-- can be safely converted to DATETIME.
|
||||
UPDATE `gsp_server_homes`
|
||||
SET `server_expiration_date` = NULL
|
||||
WHERE `server_expiration_date` IN ('X', '', '0', '0000-00-00 00:00:00')
|
||||
OR `server_expiration_date` IS NULL;
|
||||
|
||||
-- Convert to DATETIME only when it is still stored as VARCHAR.
|
||||
SET @col_type = '';
|
||||
SELECT DATA_TYPE INTO @col_type
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname
|
||||
AND TABLE_NAME = 'gsp_server_homes'
|
||||
AND COLUMN_NAME = 'server_expiration_date';
|
||||
|
||||
SET @sql = IF(
|
||||
@col_type = 'varchar',
|
||||
'ALTER TABLE `gsp_server_homes` MODIFY COLUMN `server_expiration_date` DATETIME NULL DEFAULT NULL',
|
||||
'SELECT "server_expiration_date already DATETIME – skipping" AS _msg'
|
||||
);
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 2: Add new billing lifecycle columns to gsp_server_homes
|
||||
-- ============================================================
|
||||
|
||||
-- billing_status
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND COLUMN_NAME = 'billing_status';
|
||||
SET @sql = IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD COLUMN `billing_status` ENUM(\'Active\',\'Invoiced\',\'Expired\') NOT NULL DEFAULT \'Active\' AFTER `server_expiration_date`',
|
||||
'SELECT "billing_status already exists" AS _msg'
|
||||
);
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
-- next_invoice_date
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND COLUMN_NAME = 'next_invoice_date';
|
||||
SET @sql = IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD COLUMN `next_invoice_date` DATETIME NULL DEFAULT NULL AFTER `billing_status`',
|
||||
'SELECT "next_invoice_date already exists" AS _msg'
|
||||
);
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
-- last_invoice_id
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND COLUMN_NAME = 'last_invoice_id';
|
||||
SET @sql = IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD COLUMN `last_invoice_id` INT NULL DEFAULT NULL AFTER `next_invoice_date`',
|
||||
'SELECT "last_invoice_id already exists" AS _msg'
|
||||
);
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
-- billing_enabled
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND COLUMN_NAME = 'billing_enabled';
|
||||
SET @sql = IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD COLUMN `billing_enabled` TINYINT(1) NOT NULL DEFAULT 1 AFTER `last_invoice_id`',
|
||||
'SELECT "billing_enabled already exists" AS _msg'
|
||||
);
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 3: Create gsp_invoices (post-purchase renewal invoices)
|
||||
-- Distinct from gsp_billing_invoices (pre-purchase cart)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gsp_invoices` (
|
||||
`invoice_id` INT NOT NULL AUTO_INCREMENT,
|
||||
`home_id` INT NOT NULL,
|
||||
`user_id` INT NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`due_date` DATETIME NOT NULL,
|
||||
`paid_at` DATETIME NULL DEFAULT NULL,
|
||||
`billing_status` ENUM('Invoiced','Active','Expired')
|
||||
NOT NULL DEFAULT 'Invoiced',
|
||||
`rate_type` ENUM('daily','monthly','yearly')
|
||||
NOT NULL DEFAULT 'monthly',
|
||||
`price_per_player` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`player_slots` INT NOT NULL DEFAULT 0,
|
||||
`quantity` INT NOT NULL DEFAULT 1,
|
||||
`subtotal` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`total_due` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`payment_method` VARCHAR(64) NOT NULL DEFAULT 'PayPal',
|
||||
`payment_id` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`notes` TEXT NULL,
|
||||
PRIMARY KEY (`invoice_id`),
|
||||
KEY `idx_home_id` (`home_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_billing_status`(`billing_status`),
|
||||
KEY `idx_due_date` (`due_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 4: Create gsp_billing_config (per-game or global rates)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gsp_billing_config` (
|
||||
`config_id` INT NOT NULL AUTO_INCREMENT,
|
||||
`game_key` VARCHAR(128) NULL DEFAULT NULL COMMENT 'NULL = global default',
|
||||
`rate_type` ENUM('daily','monthly','yearly')
|
||||
NOT NULL DEFAULT 'monthly',
|
||||
`price_per_player` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`grace_days` INT NOT NULL DEFAULT 0,
|
||||
`delete_after_expired_days`INT NOT NULL DEFAULT 7,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`config_id`),
|
||||
KEY `idx_game_key` (`game_key`),
|
||||
KEY `idx_enabled` (`enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Insert global default config if none exists
|
||||
INSERT INTO `gsp_billing_config`
|
||||
(`game_key`, `rate_type`, `price_per_player`, `grace_days`, `delete_after_expired_days`, `enabled`)
|
||||
SELECT NULL, 'monthly', 0.00, 0, 7, 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `gsp_billing_config` WHERE `game_key` IS NULL LIMIT 1
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 5: Populate server_homes.billing_status from existing
|
||||
-- gsp_billing_orders data
|
||||
-- Priority: Expired > Invoiced > Active
|
||||
-- ============================================================
|
||||
|
||||
-- Active: paid, installed, active, running, enabled, online
|
||||
UPDATE `gsp_server_homes` sh
|
||||
INNER JOIN `gsp_billing_orders` bo
|
||||
ON bo.home_id = sh.home_id
|
||||
AND CAST(bo.home_id AS UNSIGNED) > 0
|
||||
SET sh.`billing_status` = 'Active',
|
||||
sh.`server_expiration_date` = bo.`end_date`,
|
||||
sh.`next_invoice_date` = bo.`end_date`
|
||||
WHERE bo.`status` IN ('paid', 'installed', 'active', 'running', 'enabled', 'online');
|
||||
|
||||
-- Invoiced: renew, unpaid, pending, overdue, invoice, invoiced, in-cart
|
||||
UPDATE `gsp_server_homes` sh
|
||||
INNER JOIN `gsp_billing_orders` bo
|
||||
ON bo.home_id = sh.home_id
|
||||
AND CAST(bo.home_id AS UNSIGNED) > 0
|
||||
SET sh.`billing_status` = 'Invoiced',
|
||||
sh.`server_expiration_date` = bo.`end_date`,
|
||||
sh.`next_invoice_date` = bo.`end_date`
|
||||
WHERE bo.`status` IN ('renew', 'unpaid', 'pending', 'overdue', 'invoice', 'invoiced', 'in-cart');
|
||||
|
||||
-- Expired: expired, cancelled, terminated, suspended, deleted
|
||||
UPDATE `gsp_server_homes` sh
|
||||
INNER JOIN `gsp_billing_orders` bo
|
||||
ON bo.home_id = sh.home_id
|
||||
AND CAST(bo.home_id AS UNSIGNED) > 0
|
||||
SET sh.`billing_status` = 'Expired',
|
||||
sh.`server_expiration_date` = bo.`end_date`
|
||||
WHERE bo.`status` IN ('expired', 'cancelled', 'terminated', 'suspended', 'deleted');
|
||||
|
||||
-- Backfill server_expiration_date from billing_orders where still NULL
|
||||
UPDATE `gsp_server_homes` sh
|
||||
INNER JOIN `gsp_billing_orders` bo
|
||||
ON bo.home_id = sh.home_id
|
||||
AND CAST(bo.home_id AS UNSIGNED) > 0
|
||||
SET sh.`server_expiration_date` = bo.`end_date`
|
||||
WHERE sh.`server_expiration_date` IS NULL
|
||||
AND bo.`end_date` IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 6: Normalize gsp_billing_orders.status to new values
|
||||
-- ============================================================
|
||||
|
||||
-- Active (was: paid, installed, active, running, enabled, online)
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Active'
|
||||
WHERE `status` IN ('paid', 'installed', 'active', 'running', 'enabled', 'online');
|
||||
|
||||
-- Invoiced (was: renew, unpaid, pending, overdue, invoice, invoiced, in-cart)
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Invoiced'
|
||||
WHERE `status` IN ('renew', 'unpaid', 'pending', 'overdue', 'invoice', 'invoiced', 'in-cart');
|
||||
|
||||
-- Expired (was: expired, cancelled, terminated, suspended, deleted)
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Expired'
|
||||
WHERE `status` IN ('expired', 'cancelled', 'terminated', 'suspended', 'deleted');
|
||||
|
||||
-- ============================================================
|
||||
-- SECTION 7: Add indexes to gsp_server_homes for billing queries
|
||||
-- ============================================================
|
||||
|
||||
SET @idx = 0;
|
||||
SELECT COUNT(*) INTO @idx FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND INDEX_NAME = 'idx_billing_status';
|
||||
SET @sql = IF(@idx = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD INDEX `idx_billing_status` (`billing_status`)',
|
||||
'SELECT "idx_billing_status exists" AS _msg');
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
SET @idx = 0;
|
||||
SELECT COUNT(*) INTO @idx FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND INDEX_NAME = 'idx_server_expiration_date';
|
||||
SET @sql = IF(@idx = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD INDEX `idx_server_expiration_date` (`server_expiration_date`)',
|
||||
'SELECT "idx_server_expiration_date exists" AS _msg');
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
SET @idx = 0;
|
||||
SELECT COUNT(*) INTO @idx FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND INDEX_NAME = 'idx_next_invoice_date';
|
||||
SET @sql = IF(@idx = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD INDEX `idx_next_invoice_date` (`next_invoice_date`)',
|
||||
'SELECT "idx_next_invoice_date exists" AS _msg');
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
SET @idx = 0;
|
||||
SELECT COUNT(*) INTO @idx FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'gsp_server_homes' AND INDEX_NAME = 'idx_billing_enabled';
|
||||
SET @sql = IF(@idx = 0,
|
||||
'ALTER TABLE `gsp_server_homes` ADD INDEX `idx_billing_enabled` (`billing_enabled`)',
|
||||
'SELECT "idx_billing_enabled exists" AS _msg');
|
||||
PREPARE _stmt FROM @sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
|
||||
|
||||
-- ============================================================
|
||||
-- DONE
|
||||
-- ============================================================
|
||||
|
||||
SELECT CONCAT(
|
||||
'Migration complete. ',
|
||||
'gsp_server_homes now has billing_status/next_invoice_date/last_invoice_id/billing_enabled. ',
|
||||
'gsp_invoices and gsp_billing_config tables created. ',
|
||||
'gsp_billing_orders.status normalized to Active/Invoiced/Expired.'
|
||||
) AS Migration_Result;
|
||||
Loading…
Add table
Add a link
Reference in a new issue