Merge pull request #123 from GameServerPanel/copilot/fix-billing-order-expiration-logic
This commit is contained in:
commit
5f3013b4a4
5 changed files with 119 additions and 14 deletions
|
|
@ -197,5 +197,45 @@ function exec_ogp_module()
|
|||
|
||||
echo "</tbody></table>";
|
||||
echo "</div>";
|
||||
|
||||
// Orphaned home_id diagnostics —————————————————————————————————————————
|
||||
// Find billing_orders rows where home_id != 0 but no matching gsp_server_homes
|
||||
// record exists. These indicate provisioning failures or stale data, and they
|
||||
// are the reason the game monitor may show "No expiration date found".
|
||||
$orphans = $db->resultQuery(
|
||||
"SELECT o.order_id, o.user_id, o.home_name, o.home_id, o.status, o.end_date
|
||||
FROM OGP_DB_PREFIXbilling_orders o
|
||||
LEFT JOIN OGP_DB_PREFIXserver_homes sh ON sh.home_id = o.home_id
|
||||
WHERE o.home_id != '0'
|
||||
AND o.home_id != ''
|
||||
AND sh.home_id IS NULL
|
||||
ORDER BY o.order_id ASC"
|
||||
);
|
||||
|
||||
echo "<div style='margin-top: 30px;'>";
|
||||
echo "<h3>Orphaned home_id Diagnostics</h3>";
|
||||
echo "<p style='color:#666;'>Billing orders that reference a <code>home_id</code> which no longer exists in <code>gsp_server_homes</code>. ";
|
||||
echo "These orders will not show an expiration date on the game monitor. ";
|
||||
echo "Reset <code>home_id</code> to <code>0</code> or re-provision these orders to fix them. ";
|
||||
echo "Run <code>normalize_billing_order_status.sql</code> to standardize any legacy status values.</p>";
|
||||
|
||||
if (empty($orphans)) {
|
||||
echo "<p style='color:green;'>✓ No orphaned billing orders found.</p>";
|
||||
} else {
|
||||
echo "<table class='tablesorter' style='width:100%;'>";
|
||||
echo "<thead><tr><th>Order ID</th><th>User ID</th><th>Server Name</th><th>home_id (missing)</th><th>Status</th><th>End Date</th></tr></thead><tbody>";
|
||||
foreach ($orphans as $row) {
|
||||
echo "<tr>";
|
||||
echo "<td>".intval($row['order_id'])."</td>";
|
||||
echo "<td>".intval($row['user_id'])."</td>";
|
||||
echo "<td>".htmlspecialchars($row['home_name'] ?? '')."</td>";
|
||||
echo "<td style='color:red;'>".htmlspecialchars($row['home_id'] ?? '')."</td>";
|
||||
echo "<td>".htmlspecialchars($row['status'] ?? '')."</td>";
|
||||
echo "<td>".htmlspecialchars($row['end_date'] ?? 'NULL')."</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
echo "</tbody></table>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ $status_config = [
|
|||
'paid' => ['label' => 'Paid Invoices', 'class' => 'paid'],
|
||||
'completed' => ['label' => 'Completed Invoices', 'class' => 'paid'],
|
||||
'in-cart' => ['label' => 'In Cart', 'class' => 'pending'],
|
||||
'installed' => ['label' => 'Installed/Active', 'class' => 'paid'],
|
||||
'installed' => ['label' => 'Active', 'class' => 'paid'],
|
||||
'expired' => ['label' => 'Expired Invoices', 'class' => 'expired'],
|
||||
'cancelled' => ['label' => 'Cancelled Invoices', 'class' => 'expired'],
|
||||
];
|
||||
|
|
@ -338,7 +338,10 @@ $status_config = [
|
|||
<div class="server-actions">
|
||||
<?php
|
||||
// Show Renew action for servers that can be renewed
|
||||
$renewable_statuses = array('paid','installed','invoiced','suspended');
|
||||
// Status comparison is case-insensitive (strtolower). Canonical
|
||||
// values are 'Active' and 'Invoiced'; legacy values are included
|
||||
// as a fallback until normalize_billing_order_status.sql has run.
|
||||
$renewable_statuses = array('active','invoiced','paid','installed','suspended');
|
||||
if (!empty($server['status']) && in_array(strtolower($server['status']), $renewable_statuses)): ?>
|
||||
<a href="renew_server.php?order_id=<?php echo intval($server['order_id']); ?>" class="gsw-btn renew-btn">Renew</a>
|
||||
<?php endif; ?>
|
||||
|
|
|
|||
64
modules/billing/normalize_billing_order_status.sql
Normal file
64
modules/billing/normalize_billing_order_status.sql
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
-- normalize_billing_order_status.sql
|
||||
--
|
||||
-- One-time migration: standardize gsp_billing_orders.status to the canonical
|
||||
-- three-value set used by cron-shop.php, create_servers.php, and the game
|
||||
-- monitor expiration lookup:
|
||||
--
|
||||
-- Active – server provisioned and billing current
|
||||
-- Invoiced – renewal invoice open; service still running
|
||||
-- Expired – invoice unpaid past grace period; server suspended/awaiting deletion
|
||||
--
|
||||
-- Legacy → canonical mapping applied by this script:
|
||||
-- 'installed' → 'Active' (provisioned via old invoice-first flow)
|
||||
-- 'paid' → 'Active' (payment captured but before explicit provisioning step)
|
||||
-- 'suspended' → 'Invoiced' (overdue; renewal invoice was open — maps to Invoiced
|
||||
-- so cron Step B will expire them on the next run if
|
||||
-- still unpaid, rather than silently treating them as Active)
|
||||
--
|
||||
-- All other statuses ('in-cart', 'cancelled', 'refunded', 'Active', 'Invoiced',
|
||||
-- 'Expired') are left unchanged.
|
||||
--
|
||||
-- Compatible with MySQL 5.7+ and MariaDB 10.2+.
|
||||
-- Table prefix is hardcoded to gsp_ (standalone billing module context).
|
||||
-- Run ONCE on an existing installation; safe to run again (no-op on clean data).
|
||||
|
||||
-- 'installed' → 'Active'
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Active'
|
||||
WHERE `status` = 'installed';
|
||||
|
||||
-- 'paid' → 'Active'
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Active'
|
||||
WHERE `status` = 'paid';
|
||||
|
||||
-- 'suspended' → 'Invoiced'
|
||||
-- These rows had an open renewal invoice; cron-shop Step B will move them to
|
||||
-- 'Expired' on the next run if the invoice remains unpaid.
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Invoiced'
|
||||
WHERE `status` = 'suspended';
|
||||
|
||||
-- Diagnostic: show any remaining non-canonical status values after migration.
|
||||
-- Expected result: only rows with status IN ('Active','Invoiced','Expired',
|
||||
-- 'in-cart','cancelled','refunded') should appear.
|
||||
SELECT `status`, COUNT(*) AS `count`
|
||||
FROM `gsp_billing_orders`
|
||||
GROUP BY `status`
|
||||
ORDER BY `status`;
|
||||
|
||||
-- Diagnostic: billing_orders whose home_id references a non-existent server home.
|
||||
-- These orders will show "No expiration date found" on the game monitor until
|
||||
-- home_id is corrected (set to the real home_id or to 0 if the server is gone).
|
||||
SELECT o.`order_id`,
|
||||
o.`user_id`,
|
||||
o.`home_name`,
|
||||
o.`home_id` AS missing_home_id,
|
||||
o.`status`,
|
||||
o.`end_date`
|
||||
FROM `gsp_billing_orders` o
|
||||
LEFT JOIN `gsp_server_homes` sh ON sh.`home_id` = o.`home_id`
|
||||
WHERE o.`home_id` != '0'
|
||||
AND o.`home_id` != ''
|
||||
AND sh.`home_id` IS NULL
|
||||
ORDER BY o.`order_id`;
|
||||
|
|
@ -514,15 +514,13 @@ function get_server_billing_expiration_html(int $home_id): string
|
|||
// billing_orders.home_id is VARCHAR(255), so we quote the value in the query.
|
||||
// We exclude rows where home_id = '0' (not yet provisioned).
|
||||
//
|
||||
// Status whitelist — covers all active/paid states regardless of billing flow:
|
||||
// 'Active' – set by payment_processor.php after PayPal capture
|
||||
// 'Invoiced' – renewal invoice generated by cron-shop.php
|
||||
// 'installed'– used by the invoice-first provisioning flow
|
||||
// 'paid' – used by the invoice-first provisioning flow
|
||||
// 'suspended'– overdue but not yet fully expired; date still meaningful
|
||||
// Only canonical active statuses are considered:
|
||||
// 'Active' – provisioned and current (set by payment capture or admin creation)
|
||||
// 'Invoiced' – renewal invoice generated; service still running while unpaid
|
||||
//
|
||||
// Terminal statuses ('Expired', 'in-cart', 'cancelled', 'refunded') are excluded
|
||||
// because they represent orders with no active service period.
|
||||
// Terminal statuses ('Expired', 'in-cart', 'cancelled', 'refunded') and legacy
|
||||
// statuses ('installed', 'paid', 'suspended') are not matched here.
|
||||
// Run normalize_billing_order_status.sql to migrate any legacy rows first.
|
||||
//
|
||||
// OGP_DB_PREFIX is replaced at runtime by the panel DB wrapper (str_replace).
|
||||
$rows = $db->resultQuery(
|
||||
|
|
@ -530,7 +528,7 @@ function get_server_billing_expiration_html(int $home_id): string
|
|||
FROM OGP_DB_PREFIXbilling_orders
|
||||
WHERE home_id = '" . intval($home_id) . "'
|
||||
AND home_id != '0'
|
||||
AND status IN ('Active','Invoiced','installed','paid','suspended')
|
||||
AND status IN ('Active','Invoiced')
|
||||
ORDER BY end_date DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Shared helper for recording admin-created game servers in the billing tables,
|
||||
* so they are treated identically to FREE website orders:
|
||||
* billing_invoices (status='paid', amount=0)
|
||||
* billing_orders (status='installed', price=0)
|
||||
* billing_orders (status='Active', price=0)
|
||||
*
|
||||
* This does NOT re-provision the server — the caller (add_home.php) already
|
||||
* created the server via the panel DB layer. We only write the billing ledger
|
||||
|
|
@ -113,7 +113,7 @@ if (!function_exists('admin_register_server_in_billing')) {
|
|||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 4. Insert billing_order (status='installed', already provisioned). //
|
||||
// 4. Insert billing_order (status='Active', already provisioned). //
|
||||
// ------------------------------------------------------------------ //
|
||||
$order_fields = array(
|
||||
'user_id' => intval($user_id),
|
||||
|
|
@ -128,7 +128,7 @@ if (!function_exists('admin_register_server_in_billing')) {
|
|||
'remote_control_password' => '',
|
||||
'ftp_password' => '',
|
||||
'home_id' => intval($home_id),
|
||||
'status' => 'installed',
|
||||
'status' => 'Active',
|
||||
'order_date' => $now,
|
||||
'end_date' => $end_date,
|
||||
'payment_txid' => 'admin-created',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue