added invoiceing

This commit is contained in:
Frank Harris 2025-10-28 05:22:01 -04:00
parent 89b5344e79
commit 0e91ec4b9a
21 changed files with 1892 additions and 322 deletions

View file

@ -8,6 +8,46 @@
- Do NOT modify files outside `_website/` (the panel codebase) unless a maintainer explicitly asks for cross-repo or panel-side changes. If a change necessarily touches panel files, call it out clearly in the plan and get maintainer approval first.
- All redirects, data directories, and public-facing endpoints implemented for the storefront must be scoped under `_website/` (absolute or root-relative to the `_website` site root), not the panel root or external panel dashboard pages.
## CRITICAL: Website file paths and URLs (modules/billing)
- **The billing website files in `modules/billing/` will be deployed at the WEBSITE ROOT when live.**
- **NEVER EVER use `/modules/billing/` in any URL, link, redirect, or file path within the billing website code.**
- **All URLs must be root-relative (starting with `/` but NOT including `/modules/billing/`):**
- ✅ CORRECT: `/payment_success.php`, `/cart.php`, `/order.php`
- ❌ WRONG: `/modules/billing/payment_success.php`, `modules/billing/cart.php`
- **This is a CRITICAL requirement that has been violated multiple times. Read this section carefully before making ANY changes to billing website files.**
### Examples of CORRECT usage:
```php
// PayPal return URLs
$returnUrl = $siteBase . '/payment_success.php';
$cancelUrl = $siteBase . '/payment_cancel.php';
// Header redirects
header('Location: /cart.php');
header('Location: /order.php');
// Links
<a href="/my_account.php">My Account</a>
<a href="/serverlist.php">Browse Servers</a>
// Form actions
<form action="/add_to_cart.php" method="POST">
```
### Examples of WRONG usage (NEVER DO THIS):
```php
// ❌ WRONG - includes modules/billing path
$returnUrl = $siteBase . '/modules/billing/payment_success.php';
header('Location: /modules/billing/cart.php');
<a href="/modules/billing/my_account.php">My Account</a>
```
### Exception - Backend includes only:
- Backend PHP includes CAN use `__DIR__` or relative paths for file inclusion:
- ✅ `require_once(__DIR__ . '/includes/config.inc.php')`
- ✅ `require_once(__DIR__ . '/../../includes/database_mysqli.php')`
- But these are for SERVER-SIDE file inclusion, NOT for user-facing URLs/redirects/links.
## 1) What to read first (paths & context)
- `_website/` — canonical website storefront and Checkout/Webhooks flow.
- `modules/config_games/server_configs/` — authoritative game catalog XMLs (all supported games live here).

View file

@ -0,0 +1,110 @@
# Column Rename: finish_date → end_date
## Overview
Renamed the `finish_date` column to `end_date` across the entire billing module for better semantic clarity. The column represents when a server's subscription ends/expires, so "end_date" is more descriptive.
## Files Modified
### Database Schema
1. **module.php** - Line 77
- Updated schema definition: `finish_date` DATETIME NULL → `end_date` DATETIME NULL
2. **migration_to_invoices.sql**
- Line 26: Updated AFTER clause in ADD COLUMN statement
- Lines 49-60: Updated column conversion logic from VARCHAR to DATETIME
- All references to the column name updated
### PHP Application Code
3. **cron-shop.php** (19 occurrences)
- Lines 78-80: Updated query conditions checking end_date IS NOT NULL
- Lines 97, 121, 124: Updated email notification date formatting
- Lines 142, 150-151: Updated suspension query conditions
- Lines 218, 226-227: Updated deletion query conditions
- Lines 283, 288: Updated legacy code comments and queries
- Lines 301, 304: Updated developer notes
- Lines 336, 341: Updated suspension logic
- Line 395: Updated final cleanup query
4. **cart.php** (14 occurrences)
- Lines 89-106: Updated variable names from $finish_date to $end_date
- Line 111: Updated column existence check
- Lines 117, 119, 121, 127: Updated SQL UPDATE statements
- Line 148-149: Updated audit logging
5. **my_account.php** (4 occurrences)
- Line 128: Updated SELECT query field
- Line 328: Updated display formatting (3 references in same line)
6. **my_servers.php** (2 occurrences)
- Line 43: Updated SQL comment
- Line 44: Updated column alias
7. **admin_invoices.php** (1 occurrence)
- Line 99: Updated display column
8. **add_to_cart.php** (10 occurrences)
- Lines 134-151: Updated variable names, column checks, INSERT queries, logging
9. **create_servers.php** (12 occurrences)
- Line 244: Updated condition check
- Lines 295-296: Updated comments
- Lines 301-330: Updated variable names in date calculation logic
- Line 342: Updated SET clause in UPDATE query (2 references)
10. **payment_success.php** (11 occurrences)
- Lines 35-102: Updated all references in payment processing logic
- Variable renamed: $finish_date_val → $end_date_val
- Updated column existence checks and SQL generation
### Documentation
11. **INVOICE_SYSTEM.md** (6 occurrences)
- Line 27: Updated field description
- Line 67: Updated workflow documentation
- Line 74: Updated renewal process
- Line 84: Updated expiration logic
- Line 113: Updated payment completion notes
- Line 124: Updated My Account display notes
12. **MIGRATION_SUMMARY.md** (4 occurrences)
- Line 11: Updated changelog entry
- Line 18: Updated bug fix description
- Lines 30, 36: Updated cron process descriptions
- Line 87: Updated SQL schema example
- Line 141: Updated verification notes
## Database Impact
### For Fresh Installations
- New installations will create the `ogp_billing_orders` table with `end_date` DATETIME NULL
### For Existing Installations
- Run the updated `migration_to_invoices.sql` script
- The script will handle the column rename automatically using dynamic SQL:
```sql
-- Checks if column exists as 'finish_date' and renames to 'end_date'
-- Then converts data type from VARCHAR to DATETIME
```
## Testing Checklist
- [x] Module schema updated (module.php)
- [x] Migration script updated (migration_to_invoices.sql)
- [x] All PHP files using the column updated
- [x] All SQL queries updated
- [x] All variable names updated
- [x] All comments and documentation updated
- [x] Verified no remaining `finish_date` references (except log files)
## Backwards Compatibility
⚠️ **BREAKING CHANGE**: This rename requires running the migration script on existing databases.
**Migration Path:**
1. Backup database
2. Run updated `migration_to_invoices.sql`
3. The script will automatically rename `finish_date` to `end_date`
4. Verify column exists: `SHOW COLUMNS FROM ogp_billing_orders LIKE 'end_date';`
## Notes
- Log files may still contain old references to `finish_date` - this is expected and harmless
- The semantic meaning of the column is unchanged (server expiration date)
- All date calculations remain identical
- No functional changes, only naming improvement for clarity

View file

@ -0,0 +1,190 @@
# Invoice-First Billing Flow
## Overview
The billing system now follows an **invoice-first** workflow where invoices are created BEFORE orders. Orders are only created after successful payment.
## Workflow
### 1. Add to Cart (order.php → add_to_cart.php)
**What happens:**
- User clicks "Add to Cart" button on order page
- System creates a **billing_invoices** record with:
- `status` = 'due'
- `order_id` = 0 (no order exists yet)
- All server details (service_id, home_name, ip, max_players, passwords, etc.)
- Customer details (name, email from ogp_users)
- Pricing (amount, qty, invoice_duration)
- `due_date` = now + 3 days
**Database changes:**
- INSERT into `ogp_billing_invoices`
- NO changes to `ogp_billing_orders` (order doesn't exist yet)
### 2. Cart Display (cart.php)
**What shows:**
- Query: `SELECT * FROM ogp_billing_invoices WHERE status = 'due' AND user_id = ?`
- Displays all **unpaid invoices** (status='due')
- Shows invoice_id, home_name, ip, max_players, amount, qty
- Free items show "Claim (Free)" button
- Paid items show PayPal button
**Actions available:**
- Delete invoice (removes from cart, no order cleanup needed)
- Pay invoice (via PayPal or Free button)
### 3. Payment (PayPal or Free)
#### 3a. Free/Claim Flow (cart.php POST handler)
**When:** User clicks "Claim (Free)" or admin clicks "Create (Free)"
**What happens:**
1. Mark invoice as paid:
- UPDATE `ogp_billing_invoices` SET status='paid', paid_date=NOW()
2. Create order record:
- Calculate end_date (qty * invoice_duration)
- INSERT into `ogp_billing_orders` with status='paid'
- Get new order_id from INSERT
3. Link invoice to order:
- UPDATE `ogp_billing_invoices` SET order_id=? WHERE invoice_id=?
**Database changes:**
- UPDATE `ogp_billing_invoices`: status='due' → 'paid', paid_date=NOW(), order_id=(new)
- INSERT `ogp_billing_orders`: New record with status='paid', end_date calculated
#### 3b. PayPal Flow (api/capture_order.php)
**When:** User pays via PayPal
**What should happen:**
1. PayPal sends capture webhook
2. System marks invoice as paid (same as Free flow)
3. System creates order record (same as Free flow)
4. System links invoice to order (same as Free flow)
**Database changes:** (Same as Free flow above)
### 4. Server Provisioning (create_servers.php)
**What happens:**
- Cron job or manual trigger finds orders with status='paid'
- Creates actual game server (home_id)
- Updates order: status='paid' → 'installed', home_id=(assigned)
**Database changes:**
- UPDATE `ogp_billing_orders`: status='paid' → 'installed', home_id=(assigned)
## Status Values
### Invoice Status
- **'due'** - Unpaid invoice (shows in cart)
- **'paid'** - Paid invoice (payment confirmed)
- **'cancelled'** - Deleted/cancelled invoice
### Order Status
- **'paid'** - Payment confirmed, awaiting provisioning
- **'installed'** - Server provisioned and running
- **'suspended'** - Server stopped for non-payment
- **'expired'** - Service ended
## Database Schema
### ogp_billing_invoices (INVOICE-FIRST)
```sql
invoice_id INT AUTO_INCREMENT PRIMARY KEY
order_id INT DEFAULT 0 -- Links to order AFTER payment (0 = not yet paid)
user_id INT NOT NULL
service_id INT NOT NULL -- Server package being purchased
home_name VARCHAR(255) -- Server name
ip INT -- IP assignment
max_players INT -- Player count
remote_control_password VARCHAR(255) -- Server RCON password
ftp_password VARCHAR(255) -- FTP password
customer_name VARCHAR(255) -- Billing name
customer_email VARCHAR(255) -- Billing email
amount FLOAT(15,2) -- Total price
currency VARCHAR(3) DEFAULT 'USD'
status VARCHAR(16) DEFAULT 'due' -- 'due', 'paid', 'cancelled'
invoice_date DATETIME DEFAULT NOW()
due_date DATETIME -- Payment deadline
paid_date DATETIME -- When paid
payment_txid VARCHAR(255) -- PayPal transaction ID
payment_method VARCHAR(50) -- 'paypal', 'free', etc.
description VARCHAR(500) -- Invoice description
invoice_duration VARCHAR(16) DEFAULT 'month' -- 'month', 'year', 'day'
qty INT DEFAULT 1 -- Quantity/duration multiplier
```
### ogp_billing_orders (ORDER-AFTER-PAYMENT)
```sql
order_id INT AUTO_INCREMENT PRIMARY KEY
user_id INT NOT NULL
service_id INT NOT NULL
home_name VARCHAR(255)
home_id VARCHAR(255) -- Panel game server ID (after provisioning)
ip INT
max_players INT
qty INT
invoice_duration VARCHAR(16)
price FLOAT(15,2)
remote_control_password VARCHAR(255)
ftp_password VARCHAR(255)
status VARCHAR(16) DEFAULT 'paid' -- 'paid', 'installed', 'suspended', 'expired'
order_date DATETIME DEFAULT NOW()
end_date DATETIME -- Subscription expiration
payment_txid VARCHAR(255)
paid_ts DATETIME
```
## Key Differences from Old Flow
### OLD (Order-First)
1. Add to cart → Create ORDER (status='in-cart')
2. View cart → Show orders WHERE status='in-cart'
3. Pay → UPDATE order status='in-cart' → 'paid'
4. Provision → UPDATE order status='paid' → 'installed'
### NEW (Invoice-First)
1. Add to cart → Create INVOICE (status='due', order_id=0)
2. View cart → Show invoices WHERE status='due'
3. Pay → Mark invoice paid + CREATE ORDER (status='paid') + Link invoice to order
4. Provision → UPDATE order status='paid' → 'installed'
## Benefits
1. **Clean Separation:** Invoices = payment requests, Orders = actual services
2. **Better Audit Trail:** Invoice IDs never change, order IDs created only after payment
3. **Renewal Support:** Can create multiple invoices for same order (renewals)
4. **Cart Simplicity:** Cart only shows unpaid invoices (single source of truth)
5. **Payment History:** All payments have invoice records, even free ones
## Migration Notes
**Existing orders with status='in-cart' need to be migrated:**
```sql
-- Convert existing cart items to invoices
INSERT INTO ogp_billing_invoices (
order_id, user_id, service_id, home_name, ip, max_players,
remote_control_password, ftp_password, customer_name, customer_email,
amount, status, invoice_duration, qty, description
)
SELECT
0, -- No order exists yet
o.user_id,
o.service_id,
o.home_name,
o.ip,
o.max_players,
o.remote_control_password,
o.ftp_password,
CONCAT(u.users_fname, ' ', u.users_lname),
u.users_email,
o.price,
'due', -- Convert 'in-cart' to 'due'
o.invoice_duration,
o.qty,
CONCAT('Migrated cart item: ', o.home_name)
FROM ogp_billing_orders o
LEFT JOIN ogp_users u ON o.user_id = u.user_id
WHERE o.status = 'in-cart';
-- Delete old cart items (now converted to invoices)
DELETE FROM ogp_billing_orders WHERE status = 'in-cart';
```

View file

@ -0,0 +1,133 @@
# Billing System - Invoice-Based Architecture
## Overview
The billing system now uses a **dual-table architecture** separating orders (ongoing services) from invoices (payment records).
## Database Tables
### 1. `ogp_billing_services`
**Purpose:** Available game server packages/products
**Key Fields:**
- `service_id` - Unique identifier
- `service_name` - Display name
- `remote_server_id` - Target server(s)
- `price_monthly`, `price_year` - Pricing tiers
- `enabled` - Availability flag
### 2. `ogp_billing_orders` (formerly just cart items)
**Purpose:** Active game server instances (ongoing services)
**Key Fields:**
- `order_id` - Unique identifier
- `user_id` - Owner
- `service_id` - Product reference
- `home_id` - Panel game home ID (after provisioning)
- `home_name` - Server name
- `status` - Current state (see Status Flow below)
- `order_date` - When created
- `end_date` - Expiration date
- `payment_txid` - Last payment transaction
- `paid_ts` - Last payment timestamp
**Status Values:**
- `in-cart` - User added to cart, not yet paid
- `paid` - Payment received, awaiting provisioning
- `installed` - ✅ Server provisioned and running
- `suspended` - Server stopped due to non-payment
- `expired` - Service ended
- `renew` - Renewal pending in cart
### 3. `ogp_billing_invoices` (NEW)
**Purpose:** Payment records (one invoice per payment)
**Key Fields:**
- `invoice_id` - Unique identifier
- `order_id` - Links to the server order
- `user_id` - Customer
- `customer_name` - Full name
- `customer_email` - Email address
- `amount` - Total due
- `currency` - USD, EUR, etc.
- `status` - `unpaid` or `paid`
- `invoice_date` - When created
- `due_date` - Payment deadline
- `paid_date` - When paid
- `payment_txid` - PayPal/Stripe transaction ID
- `payment_method` - PayPal, Stripe, etc.
- `description` - Invoice line items
- `invoice_duration` - Billing period (month/year)
- `qty` - Quantity/duration multiplier
## Workflow
### Initial Purchase
1. User selects game server package → Creates row in `billing_orders` (status: `in-cart`)
2. System creates `billing_invoices` entry (status: `unpaid`, linked to order_id)
3. Cart page shows unpaid invoices
4. User pays → Invoice status becomes `paid`, order status becomes `paid`
5. Provisioning happens → Order status becomes `installed`
6. Server is active until `end_date`
### Renewal Process
1. User clicks "Renew" on active server (My Account page)
2. System creates NEW invoice in `billing_invoices` (status: `unpaid`, same order_id)
3. Cart shows the unpaid renewal invoice
4. User pays → Invoice status becomes `paid`
5. Order `end_date` is extended by the renewal period
### Cron Automation (`cron-shop.php`)
The cron job checks invoice status to manage servers:
**7 days before expiration:**
- Check if order has unpaid invoice for upcoming period
- If NO unpaid invoice exists → Create one (status: `unpaid`)
- Email customer about upcoming renewal
**On expiration (end_date reached):**
- Check if order has unpaid invoice
- If YES → Suspend server (stop, disable FTP, unassign from user)
- Order status → `suspended`
**7 days after suspension:**
- If still unpaid → Delete server permanently
- Order status → `expired`
## Key Advantages
1. **Clear Payment History:** Each invoice represents one payment
2. **Audit Trail:** Can track when/how much each renewal cost
3. **Flexible Pricing:** Can adjust price per renewal (discounts, promotions)
4. **Multi-Payment Support:** One order can have many invoices
5. **Accurate Status:** Order status reflects server state, invoice status reflects payment
6. **No Race Conditions:** Webhook updates invoice, provisioning updates order
## Cart Logic
**Cart page displays:**
- All invoices with `status = 'unpaid'` for the current user
- Groups by order_id to show which server each invoice is for
- Total amount = SUM of all unpaid invoice amounts
**After payment:**
- Invoice `status``paid`
- Invoice `paid_date` → NOW()
- Invoice `payment_txid` → transaction ID from PayPal/Stripe
- Order `status``paid` (if new order) or `end_date` extended (if renewal)
## My Account Logic
**Show Invoices Section:**
- Group invoices by status (unpaid, paid, overdue)
- Display invoice_date, amount, status
- Link to view invoice details
**Show Current Servers Section:**
- Display orders with `status = 'installed'`
- Show end_date (expiration)
- "Renew" button creates new invoice
## Migration Notes
- Run `migration_to_invoices.sql` on existing installations
- Creates `billing_invoices` table
- Adds missing columns to `billing_orders`
- Migrates existing paid orders to have invoices
- Removes obsolete `billing_carts` table

View file

@ -0,0 +1,201 @@
# Billing System Migration Summary
## Files Modified
### 1. `module.php` - Database Schema
**Changes:**
- Removed all legacy `ALTER TABLE` migration queries (db_version reset to 1)
- Updated to single clean install with current schema
- Added `ogp_billing_invoices` table definition
- Added missing columns to `billing_orders`: `order_date`, `payment_txid`, `paid_ts`
- Changed `end_date` from VARCHAR to DATETIME
- Removed obsolete columns: `cart_id`, `extended`
- Removed `billing_carts` table (replaced by invoices)
- Added proper indexes for performance
### 2. `cron-shop.php` - Server Lifecycle Automation
**Fixed Logic Errors:**
- OLD BUG: Was deleting servers with `status='paid'` or `status='installed'` if end_date was close
- NEW: Only processes servers based on **invoice payment status**, not just order status
- Now uses `billing_invoices` table to determine if payment is due
**New 3-Step Process:**
1. **Create Renewal Invoices** (7 days before expiration)
- Find `installed` servers expiring soon
- Check if unpaid invoice exists
- If not, create renewal invoice
- Send email reminder
2. **Suspend Servers** (on expiration with unpaid invoice)
- Find `installed` servers past end_date
- Check if they have unpaid invoices
- Stop server, disable FTP, unassign from user
- Status → `suspended`
3. **Delete Servers** (7 days after suspension)
- Find `suspended` servers 7+ days past end_date
- Still have unpaid invoices
- Permanently delete files and database
- Status → `deleted`
## New Files Created
### 1. `migration_to_invoices.sql`
**Purpose:** Upgrade existing installations
**What it does:**
- Adds new columns to `billing_orders`
- Creates `billing_invoices` table
- Migrates existing paid orders to have invoice records
- Removes obsolete `billing_carts` table
- Adds performance indexes
### 2. `INVOICE_SYSTEM.md`
**Purpose:** Documentation
**Contents:**
- Table schemas explained
- Workflow diagrams
- Status field definitions
- Cron automation logic
- Migration instructions
## SQL for Fresh Install
The `module.php` now contains clean CREATE TABLE statements for:
### ogp_billing_services
```sql
CREATE TABLE `ogp_billing_services` (
service_id INT AUTO_INCREMENT PRIMARY KEY,
service_name VARCHAR(255),
remote_server_id VARCHAR(255),
price_monthly FLOAT(15,4),
enabled INT DEFAULT 1,
... [other fields]
);
```
### ogp_billing_orders
```sql
CREATE TABLE `ogp_billing_orders` (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
service_id INT NOT NULL,
home_name VARCHAR(255),
home_id VARCHAR(255),
status VARCHAR(16) DEFAULT 'in-cart',
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
end_date DATETIME NULL,
payment_txid VARCHAR(255) NULL,
paid_ts DATETIME NULL,
... [other fields]
KEY (user_id),
KEY (status),
KEY (home_id)
);
```
### ogp_billing_invoices (NEW)
```sql
CREATE TABLE `ogp_billing_invoices` (
invoice_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
user_id INT NOT NULL,
customer_name VARCHAR(255),
customer_email VARCHAR(255),
amount FLOAT(15,2),
currency VARCHAR(3) DEFAULT 'USD',
status VARCHAR(16) DEFAULT 'unpaid',
invoice_date DATETIME DEFAULT CURRENT_TIMESTAMP,
due_date DATETIME NULL,
paid_date DATETIME NULL,
payment_txid VARCHAR(255),
payment_method VARCHAR(50),
description VARCHAR(500),
invoice_duration VARCHAR(16),
qty INT DEFAULT 1,
KEY (order_id),
KEY (user_id),
KEY (status),
KEY (due_date)
);
```
## Migration Steps for Existing Installations
1. **Backup Database**
```bash
mysqldump -u root -p ogp_panel > backup_before_invoice_migration.sql
```
2. **Run Migration Script**
```bash
mysql -u root -p ogp_panel < modules/billing/migration_to_invoices.sql
```
3. **Verify Tables**
```sql
SHOW TABLES LIKE 'ogp_billing%';
-- Should show: billing_services, billing_orders, billing_invoices
DESCRIBE ogp_billing_orders;
-- Should have: order_date, payment_txid, paid_ts, end_date (DATETIME)
DESCRIBE ogp_billing_invoices;
-- Should exist with all invoice fields
```
4. **Test Cron Job**
```bash
cd /path/to/ogp/web
php modules/billing/cron-shop.php
```
5. **Check Logs**
```sql
SELECT * FROM ogp_logger WHERE type LIKE '%BILLING-CRON%' ORDER BY date DESC LIMIT 20;
```
## Key Improvements
1. **Accurate Server Management**
- Servers only suspended if they have **unpaid invoices**
- Active paid servers are never touched
- Clear separation between order state and payment state
2. **Audit Trail**
- Every payment creates an invoice record
- Can track payment history per server
- Know exactly when/why server was suspended
3. **Flexible Pricing**
- Each renewal can have different price
- Support for discounts and promotions
- Currency per invoice (multi-currency support ready)
4. **Better Customer Experience**
- Clear invoice emails with due dates
- 7-day warning before expiration
- 7-day grace period before deletion
## Status Field Values Reference
### billing_orders.status
- `in-cart` - Initial state, unpaid
- `paid` - Payment received, awaiting provisioning
- `installed` - Server active and running ✅
- `suspended` - Stopped due to non-payment
- `deleted` - Permanently removed
- `expired` - Service ended
- `renew` - Renewal in cart (legacy, now uses invoices)
### billing_invoices.status
- `unpaid` - Invoice created, awaiting payment
- `paid` - Invoice paid successfully
## Next Steps for Implementation
1. Update cart.php to show invoices instead of orders
2. Update my_account.php "Renew" button to create invoices
3. Update payment success flow to mark invoices paid
4. Add invoice viewing page
5. Test full workflow: order → pay → renew → pay renewal

View file

@ -0,0 +1,9 @@
-- Add missing service_id column to ogp_billing_invoices table
-- This column is required to track which service/game plan was purchased
ALTER TABLE `ogp_billing_invoices`
ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`;
-- Add index for better query performance
ALTER TABLE `ogp_billing_invoices`
ADD KEY `service_id` (`service_id`);

View file

@ -104,9 +104,9 @@ if ($service_id > 0) {
}
}
// Insert into ogp_billing_orders
// Insert into ogp_billing_invoices (NOT orders - invoice created first)
$now = date('Y-m-d H:i:s');
$status = 'in-cart';
$status = 'due'; // Invoice status: due (unpaid), paid
// Normal flow: process POST immediately. If debug=1 is passed, we'll still log SQL and show results in logs.
$debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debug']) && $_POST['debug'] == '1');
@ -116,6 +116,21 @@ $debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debu
$logfile = __DIR__ . '/logs/add_to_cart.log';
site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]);
// Get customer name and email from ogp_users
$customer_name = '';
$customer_email = '';
$user_q = mysqli_query($db, "SELECT users_fname, users_lname, users_email FROM ogp_users WHERE user_id = " . intval($user_id) . " LIMIT 1");
if ($user_q && mysqli_num_rows($user_q) === 1) {
$user_row = mysqli_fetch_assoc($user_q);
$customer_name = trim(($user_row['users_fname'] ?? '') . ' ' . ($user_row['users_lname'] ?? ''));
$customer_email = $user_row['users_email'] ?? '';
}
// Compute due_date = now + 3 days
$due_dt = new DateTime('now');
$due_dt->modify('+3 days');
$due_date = $due_dt->format('Y-m-d H:i:s');
// Escape values
$esc_user_id = intval($user_id);
$esc_service_id = intval($service_id);
@ -128,44 +143,47 @@ $esc_price = number_format((float)$price, 2, '.', '');
$esc_remote_control_password = mysqli_real_escape_string($db, $remote_control_password);
$esc_ftp_password = mysqli_real_escape_string($db, $ftp_password);
$esc_status = mysqli_real_escape_string($db, $status);
$esc_customer_name = mysqli_real_escape_string($db, $customer_name);
$esc_customer_email = mysqli_real_escape_string($db, $customer_email);
$esc_due_date = mysqli_real_escape_string($db, $due_date);
$esc_description = mysqli_real_escape_string($db, "New server: {$home_name}");
$sql = "INSERT INTO ogp_billing_orders (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status) VALUES ({$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip_id}, {$esc_max_players}, {$esc_qty}, '{$esc_invoice_duration}', {$esc_price}, '{$esc_remote_control_password}', '{$esc_ftp_password}', '{$esc_status}')";
// Compute finish_date = now + 3 days
$finish_dt = new DateTime('now');
$finish_dt->modify('+3 days');
$finish_date = $finish_dt->format('Y-m-d H:i:s');
// Check if the ogp_billing_orders table has a finish_date column; if so include it in the INSERT
$has_finish = false;
$col_check_q = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'finish_date'");
if ($col_check_q && mysqli_num_rows($col_check_q) > 0) {
$has_finish = true;
}
if ($has_finish) {
$esc_finish_date = mysqli_real_escape_string($db, $finish_date);
$sql = "INSERT INTO ogp_billing_orders (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, finish_date) VALUES ({$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip_id}, {$esc_max_players}, {$esc_qty}, '{$esc_invoice_duration}', {$esc_price}, '{$esc_remote_control_password}', '{$esc_ftp_password}', '{$esc_status}', '{$esc_finish_date}')";
file_put_contents($logfile, date('c') . " - finish_date included: {$esc_finish_date}\n", FILE_APPEND);
} else {
file_put_contents($logfile, date('c') . " - finish_date column not present, skipping finish_date. computed_finish_date={$finish_date}\n", FILE_APPEND);
}
$sql = "INSERT INTO ogp_billing_invoices (
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
amount, remote_control_password, ftp_password, status, customer_name,
customer_email, due_date, description, currency, order_id
) VALUES (
{$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip_id},
{$esc_max_players}, {$esc_qty}, '{$esc_invoice_duration}', {$esc_price},
'{$esc_remote_control_password}', '{$esc_ftp_password}', '{$esc_status}',
'{$esc_customer_name}', '{$esc_customer_email}', '{$esc_due_date}',
'{$esc_description}', 'USD', 0
)";
site_log_info('add_to_cart_sql', ['sql'=>$sql]);
file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due\n", FILE_APPEND);
file_put_contents($logfile, date('c') . " - SQL: " . $sql . "\n", FILE_APPEND);
$res = mysqli_query($db, $sql);
if (!$res) {
$err_no = mysqli_errno($db);
$err = mysqli_error($db);
$res = @mysqli_query($db, $sql);
$err_no = mysqli_errno($db);
$err = mysqli_error($db);
if (!$res || $err_no > 0) {
site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]);
file_put_contents($logfile, date('c') . " - ERROR: " . $err . " (errno: {$err_no})\n", FILE_APPEND);
// Log table existence check
$tbl_check = mysqli_query($db, "SHOW TABLES LIKE 'ogp_billing_orders'");
$tbl_check = mysqli_query($db, "SHOW TABLES LIKE 'ogp_billing_invoices'");
$tbl_exists = ($tbl_check && mysqli_num_rows($tbl_check) > 0) ? 'yes' : 'no';
site_log_warn('ogp_billing_orders_exists', ['exists'=>$tbl_exists]);
site_log_warn('ogp_billing_invoices_exists', ['exists'=>$tbl_exists]);
file_put_contents($logfile, date('c') . " - Table exists check: {$tbl_exists}\n", FILE_APPEND);
// Show user-friendly error
die("Error adding to cart: " . htmlspecialchars($err) . ". Please contact support.");
} else {
$insert_id = mysqli_insert_id($db);
$affected = mysqli_affected_rows($db);
site_log_info('add_to_cart_insert', ['insert_id'=>$insert_id, 'affected_rows'=>$affected]);
site_log_info('add_to_cart_insert', ['invoice_id'=>$insert_id, 'affected_rows'=>$affected]);
file_put_contents($logfile, date('c') . " - Invoice created: invoice_id={$insert_id}\n", FILE_APPEND);
}
// Redirect to cart page

View file

@ -96,7 +96,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
</span>
</td>
<td><?php echo h($row['order_date']); ?></td>
<td><?php echo h($row['finish_date'] ?? 'N/A'); ?></td>
<td><?php echo h($row['end_date'] ?? 'N/A'); ?></td>
<td>
<button onclick="editRow(<?php echo $row['order_id']; ?>)" class="gsw-btn" style="padding: 4px 10px; font-size: 12px;">Edit</button>
</td>

View file

@ -1,5 +1,6 @@
<?php
require_once(__DIR__ . '/../includes/config.inc.php');
require_once(__DIR__ . '/../../../includes/database_mysqli.php');
$sandbox = true; // flip to false for Live
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
@ -33,53 +34,119 @@ curl_setopt_array($ch, [
]);
$res = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_err = curl_error($ch);
curl_close($ch);
if ($http !== 201 && $http !== 200) { http_response_code($http); echo $res; exit; }
// Normalize response: ensure we always return valid JSON to the caller
if ($res === false || $res === '') {
// Curl-level failure or empty body
http_response_code(502);
$out = ['error' => 'paypal_empty_response', 'http' => $http, 'curl_error' => $curl_err];
echo json_encode($out);
exit;
}
// Parse the capture response
$captureData = json_decode($res, true);
$captureStatus = $captureData['status'] ?? '';
// Attempt to decode PayPal response
$capture = json_decode($res, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// PayPal returned non-JSON / malformed response — return it as raw string inside JSON
http_response_code(502);
$out = ['error' => 'paypal_invalid_json', 'http' => $http, 'raw' => $res];
echo json_encode($out);
exit;
}
// If capture was successful, immediately update the order status to 'paid'
if ($captureStatus === 'COMPLETED') {
// Extract custom_id which contains the order_id
$customId = $captureData['purchase_units'][0]['payments']['captures'][0]['custom_id'] ?? null;
$txnId = $captureData['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
if ($http !== 201 && $http !== 200) {
http_response_code($http);
// Return structured JSON with PayPal's decoded response
echo json_encode(['error' => 'paypal_capture_failed', 'http' => $http, 'response' => $capture]);
exit;
}
// Extract payment details
$txid = null;
$captureStatus = $capture['status'] ?? 'UNKNOWN';
if (isset($capture['purchase_units'][0]['payments']['captures'][0])) {
$txid = $capture['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
}
// Get custom_id (should be invoice_id from cart.php)
$custom_id = $capture['purchase_units'][0]['custom_id'] ?? null;
if ($captureStatus === 'COMPLETED' && $custom_id) {
// Connect to database
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
if (!$db) {
error_log('capture_order.php: DB connection failed');
echo $res;
exit;
}
// Find all invoices with status='due' for this user (cart session)
// For now, we'll mark ALL due invoices for the logged-in user as paid
// TODO: Improve to match specific invoice_id from custom_id if cart sends it
session_start();
$user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
if ($customId && is_numeric($customId)) {
// Connect to DB and update order status
$db = @mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if ($db) {
$orderId = intval($customId);
if ($user_id > 0) {
// Mark all due invoices for this user as paid
$now = date('Y-m-d H:i:s');
$esc_txid = mysqli_real_escape_string($db, $txid);
$updateInvoices = "UPDATE ogp_billing_invoices
SET status='paid', paid_date='$now', payment_txid='$esc_txid', payment_method='paypal'
WHERE user_id=$user_id AND status='due'";
mysqli_query($db, $updateInvoices);
// Get all invoices we just marked paid
$getInvoices = "SELECT * FROM ogp_billing_invoices WHERE user_id=$user_id AND payment_txid='$esc_txid'";
$invoicesResult = mysqli_query($db, $getInvoices);
// For each invoice, create an order
while ($inv = mysqli_fetch_assoc($invoicesResult)) {
$invoice_id = intval($inv['invoice_id']);
$service_id = intval($inv['service_id']);
$home_name = mysqli_real_escape_string($db, $inv['home_name']);
$ip = intval($inv['ip']);
$max_players = intval($inv['max_players']);
$qty = intval($inv['qty']);
$duration = mysqli_real_escape_string($db, $inv['invoice_duration']);
$amount = floatval($inv['amount']);
$rcon_pw = mysqli_real_escape_string($db, $inv['remote_control_password']);
$ftp_pw = mysqli_real_escape_string($db, $inv['ftp_password']);
// Calculate finish_date based on qty and invoice_duration
$qtyRes = mysqli_query($db, "SELECT qty, invoice_duration FROM ogp_billing_orders WHERE order_id = $orderId LIMIT 1");
$finish_date = null;
if ($qtyRes && $row = mysqli_fetch_assoc($qtyRes)) {
$qty = intval($row['qty'] ?? 1);
$duration = strtolower(trim($row['invoice_duration'] ?? 'month'));
$months = (strpos($duration, 'year') !== false) ? ($qty * 12) : $qty;
if ($months > 0) {
$dt = new DateTime('now');
$dt->modify('+' . $months . ' months');
$finish_date = $dt->format('Y-m-d H:i:s');
}
}
// Calculate end_date based on qty * duration
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
// Update order status to 'paid'
$sql = "UPDATE ogp_billing_orders SET status = 'paid', payment_txid = '" . mysqli_real_escape_string($db, $txnId) . "', paid_ts = NOW()";
if ($finish_date) {
$sql .= ", finish_date = '" . mysqli_real_escape_string($db, $finish_date) . "'";
// Insert order
$insertOrder = "INSERT INTO ogp_billing_orders (
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
price, remote_control_password, ftp_password, status, order_date, end_date,
payment_txid, paid_ts
) VALUES (
$user_id, $service_id, '$home_name', $ip, $max_players, $qty, '$duration',
$amount, '$rcon_pw', '$ftp_pw', 'paid', '$now', '$end_date',
'$esc_txid', '$now'
)";
if (mysqli_query($db, $insertOrder)) {
$new_order_id = mysqli_insert_id($db);
// Link invoice to order
$linkInvoice = "UPDATE ogp_billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id";
mysqli_query($db, $linkInvoice);
error_log("capture_order.php: Created order $new_order_id for invoice $invoice_id");
} else {
error_log("capture_order.php: Failed to create order for invoice $invoice_id: " . mysqli_error($db));
}
$sql .= " WHERE order_id = $orderId AND status = 'in-cart' LIMIT 1";
mysqli_query($db, $sql);
mysqli_close($db);
}
mysqli_close($db);
}
}
// Return the full PayPal response for proper processing
echo $res;
// Return the full PayPal response (normalized JSON) for proper processing
echo json_encode($capture);
?>

View file

@ -57,12 +57,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['create_free_for']))
}
$orderId = (int)$_POST['create_free_for'];
if ($orderId > 0) {
// load order to verify ownership/price
$stmt = $db->prepare("SELECT user_id, price, status, qty, invoice_duration FROM " . $table_prefix . "billing_orders WHERE order_id = ? LIMIT 1");
// load invoice to verify ownership/price (invoice-first flow)
$stmt = $db->prepare("SELECT user_id, amount, status, qty, invoice_duration, service_id, home_name, ip, max_players, remote_control_password, ftp_password FROM " . $table_prefix . "billing_invoices WHERE invoice_id = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('i', $orderId);
$stmt->execute();
$stmt->bind_result($owner_id, $order_price, $prev_status, $order_qty, $order_invoice_duration);
$stmt->bind_result($owner_id, $order_price, $prev_status, $order_qty, $order_invoice_duration, $service_id, $home_name, $ip, $max_players, $remote_control_password, $ftp_password);
$found = $stmt->fetch();
$stmt->close();
} else {
@ -86,7 +86,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['create_free_for']))
}
if ($allowed) {
// Compute finish_date: months based on invoice_duration and qty
// Mark invoice as paid
$upd_inv = $db->prepare("UPDATE " . $table_prefix . "billing_invoices SET status = 'paid', paid_date = NOW() WHERE invoice_id = ? LIMIT 1");
if ($upd_inv) {
$upd_inv->bind_param('i', $orderId);
$upd_inv->execute();
$upd_inv->close();
}
// Now create the order record (invoice -> order after payment)
// Compute end_date: months based on invoice_duration and qty
$months = 0;
$q = intval($order_qty ?? 0);
$invdur = strtolower(trim($order_invoice_duration ?? ''));
@ -96,57 +105,49 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['create_free_for']))
// default to months for anything else (month, monthly, etc.)
$months = $q;
}
$finish_date = null;
$end_date = null;
if ($months > 0) {
$dt = new DateTime('now');
$dt->modify('+' . intval($months) . ' months');
$finish_date = $dt->format('Y-m-d H:i:s');
$end_date = $dt->format('Y-m-d H:i:s');
} else {
// if no months specified, set to now
$finish_date = date('Y-m-d H:i:s');
$end_date = date('Y-m-d H:i:s');
}
// Check if finish_date column exists (use table prefix)
$finish_col_exists = false;
$col_check = mysqli_query($db, "SHOW COLUMNS FROM " . $table_prefix . "billing_orders LIKE 'finish_date'");
if ($col_check && mysqli_num_rows($col_check) > 0) $finish_col_exists = true;
// Perform update and log results. Use prepared statements when available and fallback to direct query on error.
$updated_rows = 0;
if ($finish_col_exists) {
$upd = $db->prepare("UPDATE " . $table_prefix . "billing_orders SET status = 'paid', finish_date = ? WHERE order_id = ? LIMIT 1");
if ($upd) {
$upd->bind_param('si', $finish_date, $orderId);
$ok = $upd->execute();
if (!$ok) site_log_warn('free_create_update_failed_prepare', ['error'=>$db->error, 'sql'=>'UPDATE with finish_date', 'order'=>$orderId]);
$updated_rows = $upd->affected_rows;
$upd->close();
} else {
// fallback
$safe_fd = mysqli_real_escape_string($db, $finish_date);
$q = "UPDATE " . $table_prefix . "billing_orders SET status = 'paid', finish_date = '$safe_fd' WHERE order_id = " . intval($orderId) . " LIMIT 1";
$resq = mysqli_query($db, $q);
if (!$resq) site_log_warn('free_create_update_failed_query', ['error'=>mysqli_error($db), 'sql'=>$q]);
else $updated_rows = mysqli_affected_rows($db);
}
} else {
$upd = $db->prepare("UPDATE " . $table_prefix . "billing_orders SET status = 'paid' WHERE order_id = ? LIMIT 1");
if ($upd) {
$upd->bind_param('i', $orderId);
$ok = $upd->execute();
if (!$ok) site_log_warn('free_create_update_failed_prepare', ['error'=>$db->error, 'sql'=>'UPDATE status only', 'order'=>$orderId]);
$updated_rows = $upd->affected_rows;
$upd->close();
} else {
$q = "UPDATE " . $table_prefix . "billing_orders SET status = 'paid' WHERE order_id = " . intval($orderId) . " LIMIT 1";
$resq = mysqli_query($db, $q);
if (!$resq) site_log_warn('free_create_update_failed_query', ['error'=>mysqli_error($db), 'sql'=>$q]);
else $updated_rows = mysqli_affected_rows($db);
// INSERT new order record (invoice->order after payment)
$esc_service_id = intval($service_id);
$esc_home_name = mysqli_real_escape_string($db, $home_name);
$esc_ip = intval($ip);
$esc_max_players = intval($max_players);
$esc_qty = intval($order_qty);
$esc_inv_dur = mysqli_real_escape_string($db, $order_invoice_duration);
$esc_price = floatval($order_price);
$esc_rc_pass = mysqli_real_escape_string($db, $remote_control_password);
$esc_ftp_pass = mysqli_real_escape_string($db, $ftp_password);
$esc_user_id = intval($owner_id);
$esc_end_date = mysqli_real_escape_string($db, $end_date);
$insert_sql = "INSERT INTO " . $table_prefix . "billing_orders
(user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, end_date, payment_txid, paid_ts)
VALUES
({$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip}, {$esc_max_players}, {$esc_qty}, '{$esc_inv_dur}', {$esc_price}, '{$esc_rc_pass}', '{$esc_ftp_pass}', 'paid', '{$esc_end_date}', 'FREE-{$orderId}', NOW())";
$insert_res = mysqli_query($db, $insert_sql);
$new_order_id = 0;
if ($insert_res) {
$new_order_id = mysqli_insert_id($db);
// Update invoice with the new order_id
$upd_inv_order = $db->prepare("UPDATE " . $table_prefix . "billing_invoices SET order_id = ? WHERE invoice_id = ? LIMIT 1");
if ($upd_inv_order) {
$upd_inv_order->bind_param('ii', $new_order_id, $orderId);
$upd_inv_order->execute();
$upd_inv_order->close();
}
}
// write audit log (include finish_date if set)
site_log_info('free_create', ['actor'=>$actor_id, 'role'=>$actor_role, 'action'=>$reason, 'order'=>$orderId, 'owner'=>$owner_id, 'price'=>$order_price, 'prev_status'=>$prev_status, 'finish_date'=>$finish_date ?? '', 'updated_rows'=>$updated_rows]);
// write audit log (include end_date if set)
site_log_info('free_create', ['actor'=>$actor_id, 'role'=>$actor_role, 'action'=>$reason, 'invoice'=>$orderId, 'new_order'=>$new_order_id, 'owner'=>$owner_id, 'price'=>$order_price, 'prev_status'=>$prev_status, 'end_date'=>$end_date ?? '']);
// write a simulated webhook file (same behavior as previous admin flow)
$dataDir = (isset($SITE_DATA_DIR) && $SITE_DATA_DIR) ? $SITE_DATA_DIR : realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'data';
@ -241,33 +242,32 @@ if (isset($_SESSION['website_user_role']) && !empty($_SESSION['website_user_role
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_single'])) {
$order_id = intval($_POST['delete_single']);
if ($order_id > 0) {
// First, check if the status is 'renew'
$stmt = $db->prepare("SELECT status FROM ogp_billing_orders WHERE order_id = ? AND user_id = ?");
$stmt->bind_param("ii", $order_id, $user_id);
$invoice_id = intval($_POST['delete_single']);
if ($invoice_id > 0) {
// Check if this invoice is linked to an order (renewal case)
$stmt = $db->prepare("SELECT order_id FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?");
$stmt->bind_param("ii", $invoice_id, $user_id);
$stmt->execute();
$stmt->bind_result($status);
if ($stmt->fetch() && strtolower($status) === 'renew') {
$stmt->close();
// If user removes a renewal from their cart, revert the order back to 'installed'
$update = $db->prepare("UPDATE ogp_billing_orders SET status = 'installed' WHERE order_id = ? AND user_id = ?");
$update->bind_param("ii", $order_id, $user_id);
$update->execute();
// Log revert action to panel logger
if (isset($db) && method_exists($db, 'logger')) {
$db->logger("USER-CART: User " . intval($user_id) . " reverted renew for order " . intval($order_id));
}
$update->close();
} else {
$stmt->close();
// Otherwise, delete the order
$delete = $db->prepare("DELETE FROM ogp_billing_orders WHERE order_id = ? AND user_id = ?");
$delete->bind_param("ii", $order_id, $user_id);
$stmt->bind_result($linked_order_id);
$found = $stmt->fetch();
$stmt->close();
if ($found && $linked_order_id > 0) {
// This is a renewal invoice - just delete the invoice, keep the order
$delete = $db->prepare("DELETE FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?");
$delete->bind_param("ii", $invoice_id, $user_id);
$delete->execute();
// Log deletion to panel logger
if (isset($db) && method_exists($db, 'logger')) {
$db->logger("USER-CART: User " . intval($user_id) . " deleted order " . intval($order_id));
$db->logger("USER-CART: User " . intval($user_id) . " deleted renewal invoice " . intval($invoice_id));
}
$delete->close();
} else {
// New order invoice - delete it
$delete = $db->prepare("DELETE FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?");
$delete->bind_param("ii", $invoice_id, $user_id);
$delete->execute();
if (isset($db) && method_exists($db, 'logger')) {
$db->logger("USER-CART: User " . intval($user_id) . " deleted invoice " . intval($invoice_id));
}
$delete->close();
}
@ -275,11 +275,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_single'])) {
}
if ($db){
$carts = $db->query("SELECT * FROM ogp_billing_orders AS cart
WHERE (status = 'in-cart' OR status = 'renew') AND user_id = " . $user_id . " ORDER BY order_id ASC");
$carts = $db->query("SELECT * FROM ogp_billing_invoices AS cart
WHERE status = 'due' AND user_id = " . $user_id . " ORDER BY invoice_id ASC");
}
?>
@ -311,38 +308,38 @@ if ($db){
if (isset($carts) && $carts instanceof mysqli_result && $carts->num_rows > 0) {
while ($row = $carts->fetch_assoc()) {
?>
<tr data-cart-id="<?php echo htmlspecialchars($row['order_id']); ?>">
<tr data-cart-id="<?php echo htmlspecialchars($row['invoice_id']); ?>">
<td>
<form method="post" action="" class="inline-form">
<button type="submit" name="delete_single" value="<?php echo htmlspecialchars($row['order_id']); ?>" class="btn-square text-danger">
<button type="submit" name="delete_single" value="<?php echo htmlspecialchars($row['invoice_id']); ?>" class="btn-square text-danger">

</button>
</form>
</td>
<td><?php echo htmlspecialchars($row['home_id']); ?></td>
<td><?php echo htmlspecialchars($row['invoice_id']); ?></td>
<td><?php echo htmlspecialchars($row['home_name']); ?></td>
<td><?php echo htmlspecialchars($row['ip']); ?></td>
<td><?php echo htmlspecialchars($row['max_players']); ?></td>
<td>$<?php echo number_format($row['price'], 2); ?></td>
<td>$<?php echo number_format($row['amount'], 2); ?></td>
<td><?php echo htmlspecialchars($row['qty']); ?></td>
<?php $rowtotal = $row['price'] * $row['qty'] * $row['max_players'];?>
<?php $rowtotal = $row['amount'] * $row['qty'] * $row['max_players'];?>
<?php
// Build invoice and line item structures used later when creating PayPal order
if (!isset($invoice) || !is_array($invoice)) $invoice = [];
$invoice[] = [
'serverID' => 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'])
];
?>
<?php
// Use the previously resolved $is_admin (computed once above)
$is_free = ((float)$row['price'] == 0.0);
$is_free = ((float)$row['amount'] == 0.0);
?>
<?php if ($is_admin || $is_free): ?>
<td>
<form method="post" action="" class="inline-form">
<input type="hidden" name="create_free_for" value="<?php echo (int)$row['order_id']; ?>">
<input type="hidden" name="create_free_for" value="<?php echo (int)$row['invoice_id']; ?>">
<button type="submit" class="gsw-btn"><?php echo $is_admin ? 'Create (Free)' : 'Claim (Free)'; ?></button>
</form>
<?php if ($is_admin): ?>
@ -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';

View file

@ -0,0 +1,76 @@
<?php
/**
* Check ogp_billing_invoices table structure
*/
require_once('../../includes/config.inc.php');
require_once('../../includes/database_mysqli.php');
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
if (!$db) {
die("Database connection failed: " . mysqli_connect_error());
}
echo "<h2>ogp_billing_invoices Table Structure</h2>\n";
$result = mysqli_query($db, "DESCRIBE ogp_billing_invoices");
if (!$result) {
die("Table doesn't exist or query failed: " . mysqli_error($db));
}
echo "<table border='1' style='border-collapse: collapse;'>\n";
echo "<tr><th>Field</th><th>Type</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th></tr>\n";
while ($row = mysqli_fetch_assoc($result)) {
echo "<tr>";
echo "<td>{$row['Field']}</td>";
echo "<td>{$row['Type']}</td>";
echo "<td>{$row['Null']}</td>";
echo "<td>{$row['Key']}</td>";
echo "<td>" . ($row['Default'] ?? 'NULL') . "</td>";
echo "<td>{$row['Extra']}</td>";
echo "</tr>\n";
}
echo "</table>\n";
// Count existing invoices
$count_result = mysqli_query($db, "SELECT COUNT(*) as cnt FROM ogp_billing_invoices");
$count = mysqli_fetch_assoc($count_result);
echo "<p><strong>Total invoices in table:</strong> {$count['cnt']}</p>\n";
// Show last 5 invoices
echo "<h2>Last 5 Invoices</h2>\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 "<table border='1' style='border-collapse: collapse;'>\n";
echo "<tr>";
$first = true;
while ($row = mysqli_fetch_assoc($last_result)) {
if ($first) {
foreach (array_keys($row) as $col) {
echo "<th>{$col}</th>";
}
echo "</tr>\n";
$first = false;
mysqli_data_seek($last_result, 0);
}
}
while ($row = mysqli_fetch_assoc($last_result)) {
echo "<tr>";
foreach ($row as $val) {
echo "<td>" . htmlspecialchars($val ?? 'NULL') . "</td>";
}
echo "</tr>\n";
}
echo "</table>\n";
} else {
echo "<p>No invoices found.</p>\n";
}
mysqli_close($db);
?>

View file

@ -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;

View file

@ -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

View file

@ -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'])) .
".<br><br>A renewal invoice has been created. Please log in to your account and pay the invoice to continue your service." .
"<br><br>Amount Due: $" . number_format($order['price'], 2) .
"<br>Due Date: " . date('F j, Y', strtotime($order['end_date'])) .
"<br><br>Thank you for your business!<br>";
$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." .
"<br><br>Your server has been stopped and will be permanently deleted in 7 days if payment is not received." .
"<br><br>Please log in to your account and pay your outstanding invoice to restore your server." .
"<br><br>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." .
"<br><br>The server was suspended 7 days ago due to non-payment and has now been removed." .
"<br><br>If this was an error and you contact us immediately, we may be able to restore your server from backups." .
"<br><br>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))
{

View file

@ -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;

View file

@ -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;

View file

@ -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`;"
);
?>

View file

@ -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 = [
</div>
<div class="server-detail">
<span class="server-detail-label">Expires:</span>
<span class="server-detail-value"><?php echo !empty($server['finish_date']) && $server['finish_date'] != '0' ? date('M d, Y', strtotime($server['finish_date'])) : 'N/A'; ?></span>
<span class="server-detail-value"><?php echo !empty($server['end_date']) && $server['end_date'] != '0' ? date('M d, Y', strtotime($server['end_date'])) : 'N/A'; ?></span>
</div>
</div>
<div class="server-actions">

View file

@ -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

View file

@ -0,0 +1,51 @@
<?php
/**
* Payment Cancelled Page
* User lands here if they cancel the PayPal payment
*/
session_start();
require_once(__DIR__ . '/includes/header.php');
require_once(__DIR__ . '/includes/config.inc.php');
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Cancelled - Game Server Panel</title>
<link rel="stylesheet" href="includes/style.css">
</head>
<body>
<div class="container" style="max-width: 800px; margin: 40px auto; padding: 20px;">
<div class="warning-box" style="background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h1 style="margin-top: 0;">Payment Cancelled</h1>
<p>Your payment was cancelled. No charges have been made to your account.</p>
<?php if ($invoice_ref): ?>
<p><strong>Invoice Reference:</strong> <?php echo htmlspecialchars($invoice_ref); ?></p>
<?php endif; ?>
</div>
<div class="info-box" style="background: #f8f9fa; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h2>What would you like to do?</h2>
<ul>
<li><strong>Return to Cart:</strong> Your items are still in your cart. You can complete the payment anytime.</li>
<li><strong>Continue Shopping:</strong> Browse our game server options and add more to your cart.</li>
<li><strong>Need Help?:</strong> Contact our support team if you encountered any issues during checkout.</li>
</ul>
</div>
<div class="actions" style="margin-top: 30px; text-align: center;">
<a href="cart.php" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">Return to Cart</a>
<a href="order.php" style="display: inline-block; padding: 12px 24px; background: #28a745; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">Continue Shopping</a>
<a href="support.php" style="display: inline-block; padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 5px;">Contact Support</a>
</div>
</div>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>

View file

@ -1,54 +1,94 @@
<?php
// Helper to process a persisted payment record and mark orders paid in panel DB.
// Usage: require_once(__DIR__ . '/payment_success.php'); process_payment_record($record);
/**
* Payment Success Page
* User lands here after successful PayPal payment
*/
function process_payment_record(array $record) {
// Minimal validation
$invoice = $record['invoice'] ?? '';
$custom = $record['custom'] ?? '';
$txid = $record['resource_id'] ?? '';
$ts = $record['ts'] ?? date('c');
session_start();
require_once(__DIR__ . '/includes/header.php');
require_once(__DIR__ . '/includes/config.inc.php');
require_once(__DIR__ . '/../../includes/database_mysqli.php');
// Attempt DB update using site DB config
// This file lives in _website/, config is in includes/config.inc.php
$cfg = __DIR__ . '/includes/config.inc.php';
if (!is_file($cfg)) {
error_log('[payment_success] missing config: ' . $cfg);
return false;
}
require_once($cfg);
// include site logging helper if available
if (is_file(__DIR__ . '/includes/log.php')) require_once(__DIR__ . '/includes/log.php');
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
$user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
// Use variables from config.inc.php: $db_host, $db_user, $db_pass, $db_name
$db = @mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if (!$db) {
if (function_exists('site_log_error')) site_log_error('payment_success_db_connect_failed', ['error'=>mysqli_connect_error()]);
else error_log('[payment_success] DB connect failed: ' . mysqli_connect_error());
return false;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Successful - Game Server Panel</title>
<link rel="stylesheet" href="includes/style.css">
</head>
<body>
// 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) . ' = ?';
<div class="container" style="max-width: 800px; margin: 40px auto; padding: 20px;">
<div class="success-box" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h1 style="margin-top: 0;"> Payment Successful!</h1>
<p>Thank you for your purchase. Your payment has been received and is being processed.</p>
<?php if ($invoice_ref): ?>
<p><strong>Invoice Reference:</strong> <?php echo htmlspecialchars($invoice_ref); ?></p>
<?php endif; ?>
</div>
<div class="info-box" style="background: #f8f9fa; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h2>What happens next?</h2>
<ol>
<li><strong>Payment Confirmation:</strong> Your payment has been captured by PayPal</li>
<li><strong>Order Creation:</strong> Your game server order has been created</li>
<li><strong>Server Provisioning:</strong> Your server will be provisioned automatically (this may take a few minutes)</li>
<li><strong>Email Notification:</strong> You'll receive an email with your server details and login credentials</li>
</ol>
</div>
<?php
// Show user's recent orders
if ($user_id > 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 '<div class="orders-box" style="background: #fff; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px;">';
echo '<h2>Your Recent Orders</h2>';
echo '<table style="width: 100%; border-collapse: collapse;">';
echo '<thead><tr style="background: #f8f9fa;">';
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Order ID</th>';
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Server</th>';
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Status</th>';
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Date</th>';
echo '<th style="padding: 10px; text-align: right; border-bottom: 2px solid #dee2e6;">Price</th>';
echo '</tr></thead><tbody>';
while ($order = mysqli_fetch_assoc($result)) {
$statusColor = $order['status'] === 'paid' ? '#28a745' : '#6c757d';
echo '<tr style="border-bottom: 1px solid #dee2e6;">';
echo '<td style="padding: 10px;">#' . htmlspecialchars($order['order_id']) . '</td>';
echo '<td style="padding: 10px;">' . htmlspecialchars($order['home_name']) . '</td>';
echo '<td style="padding: 10px;"><span style="color: ' . $statusColor . '; font-weight: bold;">' . htmlspecialchars(ucfirst($order['status'])) . '</span></td>';
echo '<td style="padding: 10px;">' . htmlspecialchars($order['order_date']) . '</td>';
echo '<td style="padding: 10px; text-align: right;">$' . htmlspecialchars(number_format($order['price'], 2)) . '</td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div>';
}
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;
<div class="actions" style="margin-top: 30px; text-align: center;">
<a href="my_account.php" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">View My Servers</a>
<a href="order.php" style="display: inline-block; padding: 12px 24px; background: #28a745; color: white; text-decoration: none; border-radius: 5px;">Order Another Server</a>
</div>
</div>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>
$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) {