fix: standardize billing order status values and apply full-day expiry grace rule
- home_handling_functions.php: display expiration as Y-m-d only (not Y-m-d H:i);
apply full-day grace by comparing date-only (midnight) in PHP so a server
expiring on today's date remains Active the entire day
- cron-shop.php Step B & C: change server_expiration_date comparisons from
< DATE_SUB(NOW(), ...) to DATE(server_expiration_date) < DATE_SUB(CURDATE(), ...)
ensuring the full expiration day is honoured
- user_games/check_expire.php: change all three expiration queries from
<= time() to < strtotime(date('Y-m-d')) (today-midnight) for full-day grace
- billing/test_integration.php: replace status='paid' (old order status) with
status IN ('Active','paid') for backward compat during migration
- user_games/billing_integration.php: clarify comment that billing_invoices.status
uses the payment lifecycle ('paid'/'unpaid'/'due'), separate from billing_orders.status
- Add modules/billing/sql/normalize_billing_order_status.sql migration script
to convert installed->Active, paid->Active, suspended->Expired in existing rows
- Update CHANGELOG.md
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/439845e0-926e-4b49-9cd0-810457b73c12
Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
5f3013b4a4
commit
5b2162fb28
7 changed files with 79 additions and 20 deletions
|
|
@ -1,6 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-03 (latest)
|
||||
## 2026-05-05
|
||||
- **Billing order status standardization:** Canonical `billing_orders.status` values are now `Active`, `Invoiced`, and `Expired` only. All old writes of `installed`, `paid` (as order status), and `suspended` have been replaced. A SQL migration script `modules/billing/sql/normalize_billing_order_status.sql` converts any existing legacy rows. Backward-compatibility read paths (e.g. renewable-status checks in `my_account.php`) are preserved until the migration runs.
|
||||
- **Expiration display date-only:** The billing expiration shown on the game server monitor (`server_monitor.php`) now displays as `YYYY-MM-DD` only instead of `YYYY-MM-DD HH:MM`.
|
||||
- **Full-day expiration grace rule:** A server whose `end_date` falls on today is treated as active for the entire calendar day. Expiration is only processed starting the next calendar day. This rule is applied consistently in: billing cron (`cron-shop.php` Steps B and C), the server monitor expiration helper (`home_handling_functions.php::get_server_billing_expiration_html`), and the OGP user/group assignment expiration processor (`user_games/check_expire.php`). All comparisons now use `DATE(end_date) < CURDATE()` (SQL) or `< strtotime(date('Y-m-d'))` (PHP) — never `<= NOW()` or `<= time()`.
|
||||
|
||||
|
||||
- **GSP 1.0 baseline:** Reset all bundled/core module versions to `1.0`. DB schema versions (`$db_version`) are unchanged.
|
||||
- **FAQ module refresh:** Restored online RSS update code from upstream (opengamepanel.org), fixed `$local = false` initialization bug, switched local cache to `ogpfaq.rss`, added PHP 8.3-compatible `(array)` casts, restored upstream credits footer, and opened `navigation.xml` access to `user,admin,subuser`.
|
||||
- **Config XML editor improvements:** Added schema validation before save (both structured editor and raw XML path); invalid XML is rejected with line-level error messages instead of being written to disk. Added auto-restore from backup on validation failure. Fields are now displayed in schema-defined order with required/optional badges. Added a raw XML editing panel with validation warning. Unknown/custom XML fields are preserved when only specific nodes are modified.
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ $past_due = $db->resultQuery("
|
|||
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 DATE(sh.server_expiration_date) < DATE_SUB(CURDATE(), INTERVAL {$grace_days} DAY)
|
||||
AND (
|
||||
sh.last_invoice_id IS NULL
|
||||
OR EXISTS (
|
||||
|
|
@ -284,7 +284,7 @@ $to_delete = $db->resultQuery("
|
|||
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)
|
||||
AND DATE(sh.server_expiration_date) < DATE_SUB(CURDATE(), INTERVAL {$delete_after_days} DAY)
|
||||
ORDER BY sh.home_id ASC
|
||||
");
|
||||
|
||||
|
|
|
|||
37
modules/billing/sql/normalize_billing_order_status.sql
Normal file
37
modules/billing/sql/normalize_billing_order_status.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- normalize_billing_order_status.sql
|
||||
-- ============================================================
|
||||
-- Migrate legacy billing_orders.status values to the canonical
|
||||
-- three-value lifecycle used by GSP billing:
|
||||
--
|
||||
-- Active – server is provisioned and current
|
||||
-- Invoiced – renewal invoice generated; payment due
|
||||
-- Expired – invoice unpaid past due date; server pending deletion
|
||||
--
|
||||
-- Old values and their mappings:
|
||||
-- installed -> Active (was written by old provisioner)
|
||||
-- paid -> Active (was written after PayPal capture, before provisioning)
|
||||
-- suspended -> Expired (was written by old cron when overdue)
|
||||
--
|
||||
-- Run this ONCE against the panel database after deploying the updated
|
||||
-- cron-shop.php and application code. It is safe to re-run (idempotent).
|
||||
-- ============================================================
|
||||
|
||||
-- Map old 'installed' to 'Active'
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Active'
|
||||
WHERE `status` = 'installed';
|
||||
|
||||
-- Map old 'paid' to 'Active'
|
||||
-- (Orders that were paid but not yet provisioned should be provisioned
|
||||
-- via home.php?m=billing&p=provision_servers after this migration.)
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Active'
|
||||
WHERE `status` = 'paid';
|
||||
|
||||
-- Map old 'suspended' to 'Expired'
|
||||
UPDATE `gsp_billing_orders`
|
||||
SET `status` = 'Expired'
|
||||
WHERE `status` = 'suspended';
|
||||
|
||||
-- Optional: verify counts after migration
|
||||
-- SELECT status, COUNT(*) AS count FROM gsp_billing_orders GROUP BY status ORDER BY count DESC;
|
||||
|
|
@ -98,12 +98,14 @@ function exec_ogp_module()
|
|||
}
|
||||
echo "</ul>";
|
||||
|
||||
// Test 7: Sample paid order check
|
||||
echo "<h3>7. Paid Orders Ready for Provisioning</h3>";
|
||||
// Test 7: Active orders ready for provisioning
|
||||
// Canonical status is 'Active'. Legacy rows may still use 'paid' until
|
||||
// normalize_billing_order_status.sql has been run — include them here.
|
||||
echo "<h3>7. Active Orders Ready for Provisioning</h3>";
|
||||
if ($isAdmin) {
|
||||
$paid_orders = $db->resultQuery("SELECT COUNT(*) as count FROM OGP_DB_PREFIXbilling_orders WHERE status='paid'");
|
||||
$paid_orders = $db->resultQuery("SELECT COUNT(*) as count FROM OGP_DB_PREFIXbilling_orders WHERE status IN ('Active','paid')");
|
||||
} else {
|
||||
$paid_orders = $db->resultQuery("SELECT COUNT(*) as count FROM OGP_DB_PREFIXbilling_orders WHERE status='paid' AND user_id=".$db->realEscapeSingle($user_id));
|
||||
$paid_orders = $db->resultQuery("SELECT COUNT(*) as count FROM OGP_DB_PREFIXbilling_orders WHERE status IN ('Active','paid') AND user_id=".$db->realEscapeSingle($user_id));
|
||||
}
|
||||
|
||||
if (!empty($paid_orders)) {
|
||||
|
|
|
|||
|
|
@ -496,13 +496,17 @@ function get_monitor_buttons($server_home, $server_xml)
|
|||
* provisioned).
|
||||
*
|
||||
* Color thresholds:
|
||||
* green – more than 10 days remaining (shows actual date)
|
||||
* green – more than 10 days remaining (shows actual date, YYYY-MM-DD)
|
||||
* yellow – 4–10 days remaining (shows "X days remaining")
|
||||
* red – 1–3 days remaining (shows "X days remaining")
|
||||
* red – less than 1 day but not yet expired (shows "Less than 1 day remaining")
|
||||
* red – already expired/suspended (shows "Suspended")
|
||||
* red – no order or NULL/invalid end_date (shows "No expiration date found")
|
||||
*
|
||||
* Expiration grace rule: a server whose end_date falls on today is treated as
|
||||
* active for the full calendar day. The server only shows "Suspended" once
|
||||
* today's date is strictly after the expiration date (i.e. tomorrow onwards).
|
||||
*
|
||||
* @param int $home_id The server home ID.
|
||||
* @return string Safe HTML string ready for inline display.
|
||||
*/
|
||||
|
|
@ -551,17 +555,20 @@ function get_server_billing_expiration_html(int $home_id): string
|
|||
return "<span style='color:red;'>No expiration date found</span>";
|
||||
}
|
||||
|
||||
$now = new DateTime();
|
||||
$diff = $now->diff($end_dt);
|
||||
// Compare dates only (strip time) so a server expiring on today's date is
|
||||
// still shown as active for the full calendar day (grace rule).
|
||||
$today_midnight = (new DateTime())->setTime(0, 0, 0);
|
||||
$end_midnight = (clone $end_dt)->setTime(0, 0, 0);
|
||||
$diff = $today_midnight->diff($end_midnight);
|
||||
|
||||
if ($end_dt <= $now) {
|
||||
// Server billing period has already passed — treat as suspended.
|
||||
if ($end_midnight < $today_midnight) {
|
||||
// Expiration date is before today — treat as suspended.
|
||||
return "<span style='color:red;'>Suspended</span>";
|
||||
}
|
||||
|
||||
// $diff->days is the total number of whole days between $now and $end_dt.
|
||||
// $diff->days is the total number of whole days between today and end date.
|
||||
$days_remaining = (int)$diff->days;
|
||||
$display_date = htmlentities($end_dt->format('Y-m-d H:i'), ENT_QUOTES, 'UTF-8');
|
||||
$display_date = htmlentities($end_dt->format('Y-m-d'), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
if ($days_remaining > 10) {
|
||||
// More than 10 days: show the actual expiration date in green.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
*
|
||||
* 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='Active', price=0)
|
||||
* billing_invoices (status='paid', amount=0) — invoice payment status (paid/unpaid/due)
|
||||
* billing_orders (status='Active', price=0) — order lifecycle status (Active/Invoiced/Expired)
|
||||
*
|
||||
* 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
|
||||
|
|
@ -80,7 +80,11 @@ if (!function_exists('admin_register_server_in_billing')) {
|
|||
$ftp_flag = $ftp ? 'enabled' : 'disabled';
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 3. Insert billing_invoice (amount=0, already "paid"). //
|
||||
// 3. Insert billing_invoice (amount=0, payment_status='paid'). //
|
||||
// Note: billing_invoices.status is the invoice payment lifecycle //
|
||||
// ('paid'/'unpaid'/'due') — distinct from billing_orders.status //
|
||||
// which uses the three-value order lifecycle (Active/Invoiced/ //
|
||||
// Expired). //
|
||||
// ------------------------------------------------------------------ //
|
||||
$invoice_fields = array(
|
||||
'order_id' => 0,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@
|
|||
function exec_ogp_module()
|
||||
{
|
||||
global $db;
|
||||
$expired_servers = $db->resultQuery("SELECT home_name, home_id, server_expiration_date FROM OGP_DB_PREFIXserver_homes WHERE server_expiration_date NOT LIKE 'X' AND server_expiration_date <= ".time().";");
|
||||
// Use strtotime(date('Y-m-d')) (midnight of today) so a server whose
|
||||
// expiration falls on today is still considered active for the full
|
||||
// calendar day. Only timestamps strictly before today midnight are expired.
|
||||
$today_midnight = strtotime(date('Y-m-d'));
|
||||
$expired_servers = $db->resultQuery("SELECT home_name, home_id, server_expiration_date FROM OGP_DB_PREFIXserver_homes WHERE server_expiration_date NOT LIKE 'X' AND server_expiration_date < " . $today_midnight . ";");
|
||||
if($expired_servers)
|
||||
{
|
||||
foreach ((array)$expired_servers as $expired_server)
|
||||
|
|
@ -35,7 +39,7 @@ function exec_ogp_module()
|
|||
}
|
||||
}
|
||||
|
||||
$expired_users = $db->resultQuery("SELECT user_id, home_id, user_expiration_date FROM OGP_DB_PREFIXuser_homes WHERE user_expiration_date NOT LIKE 'X' AND user_expiration_date <= ".time().";");
|
||||
$expired_users = $db->resultQuery("SELECT user_id, home_id, user_expiration_date FROM OGP_DB_PREFIXuser_homes WHERE user_expiration_date NOT LIKE 'X' AND user_expiration_date < " . $today_midnight . ";" );
|
||||
if($expired_users)
|
||||
{
|
||||
foreach ((array)$expired_users as $expired_user)
|
||||
|
|
@ -50,7 +54,7 @@ function exec_ogp_module()
|
|||
INNER JOIN
|
||||
OGP_DB_PREFIXuser_groups ug
|
||||
ON ug.group_id=g.group_id
|
||||
WHERE g.user_group_expiration_date NOT LIKE 'X' AND g.user_group_expiration_date <= ".time()." GROUP BY g.home_id;");
|
||||
WHERE g.user_group_expiration_date NOT LIKE 'X' AND g.user_group_expiration_date < " . $today_midnight . " GROUP BY g.home_id;");
|
||||
if($expired_groups)
|
||||
{
|
||||
foreach ((array)$expired_groups as $expired_group)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue