From 2f62bd32c95b134d50030ce05bf3b6f8e783556f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 May 2026 15:55:39 +0000
Subject: [PATCH] fix: standardize billing order status values and fix
expiration lookup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- billing_integration.php: admin-created servers now use status='Active' (was 'installed')
- home_handling_functions.php: expiration query uses status IN ('Active','Invoiced') only
- my_account.php: renewable_statuses includes canonical 'active'/'invoiced'; legacy labels updated
- admin_orders.php: add orphaned home_id diagnostics section
- normalize_billing_order_status.sql: new migration to convert installed/paid→Active, suspended→Invoiced
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/c56f04bb-ecce-4f1b-9bbd-c5f83107da1d
Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
---
modules/billing/admin_orders.php | 40 ++++++++++++
modules/billing/my_account.php | 4 +-
.../normalize_billing_order_status.sql | 64 +++++++++++++++++++
.../gamemanager/home_handling_functions.php | 16 ++---
modules/user_games/billing_integration.php | 6 +-
5 files changed, 116 insertions(+), 14 deletions(-)
create mode 100644 modules/billing/normalize_billing_order_status.sql
diff --git a/modules/billing/admin_orders.php b/modules/billing/admin_orders.php
index c502565f..e726b4ff 100644
--- a/modules/billing/admin_orders.php
+++ b/modules/billing/admin_orders.php
@@ -197,5 +197,45 @@ function exec_ogp_module()
echo "";
echo "";
+
+ // 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 "
";
+ echo "
Orphaned home_id Diagnostics
";
+ echo "
Billing orders that reference a home_id which no longer exists in gsp_server_homes. ";
+ echo "These orders will not show an expiration date on the game monitor. ";
+ echo "Reset home_id to 0 or re-provision these orders to fix them. ";
+ echo "Run normalize_billing_order_status.sql to standardise any legacy status values.
";
+
+ if (empty($orphans)) {
+ echo "
✓ No orphaned billing orders found.
";
+ } else {
+ echo "
";
+ echo "| Order ID | User ID | Server Name | home_id (missing) | Status | End Date |
";
+ foreach ($orphans as $row) {
+ echo "";
+ echo "| ".intval($row['order_id'])." | ";
+ echo "".intval($row['user_id'])." | ";
+ echo "".htmlspecialchars($row['home_name'] ?? '')." | ";
+ echo "".htmlspecialchars($row['home_id'] ?? '')." | ";
+ echo "".htmlspecialchars($row['status'] ?? '')." | ";
+ echo "".htmlspecialchars($row['end_date'] ?? 'NULL')." | ";
+ echo "
";
+ }
+ echo "
";
+ }
+ echo "
";
}
?>
diff --git a/modules/billing/my_account.php b/modules/billing/my_account.php
index 6d1ac48b..5c670726 100644
--- a/modules/billing/my_account.php
+++ b/modules/billing/my_account.php
@@ -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,7 @@ $status_config = [
Renew
diff --git a/modules/billing/normalize_billing_order_status.sql b/modules/billing/normalize_billing_order_status.sql
new file mode 100644
index 00000000..b366e58f
--- /dev/null
+++ b/modules/billing/normalize_billing_order_status.sql
@@ -0,0 +1,64 @@
+-- normalize_billing_order_status.sql
+--
+-- One-time migration: standardise 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`;
diff --git a/modules/gamemanager/home_handling_functions.php b/modules/gamemanager/home_handling_functions.php
index fd58c309..d968971d 100644
--- a/modules/gamemanager/home_handling_functions.php
+++ b/modules/gamemanager/home_handling_functions.php
@@ -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"
);
diff --git a/modules/user_games/billing_integration.php b/modules/user_games/billing_integration.php
index 48ba2b8b..bc931b95 100644
--- a/modules/user_games/billing_integration.php
+++ b/modules/user_games/billing_integration.php
@@ -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',