-
+
- $
+ $
-
+
isset($row['home_id']) ? (string)$row['home_id'] : ('order'.$row['order_id']),
+ 'serverID' => 'invoice-' . $row['invoice_id'],
'amount' => number_format($rowtotal, 2, '.', ''),
- 'order_id' => intval($row['order_id'])
+ 'invoice_id' => intval($row['invoice_id'])
];
?>
@@ -428,12 +425,14 @@ if (is_array($invoice) && count($invoice) === 1 && !empty($invoice[0]['order_id'
$description = 'Game server order (' . count($lineItems) . ' item' . (count($lineItems)===1?'': 's') . ')';
// URLs
-// Define the site base URL
-$siteBaseUrl = 'http://gameservers.world/modules/billing';
+// Define the site base URL - detect protocol and host dynamically
+$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
+$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
+$siteBase = $protocol . $host;
-// Generate absolute URLs for return and cancel
-$returnUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId);
-$cancelUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1';
+// Return URLs are root-relative (website will be deployed at root, not modules/billing)
+$returnUrl = $siteBase . '/payment_success.php?invoice=' . urlencode($invoiceId);
+$cancelUrl = $siteBase . '/payment_cancel.php?invoice=' . urlencode($invoiceId);
// API base (relative) - point to billing module API endpoints
$apiBase = 'api';
diff --git a/modules/billing/check_table.php b/modules/billing/check_table.php
new file mode 100644
index 00000000..94e5d9bd
--- /dev/null
+++ b/modules/billing/check_table.php
@@ -0,0 +1,76 @@
+ogp_billing_invoices Table Structure\n";
+
+$result = mysqli_query($db, "DESCRIBE ogp_billing_invoices");
+
+if (!$result) {
+ die("Table doesn't exist or query failed: " . mysqli_error($db));
+}
+
+echo "\n";
+echo "Field Type Null Key Default Extra \n";
+
+while ($row = mysqli_fetch_assoc($result)) {
+ echo "";
+ echo "{$row['Field']} ";
+ echo "{$row['Type']} ";
+ echo "{$row['Null']} ";
+ echo "{$row['Key']} ";
+ echo "" . ($row['Default'] ?? 'NULL') . " ";
+ echo "{$row['Extra']} ";
+ echo " \n";
+}
+
+echo "
\n";
+
+// Count existing invoices
+$count_result = mysqli_query($db, "SELECT COUNT(*) as cnt FROM ogp_billing_invoices");
+$count = mysqli_fetch_assoc($count_result);
+echo "Total invoices in table: {$count['cnt']}
\n";
+
+// Show last 5 invoices
+echo "Last 5 Invoices \n";
+$last_result = mysqli_query($db, "SELECT * FROM ogp_billing_invoices ORDER BY invoice_id DESC LIMIT 5");
+
+if (mysqli_num_rows($last_result) > 0) {
+ echo "\n";
+ echo "";
+ $first = true;
+ while ($row = mysqli_fetch_assoc($last_result)) {
+ if ($first) {
+ foreach (array_keys($row) as $col) {
+ echo "{$col} ";
+ }
+ echo " \n";
+ $first = false;
+ mysqli_data_seek($last_result, 0);
+ }
+ }
+
+ while ($row = mysqli_fetch_assoc($last_result)) {
+ echo "";
+ foreach ($row as $val) {
+ echo "" . htmlspecialchars($val ?? 'NULL') . " ";
+ }
+ echo " \n";
+ }
+ echo "
\n";
+} else {
+ echo "No invoices found.
\n";
+}
+
+mysqli_close($db);
+?>
diff --git a/modules/billing/create_invoices_table.sql b/modules/billing/create_invoices_table.sql
new file mode 100644
index 00000000..29a615f6
--- /dev/null
+++ b/modules/billing/create_invoices_table.sql
@@ -0,0 +1,33 @@
+-- Create billing_invoices table for invoice-first flow
+-- Run this SQL to enable the new billing system
+
+CREATE TABLE IF NOT EXISTS `ogp_billing_invoices` (
+ `invoice_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `order_id` INT(11) NOT NULL DEFAULT 0,
+ `user_id` INT(11) NOT NULL,
+ `service_id` INT(11) NOT NULL,
+ `home_name` VARCHAR(255) NOT NULL DEFAULT '',
+ `ip` INT(11) NOT NULL DEFAULT 0,
+ `max_players` INT(11) NOT NULL DEFAULT 0,
+ `remote_control_password` VARCHAR(255) NULL,
+ `ftp_password` VARCHAR(255) NULL,
+ `customer_name` VARCHAR(255) NOT NULL DEFAULT '',
+ `customer_email` VARCHAR(255) NOT NULL DEFAULT '',
+ `amount` FLOAT(15,2) NOT NULL DEFAULT 0,
+ `currency` VARCHAR(3) NOT NULL DEFAULT 'USD',
+ `status` VARCHAR(16) NOT NULL DEFAULT 'due',
+ `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `due_date` DATETIME NULL,
+ `paid_date` DATETIME NULL,
+ `payment_txid` VARCHAR(255) NULL,
+ `payment_method` VARCHAR(50) NULL,
+ `description` VARCHAR(500) NOT NULL DEFAULT '',
+ `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month',
+ `qty` INT(11) NOT NULL DEFAULT 1,
+ PRIMARY KEY (`invoice_id`),
+ KEY `order_id` (`order_id`),
+ KEY `user_id` (`user_id`),
+ KEY `status` (`status`),
+ KEY `due_date` (`due_date`),
+ KEY `service_id` (`service_id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php
index 78ab285e..1fa84348 100644
--- a/modules/billing/create_servers.php
+++ b/modules/billing/create_servers.php
@@ -241,7 +241,7 @@ function exec_ogp_module()
//PANEL LOG
$db->logger( "CREATED NEW SERVER " . $home_id);
// SEND EMAIL to new server only
- if($order['finish_date'] == 0){
+ if($order['end_date'] == 0){
$settings = $db->getSettings();
$subject = "New Gameserver installed at " . $settings['panel_name'];
$email = $db->resultQuery(" SELECT DISTINCT users_email
@@ -292,42 +292,42 @@ function exec_ogp_module()
// 'invoiced' - invoice created for renewal
// 'suspended' - server suspended for non-payment
// 'deleted' - server deleted after extended suspension
- //finish_date the server will be suspended
- //in cron_shop the finish_date is used to delete the server
+ //end_date the server will be suspended
+ //in cron_shop the end_date is used to delete the server
//several days after being suspended
if ($order['invoice_duration'] == "day")
{
- if($order['finish_date'] == 0){
- $finish_date = strtotime('+'.$order['qty'].' day');
+ if($order['end_date'] == 0){
+ $end_date = strtotime('+'.$order['qty'].' day');
}
else{
//this is a renewel, start from end of previous order
- $finish_date = strtotime('+'.$order['qty'].' day',$order['finish_date']);
+ $end_date = strtotime('+'.$order['qty'].' day',$order['end_date']);
}
}
elseif ($order['invoice_duration'] == "month")
{
// this is a new order
- if($order['finish_date'] == 0){
- $finish_date = strtotime('+'.$order['qty'].' month');
+ if($order['end_date'] == 0){
+ $end_date = strtotime('+'.$order['qty'].' month');
}
else{
//this is a renewel, start from end of previous order
- $finish_date = strtotime('+'.$order['qty'].' month',$order['finish_date']);
+ $end_date = strtotime('+'.$order['qty'].' month',$order['end_date']);
}
}
elseif ($order['invoice_duration'] == "year")
{
// this is a new order
- if($order['finish_date'] == 0){
- $finish_date = strtotime('+'.$order['qty'].' year');
+ if($order['end_date'] == 0){
+ $end_date = strtotime('+'.$order['qty'].' year');
}
else{
//this is a renewel, start from end of previous order
- $finish_date = strtotime('+'.$order['qty'].' year',$order['finish_date']);
+ $end_date = strtotime('+'.$order['qty'].' year',$order['end_date']);
}
@@ -339,7 +339,7 @@ function exec_ogp_module()
// set the order expiration
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
- SET finish_date='" . $db->realEscapeSingle($finish_date) . "'
+ SET end_date='" . $db->realEscapeSingle($end_date) . "'
WHERE order_id=".$db->realEscapeSingle($order_id));
// Save home id created by this order
diff --git a/modules/billing/cron-shop.php b/modules/billing/cron-shop.php
index b9103ac2..523c7d10 100644
--- a/modules/billing/cron-shop.php
+++ b/modules/billing/cron-shop.php
@@ -21,18 +21,20 @@
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*
+ * INVOICE-BASED BILLING SYSTEM
+ * =============================
*
-
-Complete Status Flow:
-in-cart - User added to cart, not yet paid
-renew - Renewal order in cart
-paid - Payment received, awaiting server creation
-installed - ✅ Active/Running (server provisioned and operational)
-invoiced - Invoice generated, payment due (7 days before expiration)
-suspended - Server stopped, payment overdue
-deleted - Server permanently removed (7 days after suspension)
-expired - Order has expired
-unknown - Error/undefined state
+ * 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
*/
chdir(realpath(dirname(__FILE__))); /* Change to the current file path */
@@ -41,7 +43,7 @@ chdir("../.."); /* Base path to ogp web files */
error_reporting(E_ALL);
// Path definitions
define("CONFIG_FILE","includes/config.inc.php");
-//Requiere
+//Require
require_once("includes/functions.php");
require_once("includes/helpers.php");
require_once("includes/html_functions.php");
@@ -55,23 +57,235 @@ $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', $today);
-//these dates are configured in the Shop Settings page
-$today=time();
-$invoice_date = strtotime('+ 7 days'); //this many days until the finish_date
-$suspend_date = $today; //suspend when overdue
-//final date is 10th, we need to remove on 17th, so final date is > removal_date
-$removal_date = strtotime('- 7 days'); //finish_date is passed 7 days ago
-$rundate = date('d/M/y G:i',$today);
-$db->logger("AUTO-CLEAN: Server Cleanup running at ".$rundate);
+$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 ($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']) . "
+ )");
+
+ // 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'])) .
+ ". A renewal invoice has been created. Please log in to your account and pay the invoice to continue your service." .
+ " Amount Due: $" . number_format($order['price'], 2) .
+ " Due Date: " . date('F j, Y', strtotime($order['end_date'])) .
+ " Thank you for your business! ";
+
+ $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}");
+ }
+ }
+}
+
+// ==================================================================================
+// STEP 2: SUSPEND SERVERS THAT ARE EXPIRED AND HAVE UNPAID INVOICES
+// ==================================================================================
+// Find servers that:
+// - Are currently installed (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 = 'installed'
+ AND o.end_date IS NOT NULL
+ AND UNIX_TIMESTAMP(o.end_date) < {$suspend_date}
+ AND i.status = 'unpaid'
+");
+
+if (is_array($servers_to_suspend)) {
+ foreach ($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}");
+ 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 ($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." .
+ " Your server has been stopped and will be permanently deleted in 7 days if payment is not received." .
+ " Please log in to your account and pay your outstanding invoice to restore your server." .
+ " Thank you.";
+
+ $mail = mymail($order['users_email'], $subject, $message, $settings);
+
+ if (!$mail) {
+ $db->logger("BILLING-CRON: Email FAILED - Suspension notice for order {$order_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'
+");
+
+if (is_array($servers_to_delete)) {
+ foreach ($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
+ $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
+ @$db->query("DROP USER 'server_" . $home_id . "'@'%'");
+ @$db->query("DROP USER 'server_" . $home_id . "'@'localhost'");
+ @$db->query("DROP DATABASE IF EXISTS server_" . $home_id);
+ }
+
+ // 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}");
+
+ $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." .
+ " The server was suspended 7 days ago due to non-payment and has now been removed." .
+ " If this was an error and you contact us immediately, we may be able to restore your server from backups." .
+ " 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}");
+ }
+ }
+}
+
+$db->logger("BILLING-CRON: Server lifecycle automation completed");
+?>
-//THESE SERVERS HAVE REACHED THE DATE FOR INVOICE, FINISH_DATE - 7 (OR WHAT IS IN SETTINGS)
+//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 finish_date <" . $invoice_date);
+ WHERE status IN ('paid', 'installed') AND end_date <" . $invoice_date);
if (!is_array($user_homes))
{
@@ -84,10 +298,10 @@ else
// 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/finish_date.
+ // 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/finish_date) as implemented
+ // update the existing order (change status/end_date) as implemented
// below.
$user_id = $user_home['user_id'];
@@ -119,12 +333,12 @@ else
}
}
-//THESE ARE THE SERVERS THAT HAVE NOT BEEN PAID AND THE FINISH_DATE IS TODAY
+//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 finish_date < ".$today);
+ WHERE status IN ('invoiced', 'in-cart', 'unknown') AND end_date < ".$today);
if (!is_array($user_homes))
{
@@ -178,7 +392,7 @@ else
//set removed servers as 'deleted'
$user_homes = $db->resultQuery( "SELECT *
FROM " . $table_prefix . "billing_orders
- WHERE status = 'suspended' AND finish_date < ".$removal_date );
+ WHERE status = 'suspended' AND end_date < ".$removal_date );
if (!is_array($user_homes))
{
diff --git a/modules/billing/fix_invoices_table_columns.sql b/modules/billing/fix_invoices_table_columns.sql
new file mode 100644
index 00000000..f292b760
--- /dev/null
+++ b/modules/billing/fix_invoices_table_columns.sql
@@ -0,0 +1,204 @@
+-- Fix missing columns / indexes for ogp_billing_invoices
+-- Safe script: checks information_schema and adds each missing column/index using prepared statements.
+-- IMPORTANT: Run on the target database (use the panel DB). Make a backup before running.
+
+-- Use the current database
+SET @db = DATABASE();
+SET @tbl = 'ogp_billing_invoices';
+
+-- Helper: add a column if missing
+-- Usage pattern below; repeated for every column we expect from module.php
+
+-- 1) service_id
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'service_id';
+
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 2) home_name
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'home_name';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `home_name` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `service_id`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 3) ip
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'ip';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `ip` INT(11) NOT NULL DEFAULT 0 AFTER `home_name`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 4) max_players
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'max_players';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `max_players` INT(11) NOT NULL DEFAULT 0 AFTER `ip`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 5) remote_control_password
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'remote_control_password';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `remote_control_password` VARCHAR(255) NULL AFTER `max_players`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 6) ftp_password
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'ftp_password';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `ftp_password` VARCHAR(255) NULL AFTER `remote_control_password`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 7) customer_name
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'customer_name';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `customer_name` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `ftp_password`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 8) customer_email
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'customer_email';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `customer_email` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `customer_name`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 9) amount
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'amount';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `amount` FLOAT(15,2) NOT NULL DEFAULT 0 AFTER `customer_email`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 10) currency
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'currency';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `currency` VARCHAR(3) NOT NULL DEFAULT ''USD'' AFTER `amount`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 11) status
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'status';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT ''due'' AFTER `currency`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 12) invoice_date
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'invoice_date';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 13) due_date
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'due_date';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `due_date` DATETIME NULL AFTER `invoice_date`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 14) paid_date
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'paid_date';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `paid_date` DATETIME NULL AFTER `due_date`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 15) payment_txid
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'payment_txid';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `payment_txid` VARCHAR(255) NULL AFTER `paid_date`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 16) payment_method
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'payment_method';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `payment_method` VARCHAR(50) NULL AFTER `payment_txid`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 17) description
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'description';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `description` VARCHAR(500) NOT NULL DEFAULT '''' AFTER `payment_method`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 18) invoice_duration
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'invoice_duration';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `invoice_duration` VARCHAR(16) NOT NULL DEFAULT ''month'' AFTER `description`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 19) qty
+SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qty';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qty` INT(11) NOT NULL DEFAULT 1 AFTER `invoice_duration`');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- 20) indexes: service_id, order_id, user_id, status, due_date
+-- Add index helper
+
+SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'service_id';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `service_id` (`service_id`)');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'order_id';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `order_id` (`order_id`)');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'user_id';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `user_id` (`user_id`)');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'status';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `status` (`status`)');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'due_date';
+IF @cnt = 0 THEN
+ SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `due_date` (`due_date`)');
+ PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+END IF;
+
+-- Done
+SELECT 'done' as status;
diff --git a/modules/billing/migration_to_invoices.sql b/modules/billing/migration_to_invoices.sql
new file mode 100644
index 00000000..4123fd6c
--- /dev/null
+++ b/modules/billing/migration_to_invoices.sql
@@ -0,0 +1,175 @@
+-- Migration Script: Billing System with Invoice Table
+-- This script upgrades existing billing installations to the new invoice-based system
+-- Run this ONCE on existing installations (not needed for fresh installs)
+-- Compatible with MySQL 5.7+ and MariaDB 10.2+
+
+-- Step 1: Add new columns to billing_orders (only if they don't exist)
+SET @dbname = DATABASE();
+SET @tablename = 'ogp_billing_orders';
+
+-- Add order_date column
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'order_date';
+SET @sql = IF(@col_exists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `order_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`',
+ 'SELECT "Column order_date already exists" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Add payment_txid column
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'payment_txid';
+SET @sql = IF(@col_exists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `payment_txid` VARCHAR(255) NULL AFTER `end_date`',
+ 'SELECT "Column payment_txid already exists" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Add paid_ts column
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'paid_ts';
+SET @sql = IF(@col_exists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `paid_ts` DATETIME NULL AFTER `payment_txid`',
+ 'SELECT "Column paid_ts already exists" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Step 2: Modify existing columns to use proper data types
+ALTER TABLE `ogp_billing_orders`
+ MODIFY COLUMN `status` VARCHAR(16) NOT NULL DEFAULT 'in-cart',
+ MODIFY COLUMN `remote_control_password` VARCHAR(255) NULL,
+ MODIFY COLUMN `ftp_password` VARCHAR(255) NULL;
+
+-- Convert end_date from VARCHAR to DATETIME (handle existing data)
+-- First, update any '0' values to NULL
+UPDATE `ogp_billing_orders` SET `end_date` = NULL WHERE `end_date` = '0' OR `end_date` = '';
+
+-- Check current end_date type and convert if needed
+SET @col_type = '';
+SELECT DATA_TYPE INTO @col_type FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'end_date';
+
+SET @sql = IF(@col_type = 'varchar',
+ 'ALTER TABLE `ogp_billing_orders` MODIFY COLUMN `end_date` DATETIME NULL',
+ 'SELECT "Column end_date already DATETIME" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Step 3: Remove obsolete columns from billing_orders
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'cart_id';
+SET @sql = IF(@col_exists > 0,
+ 'ALTER TABLE `ogp_billing_orders` DROP COLUMN `cart_id`',
+ 'SELECT "Column cart_id already removed" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'extended';
+SET @sql = IF(@col_exists > 0,
+ 'ALTER TABLE `ogp_billing_orders` DROP COLUMN `extended`',
+ 'SELECT "Column extended already removed" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Step 4: Add indexes to billing_orders for better performance
+SET @index_exists = 0;
+SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_user_id';
+SET @sql = IF(@index_exists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_user_id` (`user_id`)',
+ 'SELECT "Index idx_user_id already exists" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @index_exists = 0;
+SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_status';
+SET @sql = IF(@index_exists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_status` (`status`)',
+ 'SELECT "Index idx_status already exists" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @index_exists = 0;
+SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS
+WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_home_id';
+SET @sql = IF(@index_exists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_home_id` (`home_id`)',
+ 'SELECT "Index idx_home_id already exists" AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+
+-- Step 5: Create the new billing_invoices table
+CREATE TABLE IF NOT EXISTS `ogp_billing_invoices` (
+ `invoice_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `order_id` INT(11) NOT NULL,
+ `user_id` INT(11) NOT NULL,
+ `customer_name` VARCHAR(255) NOT NULL DEFAULT '',
+ `customer_email` VARCHAR(255) NOT NULL DEFAULT '',
+ `amount` FLOAT(15,2) NOT NULL DEFAULT 0,
+ `currency` VARCHAR(3) NOT NULL DEFAULT 'USD',
+ `status` VARCHAR(16) NOT NULL DEFAULT 'unpaid',
+ `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `due_date` DATETIME NULL,
+ `paid_date` DATETIME NULL,
+ `payment_txid` VARCHAR(255) NULL,
+ `payment_method` VARCHAR(50) NULL,
+ `description` VARCHAR(500) NOT NULL DEFAULT '',
+ `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month',
+ `qty` INT(11) NOT NULL DEFAULT 1,
+ PRIMARY KEY (`invoice_id`),
+ KEY `order_id` (`order_id`),
+ KEY `user_id` (`user_id`),
+ KEY `status` (`status`),
+ KEY `due_date` (`due_date`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+
+-- Step 6: Migrate existing paid orders to create initial invoices
+-- This creates a historical invoice for each paid/installed order
+INSERT INTO `ogp_billing_invoices`
+ (`order_id`, `user_id`, `customer_name`, `customer_email`, `amount`, `currency`, `status`, `invoice_date`, `paid_date`, `payment_txid`, `description`, `invoice_duration`, `qty`)
+SELECT
+ o.order_id,
+ o.user_id,
+ CONCAT(COALESCE(u.users_fname, ''), ' ', COALESCE(u.users_lname, '')) AS customer_name,
+ u.users_email AS customer_email,
+ o.price AS amount,
+ 'USD' AS currency,
+ 'paid' AS status,
+ COALESCE(o.order_date, NOW()) AS invoice_date,
+ COALESCE(o.paid_ts, o.order_date, NOW()) AS paid_date,
+ o.payment_txid,
+ CONCAT('Initial invoice for ', o.home_name) AS description,
+ o.invoice_duration,
+ o.qty
+FROM `ogp_billing_orders` o
+LEFT JOIN `ogp_users` u ON o.user_id = u.user_id
+WHERE o.status IN ('paid', 'installed')
+ AND NOT EXISTS (
+ SELECT 1 FROM `ogp_billing_invoices` i
+ WHERE i.order_id = o.order_id AND i.status = 'paid'
+ );
+
+-- Step 7: Drop the obsolete billing_carts table (replaced by invoice system)
+DROP TABLE IF EXISTS `ogp_billing_carts`;
+
+-- Step 8: Update billing_services charset for consistency
+ALTER TABLE `ogp_billing_services` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+
+SELECT 'Migration completed successfully! Invoice-based billing system is now active.' AS Status;
diff --git a/modules/billing/module.php b/modules/billing/module.php
index f108aa9c..83426fd9 100644
--- a/modules/billing/module.php
+++ b/modules/billing/module.php
@@ -24,87 +24,97 @@
// Module general information
$module_title = "billing";
-$module_version = "2.0";
-$db_version = 5;
+$module_version = "3.0";
+$db_version = 1;
$module_required = FALSE;
// Navigation disabled - this is now a purely external module
$module_menus = array();
$install_queries = array();
+
+// Version 1: Current schema - clean install with all tables and required columns
$install_queries[0] = array(
- "DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_services`;",
+ // Billing Services - Available game server packages
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_services` (
- `service_id` int(11) NOT NULL auto_increment,
- `home_cfg_id` int(11) NOT NULL,
- `mod_cfg_id` int(11) NOT NULL,
- `service_name` varchar(255) NOT NULL,
- `remote_server_id` varchar(255) NOT NULL,
- `out_of_stock` varchar(255) NOT NULL,
- `slot_max_qty` int(11) NOT NULL,
- `slot_min_qty` int(11) NOT NULL,
- `price_daily` float(15,4) NOT NULL,
- `price_monthly` float(15,4) NOT NULL,
- `price_year` float(15,4) NOT NULL,
- `description` varchar(1000) NOT NULL,
- `img_url` varchar(255) NOT NULL,
- `ftp` varchar(255) NOT NULL,
- `install_method` varchar(255) NOT NULL,
- `manual_url` varchar(255) NOT NULL,
- `access_rights` varchar(255) NOT NULL,
- `enabled` int(11) NOT NULL,
- PRIMARY KEY (`service_id`)
- ) ENGINE=MyISAM DEFAULT CHARSET=UTF8;",
-
- "DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_orders`;",
+ `service_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `home_cfg_id` INT(11) NOT NULL,
+ `mod_cfg_id` INT(11) NOT NULL,
+ `service_name` VARCHAR(255) NOT NULL,
+ `remote_server_id` VARCHAR(255) NOT NULL,
+ `out_of_stock` VARCHAR(255) NOT NULL DEFAULT '',
+ `slot_max_qty` INT(11) NOT NULL,
+ `slot_min_qty` INT(11) NOT NULL,
+ `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0,
+ `price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0,
+ `price_year` FLOAT(15,4) NOT NULL DEFAULT 0,
+ `description` VARCHAR(1000) NOT NULL DEFAULT '',
+ `img_url` VARCHAR(255) NOT NULL DEFAULT '',
+ `ftp` VARCHAR(255) NOT NULL DEFAULT '',
+ `install_method` VARCHAR(255) NOT NULL DEFAULT '',
+ `manual_url` VARCHAR(255) NOT NULL DEFAULT '',
+ `access_rights` VARCHAR(255) NOT NULL DEFAULT '',
+ `enabled` INT(11) NOT NULL DEFAULT 1,
+ PRIMARY KEY (`service_id`),
+ KEY `enabled` (`enabled`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;",
+
+ // Billing Orders - Actual game server instances (ongoing services)
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_orders` (
- `order_id` int(11) NOT NULL auto_increment,
- `user_id` int(11) NOT NULL,
- `service_id` int(11) NOT NULL,
- `home_name` varchar(255) NOT NULL,
- `ip` varchar(255) NOT NULL,
- `qty` int(11) NOT NULL,
- `invoice_duration` varchar(16) NOT NULL,
- `max_players` int(11) NOT NULL,
- `price` float(15,2) NOT NULL,
- `remote_control_password` varchar(10) NULL,
- `ftp_password` varchar(10) NULL,
- `cart_id` int(11) NOT NULL,
- `home_id` varchar(255) NOT NULL DEFAULT '0',
- `status` varchar(16) NOT NULL DEFAULT '0',
- `finish_date` varchar(16) NOT NULL DEFAULT '0',
- `extended` tinyint(1) NOT NULL,
- `coupon_id` int(11) NOT NULL DEFAULT 0,
- PRIMARY KEY (`order_id`)
- ) ENGINE=MyISAM;"
+ `order_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `user_id` INT(11) NOT NULL,
+ `service_id` INT(11) NOT NULL,
+ `home_name` VARCHAR(255) NOT NULL,
+ `ip` VARCHAR(255) NOT NULL DEFAULT '',
+ `qty` INT(11) NOT NULL DEFAULT 1,
+ `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month',
+ `max_players` INT(11) NOT NULL DEFAULT 0,
+ `price` FLOAT(15,2) NOT NULL DEFAULT 0,
+ `remote_control_password` VARCHAR(255) NULL,
+ `ftp_password` VARCHAR(255) NULL,
+ `home_id` VARCHAR(255) NOT NULL DEFAULT '0',
+ `status` VARCHAR(16) NOT NULL DEFAULT 'in-cart',
+ `order_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `end_date` DATETIME NULL,
+ `payment_txid` VARCHAR(255) NULL,
+ `paid_ts` DATETIME NULL,
+ `coupon_id` INT(11) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`order_id`),
+ KEY `user_id` (`user_id`),
+ KEY `status` (`status`),
+ KEY `home_id` (`home_id`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;",
+
+ // Billing Invoices - Created when user adds to cart, becomes order after payment
+ "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_invoices` (
+ `invoice_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `order_id` INT(11) NOT NULL DEFAULT 0,
+ `user_id` INT(11) NOT NULL,
+ `service_id` INT(11) NOT NULL,
+ `home_name` VARCHAR(255) NOT NULL DEFAULT '',
+ `ip` INT(11) NOT NULL DEFAULT 0,
+ `max_players` INT(11) NOT NULL DEFAULT 0,
+ `remote_control_password` VARCHAR(255) NULL,
+ `ftp_password` VARCHAR(255) NULL,
+ `customer_name` VARCHAR(255) NOT NULL DEFAULT '',
+ `customer_email` VARCHAR(255) NOT NULL DEFAULT '',
+ `amount` FLOAT(15,2) NOT NULL DEFAULT 0,
+ `currency` VARCHAR(3) NOT NULL DEFAULT 'USD',
+ `status` VARCHAR(16) NOT NULL DEFAULT 'due',
+ `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `due_date` DATETIME NULL,
+ `paid_date` DATETIME NULL,
+ `payment_txid` VARCHAR(255) NULL,
+ `payment_method` VARCHAR(50) NULL,
+ `description` VARCHAR(500) NOT NULL DEFAULT '',
+ `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month',
+ `qty` INT(11) NOT NULL DEFAULT 1,
+ PRIMARY KEY (`invoice_id`),
+ KEY `order_id` (`order_id`),
+ KEY `user_id` (`user_id`),
+ KEY `status` (`status`),
+ KEY `due_date` (`due_date`),
+ KEY `service_id` (`service_id`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;"
);
-$install_queries[1] = array(
- "DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_carts`;",
- "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_carts` (
- `cart_id` int(11) NOT NULL auto_increment,
- `user_id` int(11) NOT NULL,
- `paid` int(11) NULL,
- PRIMARY KEY (`cart_id`)
- ) ENGINE=MyISAM DEFAULT CHARSET=UTF8;"
-);
-
-$install_queries[2] = array(
- "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `date` varchar(16) NOT NULL DEFAULT '0';",
- "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `tax_amount` varchar(16) NOT NULL DEFAULT '0';",
- "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `currency` varchar(3) NOT NULL DEFAULT '0';"
-);
-
-$install_queries[3] = array(
- "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `coupon_id` int(11) NOT NULL DEFAULT 0;"
-);
-
-$install_queries[4] = array(
- "ALTER TABLE `".OGP_DB_PREFIX."billing_orders` MODIFY `coupon_id` int(11) NOT NULL DEFAULT 0;"
-);
-
-$install_queries[5] = array(
- "ALTER TABLE `".OGP_DB_PREFIX."billing_services` ADD `out_of_stock` varchar(255) NOT NULL AFTER `remote_server_id`;"
-);
-
-
?>
diff --git a/modules/billing/my_account.php b/modules/billing/my_account.php
index 62a3fdfe..5ebba5d3 100644
--- a/modules/billing/my_account.php
+++ b/modules/billing/my_account.php
@@ -125,7 +125,7 @@ $servers_query = "SELECT
o.price,
o.invoice_duration,
o.home_id,
- o.finish_date,
+ o.end_date,
bs.service_name
FROM ogp_billing_orders o
LEFT JOIN ogp_billing_services bs ON o.service_id = bs.service_id
@@ -325,7 +325,7 @@ $status_config = [
Expires:
-
+
diff --git a/modules/billing/my_servers.php b/modules/billing/my_servers.php
index dde6182d..9e257da6 100644
--- a/modules/billing/my_servers.php
+++ b/modules/billing/my_servers.php
@@ -40,8 +40,8 @@ $query = "SELECT
o.order_id,
o.status,
o.invoice_duration,
- -- use finish_date as the expiration marker (set when order is paid/created)
- o.finish_date AS expiration_date,
+ -- 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
FROM ogp_home h
diff --git a/modules/billing/payment_cancel.php b/modules/billing/payment_cancel.php
new file mode 100644
index 00000000..0d079f24
--- /dev/null
+++ b/modules/billing/payment_cancel.php
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
Payment Cancelled - Game Server Panel
+
+
+
+
+
+
+
Payment Cancelled
+
Your payment was cancelled. No charges have been made to your account.
+
+
Invoice Reference:
+
+
+
+
+
What would you like to do?
+
+ Return to Cart: Your items are still in your cart. You can complete the payment anytime.
+ Continue Shopping: Browse our game server options and add more to your cart.
+ Need Help?: Contact our support team if you encountered any issues during checkout.
+
+
+
+
+
+
+
+
+
diff --git a/modules/billing/payment_success.php b/modules/billing/payment_success.php
index 93245cd0..224c026b 100644
--- a/modules/billing/payment_success.php
+++ b/modules/billing/payment_success.php
@@ -1,54 +1,94 @@
mysqli_connect_error()]);
- else error_log('[payment_success] DB connect failed: ' . mysqli_connect_error());
- return false;
- }
+?>
+
+
+
+
+
+
Payment Successful - Game Server Panel
+
+
+
- // Helper to run a prepared update
- $update_paid = function($where_sql, $bind_types, $bind_vals) use ($db, $txid, $ts) {
- // Ensure we only set paid when not already paid
- $sql = "UPDATE ogp_billing_orders SET status = 'paid'";
- // Optionally set txid/paid_ts if columns exist; also attempt finish_date
- $cols = [];
- $res = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'payment_txid'");
- if ($res && mysqli_num_rows($res) > 0) $cols[] = 'payment_txid';
- $res2 = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'paid_ts'");
- if ($res2 && mysqli_num_rows($res2) > 0) $cols[] = 'paid_ts';
- $res3 = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'finish_date'");
- $has_finish = ($res3 && mysqli_num_rows($res3) > 0);
- // We'll compute finish_date when possible by selecting qty/invoice_duration for the matched row later
- if ($cols) {
- $sql .= ', ' . implode(' = ?, ', $cols) . ' = ?';
+
+
+
✓ Payment Successful!
+
Thank you for your purchase. Your payment has been received and is being processed.
+
+
Invoice Reference:
+
+
+
+
+
What happens next?
+
+ Payment Confirmation: Your payment has been captured by PayPal
+ Order Creation: Your game server order has been created
+ Server Provisioning: Your server will be provisioned automatically (this may take a few minutes)
+ Email Notification: You'll receive an email with your server details and login credentials
+
+
+
+ 0) {
+ $db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
+ if ($db) {
+ $result = mysqli_query($db, "SELECT * FROM ogp_billing_orders WHERE user_id=$user_id ORDER BY order_date DESC LIMIT 5");
+ if ($result && mysqli_num_rows($result) > 0) {
+ echo '
';
+ echo '
Your Recent Orders ';
+ echo '
';
+ echo '';
+ echo 'Order ID ';
+ echo 'Server ';
+ echo 'Status ';
+ echo 'Date ';
+ echo 'Price ';
+ echo ' ';
+
+ while ($order = mysqli_fetch_assoc($result)) {
+ $statusColor = $order['status'] === 'paid' ? '#28a745' : '#6c757d';
+ echo '';
+ echo '#' . htmlspecialchars($order['order_id']) . ' ';
+ echo '' . htmlspecialchars($order['home_name']) . ' ';
+ echo '' . htmlspecialchars(ucfirst($order['status'])) . ' ';
+ echo '' . htmlspecialchars($order['order_date']) . ' ';
+ echo '$' . htmlspecialchars(number_format($order['price'], 2)) . ' ';
+ echo ' ';
+ }
+
+ echo '
';
+ echo '
';
+ }
+ mysqli_close($db);
}
- // placeholder for finish_date; we'll append it if we can compute it
- $sql .= ' WHERE ' . $where_sql . ' AND status <> "paid" LIMIT 1';
+ }
+ ?>
- // If we need finish_date, attempt to compute it by selecting the row first
- $finish_date_val = null;
+
+
+
+
+
+
+ $end_date_val = null;
if ($has_finish) {
// Attempt to find the target order's qty/invoice_duration using the same where clause but without LIMIT
$sel_sql = "SELECT qty, invoice_duration FROM ogp_billing_orders WHERE " . str_replace(' AND status <> \"paid\" LIMIT 1', '', $where_sql) . " LIMIT 1";
@@ -77,17 +117,17 @@ function process_payment_record(array $record) {
if ($months <= 0) $months = 0;
$dt = new DateTime('now');
if ($months > 0) $dt->modify('+' . intval($months) . ' months');
- $finish_date_val = $dt->format('Y-m-d H:i:s');
+ $end_date_val = $dt->format('Y-m-d H:i:s');
}
$sel_stmt->close();
}
- if ($finish_date_val !== null) {
- $sql = str_replace(' WHERE ', ', finish_date = ? WHERE ', $sql);
+ if ($end_date_val !== null) {
+ $sql = str_replace(' WHERE ', ', end_date = ? WHERE ', $sql);
}
}
if ($stmt = $db->prepare($sql)) {
- // Build params: first any where params, then txid/ts values if present, then finish_date if present
+ // Build params: first any where params, then txid/ts values if present, then end_date if present
$types = $bind_types;
$vals = $bind_vals;
if ($cols) {
@@ -97,9 +137,9 @@ function process_payment_record(array $record) {
else $vals[] = $ts;
}
}
- if ($finish_date_val !== null) {
+ if ($end_date_val !== null) {
$types .= 's';
- $vals[] = $finish_date_val;
+ $vals[] = $end_date_val;
}
// bind dynamically
if ($types) {