diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3b8f0c90..50fa810e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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 +My Account +Browse Servers + +// Form actions +
+``` + +### 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'); +My Account +``` + +### 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). diff --git a/modules/billing/COLUMN_RENAME_SUMMARY.md b/modules/billing/COLUMN_RENAME_SUMMARY.md new file mode 100644 index 00000000..0bce43fa --- /dev/null +++ b/modules/billing/COLUMN_RENAME_SUMMARY.md @@ -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 diff --git a/modules/billing/INVOICE_FIRST_FLOW.md b/modules/billing/INVOICE_FIRST_FLOW.md new file mode 100644 index 00000000..1fb57d52 --- /dev/null +++ b/modules/billing/INVOICE_FIRST_FLOW.md @@ -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'; +``` diff --git a/modules/billing/INVOICE_SYSTEM.md b/modules/billing/INVOICE_SYSTEM.md new file mode 100644 index 00000000..f60daf9f --- /dev/null +++ b/modules/billing/INVOICE_SYSTEM.md @@ -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 diff --git a/modules/billing/MIGRATION_SUMMARY.md b/modules/billing/MIGRATION_SUMMARY.md new file mode 100644 index 00000000..b748eda3 --- /dev/null +++ b/modules/billing/MIGRATION_SUMMARY.md @@ -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 diff --git a/modules/billing/add_service_id_column.sql b/modules/billing/add_service_id_column.sql new file mode 100644 index 00000000..3a1a8c71 --- /dev/null +++ b/modules/billing/add_service_id_column.sql @@ -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`); diff --git a/modules/billing/add_to_cart.php b/modules/billing/add_to_cart.php index 0a2dc514..a68b0f56 100644 --- a/modules/billing/add_to_cart.php +++ b/modules/billing/add_to_cart.php @@ -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 diff --git a/modules/billing/admin_invoices.php b/modules/billing/admin_invoices.php index a06f3018..3bb2143f 100644 --- a/modules/billing/admin_invoices.php +++ b/modules/billing/admin_invoices.php @@ -96,7 +96,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } - + diff --git a/modules/billing/api/capture_order.php b/modules/billing/api/capture_order.php index 919aab3a..d2bf8896 100644 --- a/modules/billing/api/capture_order.php +++ b/modules/billing/api/capture_order.php @@ -1,5 +1,6 @@ '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); ?> diff --git a/modules/billing/cart.php b/modules/billing/cart.php index d639f02f..9f72aa1b 100644 --- a/modules/billing/cart.php +++ b/modules/billing/cart.php @@ -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()) { ?> - + -
- + - $ + $ - + isset($row['home_id']) ? (string)$row['home_id'] : ('order'.$row['order_id']), + 'serverID' => 'invoice-' . $row['invoice_id'], 'amount' => number_format($rowtotal, 2, '.', ''), - 'order_id' => intval($row['order_id']) + 'invoice_id' => intval($row['invoice_id']) ]; ?>
- +
@@ -428,12 +425,14 @@ if (is_array($invoice) && count($invoice) === 1 && !empty($invoice[0]['order_id' $description = 'Game server order (' . count($lineItems) . ' item' . (count($lineItems)===1?'': 's') . ')'; // URLs -// Define the site base URL -$siteBaseUrl = 'http://gameservers.world/modules/billing'; +// Define the site base URL - detect protocol and host dynamically +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; +$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; +$siteBase = $protocol . $host; -// Generate absolute URLs for return and cancel -$returnUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId); -$cancelUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1'; +// Return URLs are root-relative (website will be deployed at root, not modules/billing) +$returnUrl = $siteBase . '/payment_success.php?invoice=' . urlencode($invoiceId); +$cancelUrl = $siteBase . '/payment_cancel.php?invoice=' . urlencode($invoiceId); // API base (relative) - point to billing module API endpoints $apiBase = 'api'; diff --git a/modules/billing/check_table.php b/modules/billing/check_table.php new file mode 100644 index 00000000..94e5d9bd --- /dev/null +++ b/modules/billing/check_table.php @@ -0,0 +1,76 @@ +ogp_billing_invoices Table Structure\n"; + +$result = mysqli_query($db, "DESCRIBE ogp_billing_invoices"); + +if (!$result) { + die("Table doesn't exist or query failed: " . mysqli_error($db)); +} + +echo "\n"; +echo "\n"; + +while ($row = mysqli_fetch_assoc($result)) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo "\n"; +} + +echo "
FieldTypeNullKeyDefaultExtra
{$row['Field']}{$row['Type']}{$row['Null']}{$row['Key']}" . ($row['Default'] ?? 'NULL') . "{$row['Extra']}
\n"; + +// Count existing invoices +$count_result = mysqli_query($db, "SELECT COUNT(*) as cnt FROM ogp_billing_invoices"); +$count = mysqli_fetch_assoc($count_result); +echo "

Total invoices in table: {$count['cnt']}

\n"; + +// Show last 5 invoices +echo "

Last 5 Invoices

\n"; +$last_result = mysqli_query($db, "SELECT * FROM ogp_billing_invoices ORDER BY invoice_id DESC LIMIT 5"); + +if (mysqli_num_rows($last_result) > 0) { + echo "\n"; + echo ""; + $first = true; + while ($row = mysqli_fetch_assoc($last_result)) { + if ($first) { + foreach (array_keys($row) as $col) { + echo ""; + } + echo "\n"; + $first = false; + mysqli_data_seek($last_result, 0); + } + } + + while ($row = mysqli_fetch_assoc($last_result)) { + echo ""; + foreach ($row as $val) { + echo ""; + } + echo "\n"; + } + echo "
{$col}
" . htmlspecialchars($val ?? 'NULL') . "
\n"; +} else { + echo "

No invoices found.

\n"; +} + +mysqli_close($db); +?> diff --git a/modules/billing/create_invoices_table.sql b/modules/billing/create_invoices_table.sql new file mode 100644 index 00000000..29a615f6 --- /dev/null +++ b/modules/billing/create_invoices_table.sql @@ -0,0 +1,33 @@ +-- Create billing_invoices table for invoice-first flow +-- Run this SQL to enable the new billing system + +CREATE TABLE IF NOT EXISTS `ogp_billing_invoices` ( + `invoice_id` INT(11) NOT NULL AUTO_INCREMENT, + `order_id` INT(11) NOT NULL DEFAULT 0, + `user_id` INT(11) NOT NULL, + `service_id` INT(11) NOT NULL, + `home_name` VARCHAR(255) NOT NULL DEFAULT '', + `ip` INT(11) NOT NULL DEFAULT 0, + `max_players` INT(11) NOT NULL DEFAULT 0, + `remote_control_password` VARCHAR(255) NULL, + `ftp_password` VARCHAR(255) NULL, + `customer_name` VARCHAR(255) NOT NULL DEFAULT '', + `customer_email` VARCHAR(255) NOT NULL DEFAULT '', + `amount` FLOAT(15,2) NOT NULL DEFAULT 0, + `currency` VARCHAR(3) NOT NULL DEFAULT 'USD', + `status` VARCHAR(16) NOT NULL DEFAULT 'due', + `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `due_date` DATETIME NULL, + `paid_date` DATETIME NULL, + `payment_txid` VARCHAR(255) NULL, + `payment_method` VARCHAR(50) NULL, + `description` VARCHAR(500) NOT NULL DEFAULT '', + `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month', + `qty` INT(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`invoice_id`), + KEY `order_id` (`order_id`), + KEY `user_id` (`user_id`), + KEY `status` (`status`), + KEY `due_date` (`due_date`), + KEY `service_id` (`service_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php index 78ab285e..1fa84348 100644 --- a/modules/billing/create_servers.php +++ b/modules/billing/create_servers.php @@ -241,7 +241,7 @@ function exec_ogp_module() //PANEL LOG $db->logger( "CREATED NEW SERVER " . $home_id); // SEND EMAIL to new server only - if($order['finish_date'] == 0){ + if($order['end_date'] == 0){ $settings = $db->getSettings(); $subject = "New Gameserver installed at " . $settings['panel_name']; $email = $db->resultQuery(" SELECT DISTINCT users_email @@ -292,42 +292,42 @@ function exec_ogp_module() // 'invoiced' - invoice created for renewal // 'suspended' - server suspended for non-payment // 'deleted' - server deleted after extended suspension - //finish_date the server will be suspended - //in cron_shop the finish_date is used to delete the server + //end_date the server will be suspended + //in cron_shop the end_date is used to delete the server //several days after being suspended if ($order['invoice_duration'] == "day") { - if($order['finish_date'] == 0){ - $finish_date = strtotime('+'.$order['qty'].' day'); + if($order['end_date'] == 0){ + $end_date = strtotime('+'.$order['qty'].' day'); } else{ //this is a renewel, start from end of previous order - $finish_date = strtotime('+'.$order['qty'].' day',$order['finish_date']); + $end_date = strtotime('+'.$order['qty'].' day',$order['end_date']); } } elseif ($order['invoice_duration'] == "month") { // this is a new order - if($order['finish_date'] == 0){ - $finish_date = strtotime('+'.$order['qty'].' month'); + if($order['end_date'] == 0){ + $end_date = strtotime('+'.$order['qty'].' month'); } else{ //this is a renewel, start from end of previous order - $finish_date = strtotime('+'.$order['qty'].' month',$order['finish_date']); + $end_date = strtotime('+'.$order['qty'].' month',$order['end_date']); } } elseif ($order['invoice_duration'] == "year") { // this is a new order - if($order['finish_date'] == 0){ - $finish_date = strtotime('+'.$order['qty'].' year'); + if($order['end_date'] == 0){ + $end_date = strtotime('+'.$order['qty'].' year'); } else{ //this is a renewel, start from end of previous order - $finish_date = strtotime('+'.$order['qty'].' year',$order['finish_date']); + $end_date = strtotime('+'.$order['qty'].' year',$order['end_date']); } @@ -339,7 +339,7 @@ function exec_ogp_module() // set the order expiration $db->query("UPDATE OGP_DB_PREFIXbilling_orders - SET finish_date='" . $db->realEscapeSingle($finish_date) . "' + SET end_date='" . $db->realEscapeSingle($end_date) . "' WHERE order_id=".$db->realEscapeSingle($order_id)); // Save home id created by this order diff --git a/modules/billing/cron-shop.php b/modules/billing/cron-shop.php index b9103ac2..523c7d10 100644 --- a/modules/billing/cron-shop.php +++ b/modules/billing/cron-shop.php @@ -21,18 +21,20 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * + * INVOICE-BASED BILLING SYSTEM + * ============================= * - -Complete Status Flow: -in-cart - User added to cart, not yet paid -renew - Renewal order in cart -paid - Payment received, awaiting server creation -installed - ✅ Active/Running (server provisioned and operational) -invoiced - Invoice generated, payment due (7 days before expiration) -suspended - Server stopped, payment overdue -deleted - Server permanently removed (7 days after suspension) -expired - Order has expired -unknown - Error/undefined state + * Status Flow for billing_orders: + * - in-cart: User added to cart, not yet paid + * - paid: Payment received, awaiting server provisioning + * - installed: ✅ Active/Running (server provisioned and operational) + * - suspended: Server stopped, payment overdue (has unpaid invoice) + * - deleted: Server permanently removed + * - expired: Order has expired + * + * Invoice Status (billing_invoices): + * - unpaid: Invoice created, awaiting payment + * - paid: Invoice paid, service extended */ chdir(realpath(dirname(__FILE__))); /* Change to the current file path */ @@ -41,7 +43,7 @@ chdir("../.."); /* Base path to ogp web files */ error_reporting(E_ALL); // Path definitions define("CONFIG_FILE","includes/config.inc.php"); -//Requiere +//Require require_once("includes/functions.php"); require_once("includes/helpers.php"); require_once("includes/html_functions.php"); @@ -55,23 +57,235 @@ $panel_settings = $db->getSettings(); if( isset($panel_settings['time_zone']) && $panel_settings['time_zone'] != "" ) date_default_timezone_set($panel_settings['time_zone']); +// Date calculations +$today = time(); +$invoice_date = strtotime('+ 7 days'); // Create invoice 7 days before expiration +$suspend_date = $today; // Suspend immediately when overdue +$removal_date = strtotime('- 7 days'); // Remove 7 days after suspension +$rundate = date('Y-m-d H:i:s', $today); -//these dates are configured in the Shop Settings page -$today=time(); -$invoice_date = strtotime('+ 7 days'); //this many days until the finish_date -$suspend_date = $today; //suspend when overdue -//final date is 10th, we need to remove on 17th, so final date is > removal_date -$removal_date = strtotime('- 7 days'); //finish_date is passed 7 days ago -$rundate = date('d/M/y G:i',$today); -$db->logger("AUTO-CLEAN: Server Cleanup running at ".$rundate); +$db->logger("BILLING-CRON: Server lifecycle automation running at " . $rundate); + +// ================================================================================== +// STEP 1: CREATE RENEWAL INVOICES FOR SERVERS EXPIRING IN 7 DAYS +// ================================================================================== +// Find all ACTIVE servers (installed) that expire within 7 days and don't have an unpaid invoice +$upcoming_expirations = $db->resultQuery(" + SELECT o.*, u.users_email, u.users_fname, u.users_lname + FROM " . $table_prefix . "billing_orders o + LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id + WHERE o.status = 'installed' + AND o.end_date IS NOT NULL + AND UNIX_TIMESTAMP(o.end_date) < {$invoice_date} + AND UNIX_TIMESTAMP(o.end_date) > {$today} + AND NOT EXISTS ( + SELECT 1 FROM " . $table_prefix . "billing_invoices i + WHERE i.order_id = o.order_id AND i.status = 'unpaid' + ) +"); + +if (is_array($upcoming_expirations)) { + foreach ($upcoming_expirations as $order) { + $user_id = $order['user_id']; + $order_id = $order['order_id']; + $home_id = $order['home_id']; + $customer_name = trim(($order['users_fname'] ?? '') . ' ' . ($order['users_lname'] ?? '')); + $customer_email = $order['users_email'] ?? ''; + + // Create renewal invoice + $invoice_desc = "Renewal for " . $order['home_name']; + $due_date = date('Y-m-d H:i:s', strtotime($order['end_date'])); + + $db->query("INSERT INTO " . $table_prefix . "billing_invoices + (order_id, user_id, customer_name, customer_email, amount, currency, status, + invoice_date, due_date, description, invoice_duration, qty) + VALUES ( + {$order_id}, + {$user_id}, + '" . $db->realEscapeSingle($customer_name) . "', + '" . $db->realEscapeSingle($customer_email) . "', + " . floatval($order['price']) . ", + 'USD', + 'unpaid', + NOW(), + '" . $db->realEscapeSingle($due_date) . "', + '" . $db->realEscapeSingle($invoice_desc) . "', + '" . $db->realEscapeSingle($order['invoice_duration']) . "', + " . intval($order['qty']) . " + )"); + + // Send renewal notice email + $settings = $db->getSettings(); + $subject = "Renewal Invoice for " . $order['home_name'] . " - " . $panel_settings['panel_name']; + $message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) will expire on " . + date('F j, Y', strtotime($order['end_date'])) . + ".

A renewal invoice has been created. Please log in to your account and pay the invoice to continue your service." . + "

Amount Due: $" . number_format($order['price'], 2) . + "
Due Date: " . date('F j, Y', strtotime($order['end_date'])) . + "

Thank you for your business!
"; + + $mail = mymail($customer_email, $subject, $message, $settings); + + $db->logger("BILLING-CRON: Created renewal invoice for order {$order_id}, home {$home_id}"); + + if (!$mail) { + $db->logger("BILLING-CRON: Email FAILED - Renewal invoice for order {$order_id}"); + } + } +} + +// ================================================================================== +// STEP 2: SUSPEND SERVERS THAT ARE EXPIRED AND HAVE UNPAID INVOICES +// ================================================================================== +// Find servers that: +// - Are currently installed (active) +// - Have passed their end_date +// - Have at least one unpaid invoice +$servers_to_suspend = $db->resultQuery(" + SELECT DISTINCT o.*, u.users_email + FROM " . $table_prefix . "billing_orders o + LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id + INNER JOIN " . $table_prefix . "billing_invoices i ON o.order_id = i.order_id + WHERE o.status = 'installed' + AND o.end_date IS NOT NULL + AND UNIX_TIMESTAMP(o.end_date) < {$suspend_date} + AND i.status = 'unpaid' +"); + +if (is_array($servers_to_suspend)) { + foreach ($servers_to_suspend as $order) { + $user_id = $order['user_id']; + $home_id = $order['home_id']; + $order_id = $order['order_id']; + + // Get home and server info + $home_info = $db->getGameHomeWithoutMods($home_id); + if (!$home_info) { + $db->logger("BILLING-CRON: WARNING - Home {$home_id} not found for order {$order_id}, marking suspended anyway"); + $db->query("UPDATE " . $table_prefix . "billing_orders SET status='suspended' WHERE order_id={$order_id}"); + continue; + } + + $server_info = $db->getRemoteServerById($home_info['remote_server_id']); + $remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'], + $server_info['encryption_key'], $server_info['timeout']); + + // Disable FTP + $ftp_login = isset($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id; + $remote->ftp_mgr("userdel", $ftp_login); + $db->changeFtpStatus('disabled', $home_id); + + // Stop the server + $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); + $control_type = isset($server_xml->control_protocol_type) ? $server_xml->control_protocol_type : ""; + $addresses = $db->getHomeIpPorts($home_id); + + foreach ($addresses as $address) { + $remote->remote_stop_server($home_id, $address['ip'], $address['port'], + $server_xml->control_protocol, $home_info['control_password'], + $control_type, $home_info['home_path']); + } + + // Unassign from user + $db->unassignHomeFrom("user", $user_id, $home_id); + + // Update order status + $db->query("UPDATE " . $table_prefix . "billing_orders SET status='suspended' WHERE order_id={$order_id}"); + + $db->logger("BILLING-CRON: SUSPENDED server {$home_id} for order {$order_id} due to unpaid invoice"); + + // Send suspension email + $settings = $db->getSettings(); + $subject = "Server Suspended - " . $order['home_name'] . " - " . $panel_settings['panel_name']; + $message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) has been suspended due to non-payment." . + "

Your server has been stopped and will be permanently deleted in 7 days if payment is not received." . + "

Please log in to your account and pay your outstanding invoice to restore your server." . + "

Thank you."; + + $mail = mymail($order['users_email'], $subject, $message, $settings); + + if (!$mail) { + $db->logger("BILLING-CRON: Email FAILED - Suspension notice for order {$order_id}"); + } + } +} + +// ================================================================================== +// STEP 3: DELETE SERVERS THAT HAVE BEEN SUSPENDED FOR 7+ DAYS +// ================================================================================== +// Find servers that: +// - Are currently suspended +// - Have been suspended for at least 7 days (end_date + 7 days has passed) +// - Still have unpaid invoices +$servers_to_delete = $db->resultQuery(" + SELECT DISTINCT o.*, u.users_email + FROM " . $table_prefix . "billing_orders o + LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id + INNER JOIN " . $table_prefix . "billing_invoices i ON o.order_id = i.order_id + WHERE o.status = 'suspended' + AND o.end_date IS NOT NULL + AND UNIX_TIMESTAMP(o.end_date) < {$removal_date} + AND i.status = 'unpaid' +"); + +if (is_array($servers_to_delete)) { + foreach ($servers_to_delete as $order) { + $user_id = $order['user_id']; + $home_id = $order['home_id']; + $order_id = $order['order_id']; + + // Get home and server info + $home_info = $db->getGameHomeWithoutMods($home_id); + if ($home_info) { + $server_info = $db->getRemoteServerById($home_info['remote_server_id']); + $remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'], + $server_info['encryption_key'], $server_info['timeout']); + + // Remove the game home from db + $db->deleteGameHome($home_id); + + // Remove the game home files from remote server + $remote->remove_home($home_info['home_path']); + + // Drop database and user if they exist + @$db->query("DROP USER 'server_" . $home_id . "'@'%'"); + @$db->query("DROP USER 'server_" . $home_id . "'@'localhost'"); + @$db->query("DROP DATABASE IF EXISTS server_" . $home_id); + } + + // Update order status and clear home_id + $db->query("UPDATE " . $table_prefix . "billing_orders + SET status='deleted', home_id='0' + WHERE order_id={$order_id}"); + + $db->logger("BILLING-CRON: DELETED server {$home_id} for order {$order_id} after 7 days suspended"); + + // Send deletion email + $settings = $db->getSettings(); + $subject = "Server Permanently Deleted - " . $order['home_name'] . " - " . $panel_settings['panel_name']; + $message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) has been permanently deleted." . + "

The server was suspended 7 days ago due to non-payment and has now been removed." . + "

If this was an error and you contact us immediately, we may be able to restore your server from backups." . + "

Thank you for being a customer. We hope to serve you again in the future."; + + $mail = mymail($order['users_email'], $subject, $message, $settings); + + if (!$mail) { + $db->logger("BILLING-CRON: Email FAILED - Deletion notice for order {$order_id}"); + } + } +} + +$db->logger("BILLING-CRON: Server lifecycle automation completed"); +?> -//THESE SERVERS HAVE REACHED THE DATE FOR INVOICE, FINISH_DATE - 7 (OR WHAT IS IN SETTINGS) +//THESE SERVERS HAVE REACHED THE DATE FOR INVOICE, END_DATE - 7 (OR WHAT IS IN SETTINGS) //SET STATUS 'invoiced' MEANING INVOICE SHOULD BE CREATED //LOOP THROUGH ALL SERVERS WITH STATUS = 'paid' OR 'installed' (ACTIVE) ----------------------------------------------------------- $user_homes = $db->resultQuery( "SELECT * FROM " . $table_prefix . "billing_orders - WHERE status IN ('paid', 'installed') AND finish_date <" . $invoice_date); + WHERE status IN ('paid', 'installed') AND end_date <" . $invoice_date); if (!is_array($user_homes)) { @@ -84,10 +298,10 @@ else // Developer note: // In future we may want to change the renewal/invoice strategy so that a // new order record is created for the renewal (leaving the original order - // intact) instead of mutating the existing order's status/finish_date. + // intact) instead of mutating the existing order's status/end_date. // Creating a separate renewal order gives a clearer, immutable purchase // history and simplifies auditing. For now this cron job continues to - // update the existing order (change status/finish_date) as implemented + // update the existing order (change status/end_date) as implemented // below. $user_id = $user_home['user_id']; @@ -119,12 +333,12 @@ else } } -//THESE ARE THE SERVERS THAT HAVE NOT BEEN PAID AND THE FINISH_DATE IS TODAY +//THESE ARE THE SERVERS THAT HAVE NOT BEEN PAID AND THE END_DATE IS TODAY //THESE SERVERS GET SUSPENDED //LOOP THROUGH ALL ORDERS WITH STATUS 'invoiced' OR 'in-cart' OR 'unknown' (INACTIVE OR INVOICED) $user_homes = $db->resultQuery( "SELECT * FROM " . $table_prefix . "billing_orders - WHERE status IN ('invoiced', 'in-cart', 'unknown') AND finish_date < ".$today); + WHERE status IN ('invoiced', 'in-cart', 'unknown') AND end_date < ".$today); if (!is_array($user_homes)) { @@ -178,7 +392,7 @@ else //set removed servers as 'deleted' $user_homes = $db->resultQuery( "SELECT * FROM " . $table_prefix . "billing_orders - WHERE status = 'suspended' AND finish_date < ".$removal_date ); + WHERE status = 'suspended' AND end_date < ".$removal_date ); if (!is_array($user_homes)) { diff --git a/modules/billing/fix_invoices_table_columns.sql b/modules/billing/fix_invoices_table_columns.sql new file mode 100644 index 00000000..f292b760 --- /dev/null +++ b/modules/billing/fix_invoices_table_columns.sql @@ -0,0 +1,204 @@ +-- Fix missing columns / indexes for ogp_billing_invoices +-- Safe script: checks information_schema and adds each missing column/index using prepared statements. +-- IMPORTANT: Run on the target database (use the panel DB). Make a backup before running. + +-- Use the current database +SET @db = DATABASE(); +SET @tbl = 'ogp_billing_invoices'; + +-- Helper: add a column if missing +-- Usage pattern below; repeated for every column we expect from module.php + +-- 1) service_id +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'service_id'; + +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 2) home_name +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'home_name'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `home_name` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `service_id`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 3) ip +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'ip'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `ip` INT(11) NOT NULL DEFAULT 0 AFTER `home_name`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 4) max_players +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'max_players'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `max_players` INT(11) NOT NULL DEFAULT 0 AFTER `ip`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 5) remote_control_password +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'remote_control_password'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `remote_control_password` VARCHAR(255) NULL AFTER `max_players`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 6) ftp_password +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'ftp_password'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `ftp_password` VARCHAR(255) NULL AFTER `remote_control_password`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 7) customer_name +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'customer_name'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `customer_name` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `ftp_password`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 8) customer_email +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'customer_email'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `customer_email` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `customer_name`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 9) amount +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'amount'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `amount` FLOAT(15,2) NOT NULL DEFAULT 0 AFTER `customer_email`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 10) currency +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'currency'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `currency` VARCHAR(3) NOT NULL DEFAULT ''USD'' AFTER `amount`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 11) status +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'status'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT ''due'' AFTER `currency`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 12) invoice_date +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'invoice_date'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 13) due_date +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'due_date'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `due_date` DATETIME NULL AFTER `invoice_date`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 14) paid_date +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'paid_date'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `paid_date` DATETIME NULL AFTER `due_date`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 15) payment_txid +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'payment_txid'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `payment_txid` VARCHAR(255) NULL AFTER `paid_date`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 16) payment_method +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'payment_method'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `payment_method` VARCHAR(50) NULL AFTER `payment_txid`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 17) description +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'description'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `description` VARCHAR(500) NOT NULL DEFAULT '''' AFTER `payment_method`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 18) invoice_duration +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'invoice_duration'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `invoice_duration` VARCHAR(16) NOT NULL DEFAULT ''month'' AFTER `description`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 19) qty +SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qty'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qty` INT(11) NOT NULL DEFAULT 1 AFTER `invoice_duration`'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- 20) indexes: service_id, order_id, user_id, status, due_date +-- Add index helper + +SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'service_id'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `service_id` (`service_id`)'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'order_id'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `order_id` (`order_id`)'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'user_id'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `user_id` (`user_id`)'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'status'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `status` (`status`)'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'due_date'; +IF @cnt = 0 THEN + SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `due_date` (`due_date`)'); + PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; +END IF; + +-- Done +SELECT 'done' as status; diff --git a/modules/billing/migration_to_invoices.sql b/modules/billing/migration_to_invoices.sql new file mode 100644 index 00000000..4123fd6c --- /dev/null +++ b/modules/billing/migration_to_invoices.sql @@ -0,0 +1,175 @@ +-- Migration Script: Billing System with Invoice Table +-- This script upgrades existing billing installations to the new invoice-based system +-- Run this ONCE on existing installations (not needed for fresh installs) +-- Compatible with MySQL 5.7+ and MariaDB 10.2+ + +-- Step 1: Add new columns to billing_orders (only if they don't exist) +SET @dbname = DATABASE(); +SET @tablename = 'ogp_billing_orders'; + +-- Add order_date column +SET @col_exists = 0; +SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'order_date'; +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `order_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`', + 'SELECT "Column order_date already exists" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Add payment_txid column +SET @col_exists = 0; +SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'payment_txid'; +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `payment_txid` VARCHAR(255) NULL AFTER `end_date`', + 'SELECT "Column payment_txid already exists" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Add paid_ts column +SET @col_exists = 0; +SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'paid_ts'; +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `paid_ts` DATETIME NULL AFTER `payment_txid`', + 'SELECT "Column paid_ts already exists" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Step 2: Modify existing columns to use proper data types +ALTER TABLE `ogp_billing_orders` + MODIFY COLUMN `status` VARCHAR(16) NOT NULL DEFAULT 'in-cart', + MODIFY COLUMN `remote_control_password` VARCHAR(255) NULL, + MODIFY COLUMN `ftp_password` VARCHAR(255) NULL; + +-- Convert end_date from VARCHAR to DATETIME (handle existing data) +-- First, update any '0' values to NULL +UPDATE `ogp_billing_orders` SET `end_date` = NULL WHERE `end_date` = '0' OR `end_date` = ''; + +-- Check current end_date type and convert if needed +SET @col_type = ''; +SELECT DATA_TYPE INTO @col_type FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'end_date'; + +SET @sql = IF(@col_type = 'varchar', + 'ALTER TABLE `ogp_billing_orders` MODIFY COLUMN `end_date` DATETIME NULL', + 'SELECT "Column end_date already DATETIME" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Step 3: Remove obsolete columns from billing_orders +SET @col_exists = 0; +SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'cart_id'; +SET @sql = IF(@col_exists > 0, + 'ALTER TABLE `ogp_billing_orders` DROP COLUMN `cart_id`', + 'SELECT "Column cart_id already removed" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @col_exists = 0; +SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'extended'; +SET @sql = IF(@col_exists > 0, + 'ALTER TABLE `ogp_billing_orders` DROP COLUMN `extended`', + 'SELECT "Column extended already removed" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Step 4: Add indexes to billing_orders for better performance +SET @index_exists = 0; +SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_user_id'; +SET @sql = IF(@index_exists = 0, + 'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_user_id` (`user_id`)', + 'SELECT "Index idx_user_id already exists" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @index_exists = 0; +SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_status'; +SET @sql = IF(@index_exists = 0, + 'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_status` (`status`)', + 'SELECT "Index idx_status already exists" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @index_exists = 0; +SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS +WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_home_id'; +SET @sql = IF(@index_exists = 0, + 'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_home_id` (`home_id`)', + 'SELECT "Index idx_home_id already exists" AS message'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + + +-- Step 5: Create the new billing_invoices table +CREATE TABLE IF NOT EXISTS `ogp_billing_invoices` ( + `invoice_id` INT(11) NOT NULL AUTO_INCREMENT, + `order_id` INT(11) NOT NULL, + `user_id` INT(11) NOT NULL, + `customer_name` VARCHAR(255) NOT NULL DEFAULT '', + `customer_email` VARCHAR(255) NOT NULL DEFAULT '', + `amount` FLOAT(15,2) NOT NULL DEFAULT 0, + `currency` VARCHAR(3) NOT NULL DEFAULT 'USD', + `status` VARCHAR(16) NOT NULL DEFAULT 'unpaid', + `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `due_date` DATETIME NULL, + `paid_date` DATETIME NULL, + `payment_txid` VARCHAR(255) NULL, + `payment_method` VARCHAR(50) NULL, + `description` VARCHAR(500) NOT NULL DEFAULT '', + `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month', + `qty` INT(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`invoice_id`), + KEY `order_id` (`order_id`), + KEY `user_id` (`user_id`), + KEY `status` (`status`), + KEY `due_date` (`due_date`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +-- Step 6: Migrate existing paid orders to create initial invoices +-- This creates a historical invoice for each paid/installed order +INSERT INTO `ogp_billing_invoices` + (`order_id`, `user_id`, `customer_name`, `customer_email`, `amount`, `currency`, `status`, `invoice_date`, `paid_date`, `payment_txid`, `description`, `invoice_duration`, `qty`) +SELECT + o.order_id, + o.user_id, + CONCAT(COALESCE(u.users_fname, ''), ' ', COALESCE(u.users_lname, '')) AS customer_name, + u.users_email AS customer_email, + o.price AS amount, + 'USD' AS currency, + 'paid' AS status, + COALESCE(o.order_date, NOW()) AS invoice_date, + COALESCE(o.paid_ts, o.order_date, NOW()) AS paid_date, + o.payment_txid, + CONCAT('Initial invoice for ', o.home_name) AS description, + o.invoice_duration, + o.qty +FROM `ogp_billing_orders` o +LEFT JOIN `ogp_users` u ON o.user_id = u.user_id +WHERE o.status IN ('paid', 'installed') + AND NOT EXISTS ( + SELECT 1 FROM `ogp_billing_invoices` i + WHERE i.order_id = o.order_id AND i.status = 'paid' + ); + +-- Step 7: Drop the obsolete billing_carts table (replaced by invoice system) +DROP TABLE IF EXISTS `ogp_billing_carts`; + +-- Step 8: Update billing_services charset for consistency +ALTER TABLE `ogp_billing_services` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +SELECT 'Migration completed successfully! Invoice-based billing system is now active.' AS Status; diff --git a/modules/billing/module.php b/modules/billing/module.php index f108aa9c..83426fd9 100644 --- a/modules/billing/module.php +++ b/modules/billing/module.php @@ -24,87 +24,97 @@ // Module general information $module_title = "billing"; -$module_version = "2.0"; -$db_version = 5; +$module_version = "3.0"; +$db_version = 1; $module_required = FALSE; // Navigation disabled - this is now a purely external module $module_menus = array(); $install_queries = array(); + +// Version 1: Current schema - clean install with all tables and required columns $install_queries[0] = array( - "DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_services`;", + // Billing Services - Available game server packages "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_services` ( - `service_id` int(11) NOT NULL auto_increment, - `home_cfg_id` int(11) NOT NULL, - `mod_cfg_id` int(11) NOT NULL, - `service_name` varchar(255) NOT NULL, - `remote_server_id` varchar(255) NOT NULL, - `out_of_stock` varchar(255) NOT NULL, - `slot_max_qty` int(11) NOT NULL, - `slot_min_qty` int(11) NOT NULL, - `price_daily` float(15,4) NOT NULL, - `price_monthly` float(15,4) NOT NULL, - `price_year` float(15,4) NOT NULL, - `description` varchar(1000) NOT NULL, - `img_url` varchar(255) NOT NULL, - `ftp` varchar(255) NOT NULL, - `install_method` varchar(255) NOT NULL, - `manual_url` varchar(255) NOT NULL, - `access_rights` varchar(255) NOT NULL, - `enabled` int(11) NOT NULL, - PRIMARY KEY (`service_id`) - ) ENGINE=MyISAM DEFAULT CHARSET=UTF8;", - - "DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_orders`;", + `service_id` INT(11) NOT NULL AUTO_INCREMENT, + `home_cfg_id` INT(11) NOT NULL, + `mod_cfg_id` INT(11) NOT NULL, + `service_name` VARCHAR(255) NOT NULL, + `remote_server_id` VARCHAR(255) NOT NULL, + `out_of_stock` VARCHAR(255) NOT NULL DEFAULT '', + `slot_max_qty` INT(11) NOT NULL, + `slot_min_qty` INT(11) NOT NULL, + `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0, + `price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0, + `price_year` FLOAT(15,4) NOT NULL DEFAULT 0, + `description` VARCHAR(1000) NOT NULL DEFAULT '', + `img_url` VARCHAR(255) NOT NULL DEFAULT '', + `ftp` VARCHAR(255) NOT NULL DEFAULT '', + `install_method` VARCHAR(255) NOT NULL DEFAULT '', + `manual_url` VARCHAR(255) NOT NULL DEFAULT '', + `access_rights` VARCHAR(255) NOT NULL DEFAULT '', + `enabled` INT(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`service_id`), + KEY `enabled` (`enabled`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;", + + // Billing Orders - Actual game server instances (ongoing services) "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_orders` ( - `order_id` int(11) NOT NULL auto_increment, - `user_id` int(11) NOT NULL, - `service_id` int(11) NOT NULL, - `home_name` varchar(255) NOT NULL, - `ip` varchar(255) NOT NULL, - `qty` int(11) NOT NULL, - `invoice_duration` varchar(16) NOT NULL, - `max_players` int(11) NOT NULL, - `price` float(15,2) NOT NULL, - `remote_control_password` varchar(10) NULL, - `ftp_password` varchar(10) NULL, - `cart_id` int(11) NOT NULL, - `home_id` varchar(255) NOT NULL DEFAULT '0', - `status` varchar(16) NOT NULL DEFAULT '0', - `finish_date` varchar(16) NOT NULL DEFAULT '0', - `extended` tinyint(1) NOT NULL, - `coupon_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`order_id`) - ) ENGINE=MyISAM;" + `order_id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `service_id` INT(11) NOT NULL, + `home_name` VARCHAR(255) NOT NULL, + `ip` VARCHAR(255) NOT NULL DEFAULT '', + `qty` INT(11) NOT NULL DEFAULT 1, + `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month', + `max_players` INT(11) NOT NULL DEFAULT 0, + `price` FLOAT(15,2) NOT NULL DEFAULT 0, + `remote_control_password` VARCHAR(255) NULL, + `ftp_password` VARCHAR(255) NULL, + `home_id` VARCHAR(255) NOT NULL DEFAULT '0', + `status` VARCHAR(16) NOT NULL DEFAULT 'in-cart', + `order_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `end_date` DATETIME NULL, + `payment_txid` VARCHAR(255) NULL, + `paid_ts` DATETIME NULL, + `coupon_id` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`order_id`), + KEY `user_id` (`user_id`), + KEY `status` (`status`), + KEY `home_id` (`home_id`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;", + + // Billing Invoices - Created when user adds to cart, becomes order after payment + "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_invoices` ( + `invoice_id` INT(11) NOT NULL AUTO_INCREMENT, + `order_id` INT(11) NOT NULL DEFAULT 0, + `user_id` INT(11) NOT NULL, + `service_id` INT(11) NOT NULL, + `home_name` VARCHAR(255) NOT NULL DEFAULT '', + `ip` INT(11) NOT NULL DEFAULT 0, + `max_players` INT(11) NOT NULL DEFAULT 0, + `remote_control_password` VARCHAR(255) NULL, + `ftp_password` VARCHAR(255) NULL, + `customer_name` VARCHAR(255) NOT NULL DEFAULT '', + `customer_email` VARCHAR(255) NOT NULL DEFAULT '', + `amount` FLOAT(15,2) NOT NULL DEFAULT 0, + `currency` VARCHAR(3) NOT NULL DEFAULT 'USD', + `status` VARCHAR(16) NOT NULL DEFAULT 'due', + `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `due_date` DATETIME NULL, + `paid_date` DATETIME NULL, + `payment_txid` VARCHAR(255) NULL, + `payment_method` VARCHAR(50) NULL, + `description` VARCHAR(500) NOT NULL DEFAULT '', + `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month', + `qty` INT(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`invoice_id`), + KEY `order_id` (`order_id`), + KEY `user_id` (`user_id`), + KEY `status` (`status`), + KEY `due_date` (`due_date`), + KEY `service_id` (`service_id`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;" ); -$install_queries[1] = array( - "DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_carts`;", - "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_carts` ( - `cart_id` int(11) NOT NULL auto_increment, - `user_id` int(11) NOT NULL, - `paid` int(11) NULL, - PRIMARY KEY (`cart_id`) - ) ENGINE=MyISAM DEFAULT CHARSET=UTF8;" -); - -$install_queries[2] = array( - "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `date` varchar(16) NOT NULL DEFAULT '0';", - "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `tax_amount` varchar(16) NOT NULL DEFAULT '0';", - "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `currency` varchar(3) NOT NULL DEFAULT '0';" -); - -$install_queries[3] = array( - "ALTER TABLE `".OGP_DB_PREFIX."billing_carts` ADD `coupon_id` int(11) NOT NULL DEFAULT 0;" -); - -$install_queries[4] = array( - "ALTER TABLE `".OGP_DB_PREFIX."billing_orders` MODIFY `coupon_id` int(11) NOT NULL DEFAULT 0;" -); - -$install_queries[5] = array( - "ALTER TABLE `".OGP_DB_PREFIX."billing_services` ADD `out_of_stock` varchar(255) NOT NULL AFTER `remote_server_id`;" -); - - ?> diff --git a/modules/billing/my_account.php b/modules/billing/my_account.php index 62a3fdfe..5ebba5d3 100644 --- a/modules/billing/my_account.php +++ b/modules/billing/my_account.php @@ -125,7 +125,7 @@ $servers_query = "SELECT o.price, o.invoice_duration, o.home_id, - o.finish_date, + o.end_date, bs.service_name FROM ogp_billing_orders o LEFT JOIN ogp_billing_services bs ON o.service_id = bs.service_id @@ -325,7 +325,7 @@ $status_config = [
Expires: - +
diff --git a/modules/billing/my_servers.php b/modules/billing/my_servers.php index dde6182d..9e257da6 100644 --- a/modules/billing/my_servers.php +++ b/modules/billing/my_servers.php @@ -40,8 +40,8 @@ $query = "SELECT o.order_id, o.status, o.invoice_duration, - -- use finish_date as the expiration marker (set when order is paid/created) - o.finish_date AS expiration_date, + -- use end_date as the expiration marker (set when order is paid/created) + o.end_date AS expiration_date, bs.service_name, bs.price_monthly FROM ogp_home h diff --git a/modules/billing/payment_cancel.php b/modules/billing/payment_cancel.php new file mode 100644 index 00000000..0d079f24 --- /dev/null +++ b/modules/billing/payment_cancel.php @@ -0,0 +1,51 @@ + + + + + + + Payment Cancelled - Game Server Panel + + + + +
+
+

Payment Cancelled

+

Your payment was cancelled. No charges have been made to your account.

+ +

Invoice Reference:

+ +
+ +
+

What would you like to do?

+
    +
  • Return to Cart: Your items are still in your cart. You can complete the payment anytime.
  • +
  • Continue Shopping: Browse our game server options and add more to your cart.
  • +
  • Need Help?: Contact our support team if you encountered any issues during checkout.
  • +
+
+ + +
+ + + + diff --git a/modules/billing/payment_success.php b/modules/billing/payment_success.php index 93245cd0..224c026b 100644 --- a/modules/billing/payment_success.php +++ b/modules/billing/payment_success.php @@ -1,54 +1,94 @@ mysqli_connect_error()]); - else error_log('[payment_success] DB connect failed: ' . mysqli_connect_error()); - return false; - } +?> + + + + + + Payment Successful - Game Server Panel + + + - // Helper to run a prepared update - $update_paid = function($where_sql, $bind_types, $bind_vals) use ($db, $txid, $ts) { - // Ensure we only set paid when not already paid - $sql = "UPDATE ogp_billing_orders SET status = 'paid'"; - // Optionally set txid/paid_ts if columns exist; also attempt finish_date - $cols = []; - $res = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'payment_txid'"); - if ($res && mysqli_num_rows($res) > 0) $cols[] = 'payment_txid'; - $res2 = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'paid_ts'"); - if ($res2 && mysqli_num_rows($res2) > 0) $cols[] = 'paid_ts'; - $res3 = mysqli_query($db, "SHOW COLUMNS FROM ogp_billing_orders LIKE 'finish_date'"); - $has_finish = ($res3 && mysqli_num_rows($res3) > 0); - // We'll compute finish_date when possible by selecting qty/invoice_duration for the matched row later - if ($cols) { - $sql .= ', ' . implode(' = ?, ', $cols) . ' = ?'; +
+
+

✓ Payment Successful!

+

Thank you for your purchase. Your payment has been received and is being processed.

+ +

Invoice Reference:

+ +
+ +
+

What happens next?

+
    +
  1. Payment Confirmation: Your payment has been captured by PayPal
  2. +
  3. Order Creation: Your game server order has been created
  4. +
  5. Server Provisioning: Your server will be provisioned automatically (this may take a few minutes)
  6. +
  7. Email Notification: You'll receive an email with your server details and login credentials
  8. +
+
+ + 0) { + $db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port); + if ($db) { + $result = mysqli_query($db, "SELECT * FROM ogp_billing_orders WHERE user_id=$user_id ORDER BY order_date DESC LIMIT 5"); + if ($result && mysqli_num_rows($result) > 0) { + echo '
'; + echo '

Your Recent Orders

'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + while ($order = mysqli_fetch_assoc($result)) { + $statusColor = $order['status'] === 'paid' ? '#28a745' : '#6c757d'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
Order IDServerStatusDatePrice
#' . htmlspecialchars($order['order_id']) . '' . htmlspecialchars($order['home_name']) . '' . htmlspecialchars(ucfirst($order['status'])) . '' . htmlspecialchars($order['order_date']) . '$' . htmlspecialchars(number_format($order['price'], 2)) . '
'; + echo '
'; + } + mysqli_close($db); } - // placeholder for finish_date; we'll append it if we can compute it - $sql .= ' WHERE ' . $where_sql . ' AND status <> "paid" LIMIT 1'; + } + ?> - // If we need finish_date, attempt to compute it by selecting the row first - $finish_date_val = null; + +
+ + + + + $end_date_val = null; if ($has_finish) { // Attempt to find the target order's qty/invoice_duration using the same where clause but without LIMIT $sel_sql = "SELECT qty, invoice_duration FROM ogp_billing_orders WHERE " . str_replace(' AND status <> \"paid\" LIMIT 1', '', $where_sql) . " LIMIT 1"; @@ -77,17 +117,17 @@ function process_payment_record(array $record) { if ($months <= 0) $months = 0; $dt = new DateTime('now'); if ($months > 0) $dt->modify('+' . intval($months) . ' months'); - $finish_date_val = $dt->format('Y-m-d H:i:s'); + $end_date_val = $dt->format('Y-m-d H:i:s'); } $sel_stmt->close(); } - if ($finish_date_val !== null) { - $sql = str_replace(' WHERE ', ', finish_date = ? WHERE ', $sql); + if ($end_date_val !== null) { + $sql = str_replace(' WHERE ', ', end_date = ? WHERE ', $sql); } } if ($stmt = $db->prepare($sql)) { - // Build params: first any where params, then txid/ts values if present, then finish_date if present + // Build params: first any where params, then txid/ts values if present, then end_date if present $types = $bind_types; $vals = $bind_vals; if ($cols) { @@ -97,9 +137,9 @@ function process_payment_record(array $record) { else $vals[] = $ts; } } - if ($finish_date_val !== null) { + if ($end_date_val !== null) { $types .= 's'; - $vals[] = $finish_date_val; + $vals[] = $end_date_val; } // bind dynamically if ($types) {