diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e873c9..dac065d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/modules/billing/cron-shop.php b/modules/billing/cron-shop.php index 72d82707..9884944d 100644 --- a/modules/billing/cron-shop.php +++ b/modules/billing/cron-shop.php @@ -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 "); diff --git a/modules/billing/sql/normalize_billing_order_status.sql b/modules/billing/sql/normalize_billing_order_status.sql new file mode 100644 index 00000000..bda6d6cf --- /dev/null +++ b/modules/billing/sql/normalize_billing_order_status.sql @@ -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 the admin orders panel 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; diff --git a/modules/billing/test_integration.php b/modules/billing/test_integration.php index 5617c6ac..e1463427 100644 --- a/modules/billing/test_integration.php +++ b/modules/billing/test_integration.php @@ -98,12 +98,14 @@ function exec_ogp_module() } echo ""; - // Test 7: Sample paid order check - echo "

7. Paid Orders Ready for Provisioning

"; + // 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 "

7. Active Orders Ready for Provisioning

"; 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)) { diff --git a/modules/gamemanager/home_handling_functions.php b/modules/gamemanager/home_handling_functions.php index d968971d..8196001c 100644 --- a/modules/gamemanager/home_handling_functions.php +++ b/modules/gamemanager/home_handling_functions.php @@ -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 "No expiration date found"; } - $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 "Suspended"; } - // $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. diff --git a/modules/user_games/billing_integration.php b/modules/user_games/billing_integration.php index bc931b95..08b2cb2b 100644 --- a/modules/user_games/billing_integration.php +++ b/modules/user_games/billing_integration.php @@ -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, diff --git a/modules/user_games/check_expire.php b/modules/user_games/check_expire.php index 9cf7b255..4e32ab38 100644 --- a/modules/user_games/check_expire.php +++ b/modules/user_games/check_expire.php @@ -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)