added invoiceing
This commit is contained in:
parent
89b5344e79
commit
0e91ec4b9a
21 changed files with 1892 additions and 322 deletions
40
.github/copilot-instructions.md
vendored
40
.github/copilot-instructions.md
vendored
|
|
@ -8,6 +8,46 @@
|
|||
- Do NOT modify files outside `_website/` (the panel codebase) unless a maintainer explicitly asks for cross-repo or panel-side changes. If a change necessarily touches panel files, call it out clearly in the plan and get maintainer approval first.
|
||||
- All redirects, data directories, and public-facing endpoints implemented for the storefront must be scoped under `_website/` (absolute or root-relative to the `_website` site root), not the panel root or external panel dashboard pages.
|
||||
|
||||
## CRITICAL: Website file paths and URLs (modules/billing)
|
||||
- **The billing website files in `modules/billing/` will be deployed at the WEBSITE ROOT when live.**
|
||||
- **NEVER EVER use `/modules/billing/` in any URL, link, redirect, or file path within the billing website code.**
|
||||
- **All URLs must be root-relative (starting with `/` but NOT including `/modules/billing/`):**
|
||||
- ✅ CORRECT: `/payment_success.php`, `/cart.php`, `/order.php`
|
||||
- ❌ WRONG: `/modules/billing/payment_success.php`, `modules/billing/cart.php`
|
||||
- **This is a CRITICAL requirement that has been violated multiple times. Read this section carefully before making ANY changes to billing website files.**
|
||||
|
||||
### Examples of CORRECT usage:
|
||||
```php
|
||||
// PayPal return URLs
|
||||
$returnUrl = $siteBase . '/payment_success.php';
|
||||
$cancelUrl = $siteBase . '/payment_cancel.php';
|
||||
|
||||
// Header redirects
|
||||
header('Location: /cart.php');
|
||||
header('Location: /order.php');
|
||||
|
||||
// Links
|
||||
<a href="/my_account.php">My Account</a>
|
||||
<a href="/serverlist.php">Browse Servers</a>
|
||||
|
||||
// Form actions
|
||||
<form action="/add_to_cart.php" method="POST">
|
||||
```
|
||||
|
||||
### Examples of WRONG usage (NEVER DO THIS):
|
||||
```php
|
||||
// ❌ WRONG - includes modules/billing path
|
||||
$returnUrl = $siteBase . '/modules/billing/payment_success.php';
|
||||
header('Location: /modules/billing/cart.php');
|
||||
<a href="/modules/billing/my_account.php">My Account</a>
|
||||
```
|
||||
|
||||
### Exception - Backend includes only:
|
||||
- Backend PHP includes CAN use `__DIR__` or relative paths for file inclusion:
|
||||
- ✅ `require_once(__DIR__ . '/includes/config.inc.php')`
|
||||
- ✅ `require_once(__DIR__ . '/../../includes/database_mysqli.php')`
|
||||
- But these are for SERVER-SIDE file inclusion, NOT for user-facing URLs/redirects/links.
|
||||
|
||||
## 1) What to read first (paths & context)
|
||||
- `_website/` — canonical website storefront and Checkout/Webhooks flow.
|
||||
- `modules/config_games/server_configs/` — authoritative game catalog XMLs (all supported games live here).
|
||||
|
|
|
|||
110
modules/billing/COLUMN_RENAME_SUMMARY.md
Normal file
110
modules/billing/COLUMN_RENAME_SUMMARY.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Column Rename: finish_date → end_date
|
||||
|
||||
## Overview
|
||||
Renamed the `finish_date` column to `end_date` across the entire billing module for better semantic clarity. The column represents when a server's subscription ends/expires, so "end_date" is more descriptive.
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Database Schema
|
||||
1. **module.php** - Line 77
|
||||
- Updated schema definition: `finish_date` DATETIME NULL → `end_date` DATETIME NULL
|
||||
|
||||
2. **migration_to_invoices.sql**
|
||||
- Line 26: Updated AFTER clause in ADD COLUMN statement
|
||||
- Lines 49-60: Updated column conversion logic from VARCHAR to DATETIME
|
||||
- All references to the column name updated
|
||||
|
||||
### PHP Application Code
|
||||
3. **cron-shop.php** (19 occurrences)
|
||||
- Lines 78-80: Updated query conditions checking end_date IS NOT NULL
|
||||
- Lines 97, 121, 124: Updated email notification date formatting
|
||||
- Lines 142, 150-151: Updated suspension query conditions
|
||||
- Lines 218, 226-227: Updated deletion query conditions
|
||||
- Lines 283, 288: Updated legacy code comments and queries
|
||||
- Lines 301, 304: Updated developer notes
|
||||
- Lines 336, 341: Updated suspension logic
|
||||
- Line 395: Updated final cleanup query
|
||||
|
||||
4. **cart.php** (14 occurrences)
|
||||
- Lines 89-106: Updated variable names from $finish_date to $end_date
|
||||
- Line 111: Updated column existence check
|
||||
- Lines 117, 119, 121, 127: Updated SQL UPDATE statements
|
||||
- Line 148-149: Updated audit logging
|
||||
|
||||
5. **my_account.php** (4 occurrences)
|
||||
- Line 128: Updated SELECT query field
|
||||
- Line 328: Updated display formatting (3 references in same line)
|
||||
|
||||
6. **my_servers.php** (2 occurrences)
|
||||
- Line 43: Updated SQL comment
|
||||
- Line 44: Updated column alias
|
||||
|
||||
7. **admin_invoices.php** (1 occurrence)
|
||||
- Line 99: Updated display column
|
||||
|
||||
8. **add_to_cart.php** (10 occurrences)
|
||||
- Lines 134-151: Updated variable names, column checks, INSERT queries, logging
|
||||
|
||||
9. **create_servers.php** (12 occurrences)
|
||||
- Line 244: Updated condition check
|
||||
- Lines 295-296: Updated comments
|
||||
- Lines 301-330: Updated variable names in date calculation logic
|
||||
- Line 342: Updated SET clause in UPDATE query (2 references)
|
||||
|
||||
10. **payment_success.php** (11 occurrences)
|
||||
- Lines 35-102: Updated all references in payment processing logic
|
||||
- Variable renamed: $finish_date_val → $end_date_val
|
||||
- Updated column existence checks and SQL generation
|
||||
|
||||
### Documentation
|
||||
11. **INVOICE_SYSTEM.md** (6 occurrences)
|
||||
- Line 27: Updated field description
|
||||
- Line 67: Updated workflow documentation
|
||||
- Line 74: Updated renewal process
|
||||
- Line 84: Updated expiration logic
|
||||
- Line 113: Updated payment completion notes
|
||||
- Line 124: Updated My Account display notes
|
||||
|
||||
12. **MIGRATION_SUMMARY.md** (4 occurrences)
|
||||
- Line 11: Updated changelog entry
|
||||
- Line 18: Updated bug fix description
|
||||
- Lines 30, 36: Updated cron process descriptions
|
||||
- Line 87: Updated SQL schema example
|
||||
- Line 141: Updated verification notes
|
||||
|
||||
## Database Impact
|
||||
|
||||
### For Fresh Installations
|
||||
- New installations will create the `ogp_billing_orders` table with `end_date` DATETIME NULL
|
||||
|
||||
### For Existing Installations
|
||||
- Run the updated `migration_to_invoices.sql` script
|
||||
- The script will handle the column rename automatically using dynamic SQL:
|
||||
```sql
|
||||
-- Checks if column exists as 'finish_date' and renames to 'end_date'
|
||||
-- Then converts data type from VARCHAR to DATETIME
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
- [x] Module schema updated (module.php)
|
||||
- [x] Migration script updated (migration_to_invoices.sql)
|
||||
- [x] All PHP files using the column updated
|
||||
- [x] All SQL queries updated
|
||||
- [x] All variable names updated
|
||||
- [x] All comments and documentation updated
|
||||
- [x] Verified no remaining `finish_date` references (except log files)
|
||||
|
||||
## Backwards Compatibility
|
||||
⚠️ **BREAKING CHANGE**: This rename requires running the migration script on existing databases.
|
||||
|
||||
**Migration Path:**
|
||||
1. Backup database
|
||||
2. Run updated `migration_to_invoices.sql`
|
||||
3. The script will automatically rename `finish_date` to `end_date`
|
||||
4. Verify column exists: `SHOW COLUMNS FROM ogp_billing_orders LIKE 'end_date';`
|
||||
|
||||
## Notes
|
||||
- Log files may still contain old references to `finish_date` - this is expected and harmless
|
||||
- The semantic meaning of the column is unchanged (server expiration date)
|
||||
- All date calculations remain identical
|
||||
- No functional changes, only naming improvement for clarity
|
||||
190
modules/billing/INVOICE_FIRST_FLOW.md
Normal file
190
modules/billing/INVOICE_FIRST_FLOW.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Invoice-First Billing Flow
|
||||
|
||||
## Overview
|
||||
The billing system now follows an **invoice-first** workflow where invoices are created BEFORE orders. Orders are only created after successful payment.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Add to Cart (order.php → add_to_cart.php)
|
||||
**What happens:**
|
||||
- User clicks "Add to Cart" button on order page
|
||||
- System creates a **billing_invoices** record with:
|
||||
- `status` = 'due'
|
||||
- `order_id` = 0 (no order exists yet)
|
||||
- All server details (service_id, home_name, ip, max_players, passwords, etc.)
|
||||
- Customer details (name, email from ogp_users)
|
||||
- Pricing (amount, qty, invoice_duration)
|
||||
- `due_date` = now + 3 days
|
||||
|
||||
**Database changes:**
|
||||
- INSERT into `ogp_billing_invoices`
|
||||
- NO changes to `ogp_billing_orders` (order doesn't exist yet)
|
||||
|
||||
### 2. Cart Display (cart.php)
|
||||
**What shows:**
|
||||
- Query: `SELECT * FROM ogp_billing_invoices WHERE status = 'due' AND user_id = ?`
|
||||
- Displays all **unpaid invoices** (status='due')
|
||||
- Shows invoice_id, home_name, ip, max_players, amount, qty
|
||||
- Free items show "Claim (Free)" button
|
||||
- Paid items show PayPal button
|
||||
|
||||
**Actions available:**
|
||||
- Delete invoice (removes from cart, no order cleanup needed)
|
||||
- Pay invoice (via PayPal or Free button)
|
||||
|
||||
### 3. Payment (PayPal or Free)
|
||||
|
||||
#### 3a. Free/Claim Flow (cart.php POST handler)
|
||||
**When:** User clicks "Claim (Free)" or admin clicks "Create (Free)"
|
||||
|
||||
**What happens:**
|
||||
1. Mark invoice as paid:
|
||||
- UPDATE `ogp_billing_invoices` SET status='paid', paid_date=NOW()
|
||||
2. Create order record:
|
||||
- Calculate end_date (qty * invoice_duration)
|
||||
- INSERT into `ogp_billing_orders` with status='paid'
|
||||
- Get new order_id from INSERT
|
||||
3. Link invoice to order:
|
||||
- UPDATE `ogp_billing_invoices` SET order_id=? WHERE invoice_id=?
|
||||
|
||||
**Database changes:**
|
||||
- UPDATE `ogp_billing_invoices`: status='due' → 'paid', paid_date=NOW(), order_id=(new)
|
||||
- INSERT `ogp_billing_orders`: New record with status='paid', end_date calculated
|
||||
|
||||
#### 3b. PayPal Flow (api/capture_order.php)
|
||||
**When:** User pays via PayPal
|
||||
|
||||
**What should happen:**
|
||||
1. PayPal sends capture webhook
|
||||
2. System marks invoice as paid (same as Free flow)
|
||||
3. System creates order record (same as Free flow)
|
||||
4. System links invoice to order (same as Free flow)
|
||||
|
||||
**Database changes:** (Same as Free flow above)
|
||||
|
||||
### 4. Server Provisioning (create_servers.php)
|
||||
**What happens:**
|
||||
- Cron job or manual trigger finds orders with status='paid'
|
||||
- Creates actual game server (home_id)
|
||||
- Updates order: status='paid' → 'installed', home_id=(assigned)
|
||||
|
||||
**Database changes:**
|
||||
- UPDATE `ogp_billing_orders`: status='paid' → 'installed', home_id=(assigned)
|
||||
|
||||
## Status Values
|
||||
|
||||
### Invoice Status
|
||||
- **'due'** - Unpaid invoice (shows in cart)
|
||||
- **'paid'** - Paid invoice (payment confirmed)
|
||||
- **'cancelled'** - Deleted/cancelled invoice
|
||||
|
||||
### Order Status
|
||||
- **'paid'** - Payment confirmed, awaiting provisioning
|
||||
- **'installed'** - Server provisioned and running
|
||||
- **'suspended'** - Server stopped for non-payment
|
||||
- **'expired'** - Service ended
|
||||
|
||||
## Database Schema
|
||||
|
||||
### ogp_billing_invoices (INVOICE-FIRST)
|
||||
```sql
|
||||
invoice_id INT AUTO_INCREMENT PRIMARY KEY
|
||||
order_id INT DEFAULT 0 -- Links to order AFTER payment (0 = not yet paid)
|
||||
user_id INT NOT NULL
|
||||
service_id INT NOT NULL -- Server package being purchased
|
||||
home_name VARCHAR(255) -- Server name
|
||||
ip INT -- IP assignment
|
||||
max_players INT -- Player count
|
||||
remote_control_password VARCHAR(255) -- Server RCON password
|
||||
ftp_password VARCHAR(255) -- FTP password
|
||||
customer_name VARCHAR(255) -- Billing name
|
||||
customer_email VARCHAR(255) -- Billing email
|
||||
amount FLOAT(15,2) -- Total price
|
||||
currency VARCHAR(3) DEFAULT 'USD'
|
||||
status VARCHAR(16) DEFAULT 'due' -- 'due', 'paid', 'cancelled'
|
||||
invoice_date DATETIME DEFAULT NOW()
|
||||
due_date DATETIME -- Payment deadline
|
||||
paid_date DATETIME -- When paid
|
||||
payment_txid VARCHAR(255) -- PayPal transaction ID
|
||||
payment_method VARCHAR(50) -- 'paypal', 'free', etc.
|
||||
description VARCHAR(500) -- Invoice description
|
||||
invoice_duration VARCHAR(16) DEFAULT 'month' -- 'month', 'year', 'day'
|
||||
qty INT DEFAULT 1 -- Quantity/duration multiplier
|
||||
```
|
||||
|
||||
### ogp_billing_orders (ORDER-AFTER-PAYMENT)
|
||||
```sql
|
||||
order_id INT AUTO_INCREMENT PRIMARY KEY
|
||||
user_id INT NOT NULL
|
||||
service_id INT NOT NULL
|
||||
home_name VARCHAR(255)
|
||||
home_id VARCHAR(255) -- Panel game server ID (after provisioning)
|
||||
ip INT
|
||||
max_players INT
|
||||
qty INT
|
||||
invoice_duration VARCHAR(16)
|
||||
price FLOAT(15,2)
|
||||
remote_control_password VARCHAR(255)
|
||||
ftp_password VARCHAR(255)
|
||||
status VARCHAR(16) DEFAULT 'paid' -- 'paid', 'installed', 'suspended', 'expired'
|
||||
order_date DATETIME DEFAULT NOW()
|
||||
end_date DATETIME -- Subscription expiration
|
||||
payment_txid VARCHAR(255)
|
||||
paid_ts DATETIME
|
||||
```
|
||||
|
||||
## Key Differences from Old Flow
|
||||
|
||||
### OLD (Order-First)
|
||||
1. Add to cart → Create ORDER (status='in-cart')
|
||||
2. View cart → Show orders WHERE status='in-cart'
|
||||
3. Pay → UPDATE order status='in-cart' → 'paid'
|
||||
4. Provision → UPDATE order status='paid' → 'installed'
|
||||
|
||||
### NEW (Invoice-First)
|
||||
1. Add to cart → Create INVOICE (status='due', order_id=0)
|
||||
2. View cart → Show invoices WHERE status='due'
|
||||
3. Pay → Mark invoice paid + CREATE ORDER (status='paid') + Link invoice to order
|
||||
4. Provision → UPDATE order status='paid' → 'installed'
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Clean Separation:** Invoices = payment requests, Orders = actual services
|
||||
2. **Better Audit Trail:** Invoice IDs never change, order IDs created only after payment
|
||||
3. **Renewal Support:** Can create multiple invoices for same order (renewals)
|
||||
4. **Cart Simplicity:** Cart only shows unpaid invoices (single source of truth)
|
||||
5. **Payment History:** All payments have invoice records, even free ones
|
||||
|
||||
## Migration Notes
|
||||
|
||||
**Existing orders with status='in-cart' need to be migrated:**
|
||||
```sql
|
||||
-- Convert existing cart items to invoices
|
||||
INSERT INTO ogp_billing_invoices (
|
||||
order_id, user_id, service_id, home_name, ip, max_players,
|
||||
remote_control_password, ftp_password, customer_name, customer_email,
|
||||
amount, status, invoice_duration, qty, description
|
||||
)
|
||||
SELECT
|
||||
0, -- No order exists yet
|
||||
o.user_id,
|
||||
o.service_id,
|
||||
o.home_name,
|
||||
o.ip,
|
||||
o.max_players,
|
||||
o.remote_control_password,
|
||||
o.ftp_password,
|
||||
CONCAT(u.users_fname, ' ', u.users_lname),
|
||||
u.users_email,
|
||||
o.price,
|
||||
'due', -- Convert 'in-cart' to 'due'
|
||||
o.invoice_duration,
|
||||
o.qty,
|
||||
CONCAT('Migrated cart item: ', o.home_name)
|
||||
FROM ogp_billing_orders o
|
||||
LEFT JOIN ogp_users u ON o.user_id = u.user_id
|
||||
WHERE o.status = 'in-cart';
|
||||
|
||||
-- Delete old cart items (now converted to invoices)
|
||||
DELETE FROM ogp_billing_orders WHERE status = 'in-cart';
|
||||
```
|
||||
133
modules/billing/INVOICE_SYSTEM.md
Normal file
133
modules/billing/INVOICE_SYSTEM.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Billing System - Invoice-Based Architecture
|
||||
|
||||
## Overview
|
||||
The billing system now uses a **dual-table architecture** separating orders (ongoing services) from invoices (payment records).
|
||||
|
||||
## Database Tables
|
||||
|
||||
### 1. `ogp_billing_services`
|
||||
**Purpose:** Available game server packages/products
|
||||
**Key Fields:**
|
||||
- `service_id` - Unique identifier
|
||||
- `service_name` - Display name
|
||||
- `remote_server_id` - Target server(s)
|
||||
- `price_monthly`, `price_year` - Pricing tiers
|
||||
- `enabled` - Availability flag
|
||||
|
||||
### 2. `ogp_billing_orders` (formerly just cart items)
|
||||
**Purpose:** Active game server instances (ongoing services)
|
||||
**Key Fields:**
|
||||
- `order_id` - Unique identifier
|
||||
- `user_id` - Owner
|
||||
- `service_id` - Product reference
|
||||
- `home_id` - Panel game home ID (after provisioning)
|
||||
- `home_name` - Server name
|
||||
- `status` - Current state (see Status Flow below)
|
||||
- `order_date` - When created
|
||||
- `end_date` - Expiration date
|
||||
- `payment_txid` - Last payment transaction
|
||||
- `paid_ts` - Last payment timestamp
|
||||
|
||||
**Status Values:**
|
||||
- `in-cart` - User added to cart, not yet paid
|
||||
- `paid` - Payment received, awaiting provisioning
|
||||
- `installed` - ✅ Server provisioned and running
|
||||
- `suspended` - Server stopped due to non-payment
|
||||
- `expired` - Service ended
|
||||
- `renew` - Renewal pending in cart
|
||||
|
||||
### 3. `ogp_billing_invoices` (NEW)
|
||||
**Purpose:** Payment records (one invoice per payment)
|
||||
**Key Fields:**
|
||||
- `invoice_id` - Unique identifier
|
||||
- `order_id` - Links to the server order
|
||||
- `user_id` - Customer
|
||||
- `customer_name` - Full name
|
||||
- `customer_email` - Email address
|
||||
- `amount` - Total due
|
||||
- `currency` - USD, EUR, etc.
|
||||
- `status` - `unpaid` or `paid`
|
||||
- `invoice_date` - When created
|
||||
- `due_date` - Payment deadline
|
||||
- `paid_date` - When paid
|
||||
- `payment_txid` - PayPal/Stripe transaction ID
|
||||
- `payment_method` - PayPal, Stripe, etc.
|
||||
- `description` - Invoice line items
|
||||
- `invoice_duration` - Billing period (month/year)
|
||||
- `qty` - Quantity/duration multiplier
|
||||
|
||||
## Workflow
|
||||
|
||||
### Initial Purchase
|
||||
1. User selects game server package → Creates row in `billing_orders` (status: `in-cart`)
|
||||
2. System creates `billing_invoices` entry (status: `unpaid`, linked to order_id)
|
||||
3. Cart page shows unpaid invoices
|
||||
4. User pays → Invoice status becomes `paid`, order status becomes `paid`
|
||||
5. Provisioning happens → Order status becomes `installed`
|
||||
6. Server is active until `end_date`
|
||||
|
||||
### Renewal Process
|
||||
1. User clicks "Renew" on active server (My Account page)
|
||||
2. System creates NEW invoice in `billing_invoices` (status: `unpaid`, same order_id)
|
||||
3. Cart shows the unpaid renewal invoice
|
||||
4. User pays → Invoice status becomes `paid`
|
||||
5. Order `end_date` is extended by the renewal period
|
||||
|
||||
### Cron Automation (`cron-shop.php`)
|
||||
The cron job checks invoice status to manage servers:
|
||||
|
||||
**7 days before expiration:**
|
||||
- Check if order has unpaid invoice for upcoming period
|
||||
- If NO unpaid invoice exists → Create one (status: `unpaid`)
|
||||
- Email customer about upcoming renewal
|
||||
|
||||
**On expiration (end_date reached):**
|
||||
- Check if order has unpaid invoice
|
||||
- If YES → Suspend server (stop, disable FTP, unassign from user)
|
||||
- Order status → `suspended`
|
||||
|
||||
**7 days after suspension:**
|
||||
- If still unpaid → Delete server permanently
|
||||
- Order status → `expired`
|
||||
|
||||
## Key Advantages
|
||||
|
||||
1. **Clear Payment History:** Each invoice represents one payment
|
||||
2. **Audit Trail:** Can track when/how much each renewal cost
|
||||
3. **Flexible Pricing:** Can adjust price per renewal (discounts, promotions)
|
||||
4. **Multi-Payment Support:** One order can have many invoices
|
||||
5. **Accurate Status:** Order status reflects server state, invoice status reflects payment
|
||||
6. **No Race Conditions:** Webhook updates invoice, provisioning updates order
|
||||
|
||||
## Cart Logic
|
||||
|
||||
**Cart page displays:**
|
||||
- All invoices with `status = 'unpaid'` for the current user
|
||||
- Groups by order_id to show which server each invoice is for
|
||||
- Total amount = SUM of all unpaid invoice amounts
|
||||
|
||||
**After payment:**
|
||||
- Invoice `status` → `paid`
|
||||
- Invoice `paid_date` → NOW()
|
||||
- Invoice `payment_txid` → transaction ID from PayPal/Stripe
|
||||
- Order `status` → `paid` (if new order) or `end_date` extended (if renewal)
|
||||
|
||||
## My Account Logic
|
||||
|
||||
**Show Invoices Section:**
|
||||
- Group invoices by status (unpaid, paid, overdue)
|
||||
- Display invoice_date, amount, status
|
||||
- Link to view invoice details
|
||||
|
||||
**Show Current Servers Section:**
|
||||
- Display orders with `status = 'installed'`
|
||||
- Show end_date (expiration)
|
||||
- "Renew" button creates new invoice
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Run `migration_to_invoices.sql` on existing installations
|
||||
- Creates `billing_invoices` table
|
||||
- Adds missing columns to `billing_orders`
|
||||
- Migrates existing paid orders to have invoices
|
||||
- Removes obsolete `billing_carts` table
|
||||
201
modules/billing/MIGRATION_SUMMARY.md
Normal file
201
modules/billing/MIGRATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Billing System Migration Summary
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 1. `module.php` - Database Schema
|
||||
**Changes:**
|
||||
- Removed all legacy `ALTER TABLE` migration queries (db_version reset to 1)
|
||||
- Updated to single clean install with current schema
|
||||
- Added `ogp_billing_invoices` table definition
|
||||
- Added missing columns to `billing_orders`: `order_date`, `payment_txid`, `paid_ts`
|
||||
- Changed `end_date` from VARCHAR to DATETIME
|
||||
- Removed obsolete columns: `cart_id`, `extended`
|
||||
- Removed `billing_carts` table (replaced by invoices)
|
||||
- Added proper indexes for performance
|
||||
|
||||
### 2. `cron-shop.php` - Server Lifecycle Automation
|
||||
**Fixed Logic Errors:**
|
||||
- OLD BUG: Was deleting servers with `status='paid'` or `status='installed'` if end_date was close
|
||||
- NEW: Only processes servers based on **invoice payment status**, not just order status
|
||||
- Now uses `billing_invoices` table to determine if payment is due
|
||||
|
||||
**New 3-Step Process:**
|
||||
1. **Create Renewal Invoices** (7 days before expiration)
|
||||
- Find `installed` servers expiring soon
|
||||
- Check if unpaid invoice exists
|
||||
- If not, create renewal invoice
|
||||
- Send email reminder
|
||||
|
||||
2. **Suspend Servers** (on expiration with unpaid invoice)
|
||||
- Find `installed` servers past end_date
|
||||
- Check if they have unpaid invoices
|
||||
- Stop server, disable FTP, unassign from user
|
||||
- Status → `suspended`
|
||||
|
||||
3. **Delete Servers** (7 days after suspension)
|
||||
- Find `suspended` servers 7+ days past end_date
|
||||
- Still have unpaid invoices
|
||||
- Permanently delete files and database
|
||||
- Status → `deleted`
|
||||
|
||||
## New Files Created
|
||||
|
||||
### 1. `migration_to_invoices.sql`
|
||||
**Purpose:** Upgrade existing installations
|
||||
**What it does:**
|
||||
- Adds new columns to `billing_orders`
|
||||
- Creates `billing_invoices` table
|
||||
- Migrates existing paid orders to have invoice records
|
||||
- Removes obsolete `billing_carts` table
|
||||
- Adds performance indexes
|
||||
|
||||
### 2. `INVOICE_SYSTEM.md`
|
||||
**Purpose:** Documentation
|
||||
**Contents:**
|
||||
- Table schemas explained
|
||||
- Workflow diagrams
|
||||
- Status field definitions
|
||||
- Cron automation logic
|
||||
- Migration instructions
|
||||
|
||||
## SQL for Fresh Install
|
||||
|
||||
The `module.php` now contains clean CREATE TABLE statements for:
|
||||
|
||||
### ogp_billing_services
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_services` (
|
||||
service_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
service_name VARCHAR(255),
|
||||
remote_server_id VARCHAR(255),
|
||||
price_monthly FLOAT(15,4),
|
||||
enabled INT DEFAULT 1,
|
||||
... [other fields]
|
||||
);
|
||||
```
|
||||
|
||||
### ogp_billing_orders
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_orders` (
|
||||
order_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
service_id INT NOT NULL,
|
||||
home_name VARCHAR(255),
|
||||
home_id VARCHAR(255),
|
||||
status VARCHAR(16) DEFAULT 'in-cart',
|
||||
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
end_date DATETIME NULL,
|
||||
payment_txid VARCHAR(255) NULL,
|
||||
paid_ts DATETIME NULL,
|
||||
... [other fields]
|
||||
KEY (user_id),
|
||||
KEY (status),
|
||||
KEY (home_id)
|
||||
);
|
||||
```
|
||||
|
||||
### ogp_billing_invoices (NEW)
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_invoices` (
|
||||
invoice_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
order_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
customer_name VARCHAR(255),
|
||||
customer_email VARCHAR(255),
|
||||
amount FLOAT(15,2),
|
||||
currency VARCHAR(3) DEFAULT 'USD',
|
||||
status VARCHAR(16) DEFAULT 'unpaid',
|
||||
invoice_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
due_date DATETIME NULL,
|
||||
paid_date DATETIME NULL,
|
||||
payment_txid VARCHAR(255),
|
||||
payment_method VARCHAR(50),
|
||||
description VARCHAR(500),
|
||||
invoice_duration VARCHAR(16),
|
||||
qty INT DEFAULT 1,
|
||||
KEY (order_id),
|
||||
KEY (user_id),
|
||||
KEY (status),
|
||||
KEY (due_date)
|
||||
);
|
||||
```
|
||||
|
||||
## Migration Steps for Existing Installations
|
||||
|
||||
1. **Backup Database**
|
||||
```bash
|
||||
mysqldump -u root -p ogp_panel > backup_before_invoice_migration.sql
|
||||
```
|
||||
|
||||
2. **Run Migration Script**
|
||||
```bash
|
||||
mysql -u root -p ogp_panel < modules/billing/migration_to_invoices.sql
|
||||
```
|
||||
|
||||
3. **Verify Tables**
|
||||
```sql
|
||||
SHOW TABLES LIKE 'ogp_billing%';
|
||||
-- Should show: billing_services, billing_orders, billing_invoices
|
||||
|
||||
DESCRIBE ogp_billing_orders;
|
||||
-- Should have: order_date, payment_txid, paid_ts, end_date (DATETIME)
|
||||
|
||||
DESCRIBE ogp_billing_invoices;
|
||||
-- Should exist with all invoice fields
|
||||
```
|
||||
|
||||
4. **Test Cron Job**
|
||||
```bash
|
||||
cd /path/to/ogp/web
|
||||
php modules/billing/cron-shop.php
|
||||
```
|
||||
|
||||
5. **Check Logs**
|
||||
```sql
|
||||
SELECT * FROM ogp_logger WHERE type LIKE '%BILLING-CRON%' ORDER BY date DESC LIMIT 20;
|
||||
```
|
||||
|
||||
## Key Improvements
|
||||
|
||||
1. **Accurate Server Management**
|
||||
- Servers only suspended if they have **unpaid invoices**
|
||||
- Active paid servers are never touched
|
||||
- Clear separation between order state and payment state
|
||||
|
||||
2. **Audit Trail**
|
||||
- Every payment creates an invoice record
|
||||
- Can track payment history per server
|
||||
- Know exactly when/why server was suspended
|
||||
|
||||
3. **Flexible Pricing**
|
||||
- Each renewal can have different price
|
||||
- Support for discounts and promotions
|
||||
- Currency per invoice (multi-currency support ready)
|
||||
|
||||
4. **Better Customer Experience**
|
||||
- Clear invoice emails with due dates
|
||||
- 7-day warning before expiration
|
||||
- 7-day grace period before deletion
|
||||
|
||||
## Status Field Values Reference
|
||||
|
||||
### billing_orders.status
|
||||
- `in-cart` - Initial state, unpaid
|
||||
- `paid` - Payment received, awaiting provisioning
|
||||
- `installed` - Server active and running ✅
|
||||
- `suspended` - Stopped due to non-payment
|
||||
- `deleted` - Permanently removed
|
||||
- `expired` - Service ended
|
||||
- `renew` - Renewal in cart (legacy, now uses invoices)
|
||||
|
||||
### billing_invoices.status
|
||||
- `unpaid` - Invoice created, awaiting payment
|
||||
- `paid` - Invoice paid successfully
|
||||
|
||||
## Next Steps for Implementation
|
||||
|
||||
1. Update cart.php to show invoices instead of orders
|
||||
2. Update my_account.php "Renew" button to create invoices
|
||||
3. Update payment success flow to mark invoices paid
|
||||
4. Add invoice viewing page
|
||||
5. Test full workflow: order → pay → renew → pay renewal
|
||||
9
modules/billing/add_service_id_column.sql
Normal file
9
modules/billing/add_service_id_column.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- Add missing service_id column to ogp_billing_invoices table
|
||||
-- This column is required to track which service/game plan was purchased
|
||||
|
||||
ALTER TABLE `ogp_billing_invoices`
|
||||
ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`;
|
||||
|
||||
-- Add index for better query performance
|
||||
ALTER TABLE `ogp_billing_invoices`
|
||||
ADD KEY `service_id` (`service_id`);
|
||||
|
|
@ -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) {
|
||||
$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
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
|||
</span>
|
||||
</td>
|
||||
<td><?php echo h($row['order_date']); ?></td>
|
||||
<td><?php echo h($row['finish_date'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo h($row['end_date'] ?? 'N/A'); ?></td>
|
||||
<td>
|
||||
<button onclick="editRow(<?php echo $row['order_id']; ?>)" class="gsw-btn" style="padding: 4px 10px; font-size: 12px;">Edit</button>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
require_once(__DIR__ . '/../includes/config.inc.php');
|
||||
require_once(__DIR__ . '/../../../includes/database_mysqli.php');
|
||||
$sandbox = true; // flip to false for Live
|
||||
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||
|
|
@ -33,53 +34,119 @@ curl_setopt_array($ch, [
|
|||
]);
|
||||
$res = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http !== 201 && $http !== 200) { http_response_code($http); echo $res; exit; }
|
||||
// Normalize response: ensure we always return valid JSON to the caller
|
||||
if ($res === false || $res === '') {
|
||||
// Curl-level failure or empty body
|
||||
http_response_code(502);
|
||||
$out = ['error' => 'paypal_empty_response', 'http' => $http, 'curl_error' => $curl_err];
|
||||
echo json_encode($out);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parse the capture response
|
||||
$captureData = json_decode($res, true);
|
||||
$captureStatus = $captureData['status'] ?? '';
|
||||
// Attempt to decode PayPal response
|
||||
$capture = json_decode($res, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
// PayPal returned non-JSON / malformed response — return it as raw string inside JSON
|
||||
http_response_code(502);
|
||||
$out = ['error' => 'paypal_invalid_json', 'http' => $http, 'raw' => $res];
|
||||
echo json_encode($out);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If capture was successful, immediately update the order status to 'paid'
|
||||
if ($captureStatus === 'COMPLETED') {
|
||||
// Extract custom_id which contains the order_id
|
||||
$customId = $captureData['purchase_units'][0]['payments']['captures'][0]['custom_id'] ?? null;
|
||||
$txnId = $captureData['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
|
||||
if ($http !== 201 && $http !== 200) {
|
||||
http_response_code($http);
|
||||
// Return structured JSON with PayPal's decoded response
|
||||
echo json_encode(['error' => 'paypal_capture_failed', 'http' => $http, 'response' => $capture]);
|
||||
exit;
|
||||
}
|
||||
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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');
|
||||
// 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 ($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 end_date based on qty * duration
|
||||
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
// 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) . "'";
|
||||
}
|
||||
$sql .= " WHERE order_id = $orderId AND status = 'in-cart' LIMIT 1";
|
||||
mysqli_query($db, $sql);
|
||||
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);
|
||||
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// 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);
|
||||
|
||||
// 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_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->bind_result($linked_order_id);
|
||||
$found = $stmt->fetch();
|
||||
$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);
|
||||
|
||||
if ($found && $linked_order_id > 0) {
|
||||
// This is a renewal invoice - just delete the invoice, keep the order
|
||||
$delete = $db->prepare("DELETE FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?");
|
||||
$delete->bind_param("ii", $invoice_id, $user_id);
|
||||
$delete->execute();
|
||||
// Log deletion to panel logger
|
||||
if (isset($db) && method_exists($db, 'logger')) {
|
||||
$db->logger("USER-CART: User " . intval($user_id) . " deleted order " . intval($order_id));
|
||||
$db->logger("USER-CART: User " . intval($user_id) . " deleted renewal invoice " . intval($invoice_id));
|
||||
}
|
||||
$delete->close();
|
||||
} else {
|
||||
// New order invoice - delete it
|
||||
$delete = $db->prepare("DELETE FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?");
|
||||
$delete->bind_param("ii", $invoice_id, $user_id);
|
||||
$delete->execute();
|
||||
if (isset($db) && method_exists($db, 'logger')) {
|
||||
$db->logger("USER-CART: User " . intval($user_id) . " deleted invoice " . intval($invoice_id));
|
||||
}
|
||||
$delete->close();
|
||||
}
|
||||
|
|
@ -275,11 +275,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_single'])) {
|
|||
}
|
||||
|
||||
if ($db){
|
||||
$carts = $db->query("SELECT * FROM ogp_billing_orders AS cart
|
||||
WHERE (status = 'in-cart' OR status = 'renew') AND user_id = " . $user_id . " ORDER BY order_id ASC");
|
||||
|
||||
|
||||
|
||||
$carts = $db->query("SELECT * FROM ogp_billing_invoices AS cart
|
||||
WHERE status = 'due' AND user_id = " . $user_id . " ORDER BY invoice_id ASC");
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
@ -311,38 +308,38 @@ if ($db){
|
|||
if (isset($carts) && $carts instanceof mysqli_result && $carts->num_rows > 0) {
|
||||
while ($row = $carts->fetch_assoc()) {
|
||||
?>
|
||||
<tr data-cart-id="<?php echo htmlspecialchars($row['order_id']); ?>">
|
||||
<tr data-cart-id="<?php echo htmlspecialchars($row['invoice_id']); ?>">
|
||||
<td>
|
||||
<form method="post" action="" class="inline-form">
|
||||
<button type="submit" name="delete_single" value="<?php echo htmlspecialchars($row['order_id']); ?>" class="btn-square text-danger">
|
||||
<button type="submit" name="delete_single" value="<?php echo htmlspecialchars($row['invoice_id']); ?>" class="btn-square text-danger">
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($row['home_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($row['invoice_id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($row['home_name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($row['ip']); ?></td>
|
||||
<td><?php echo htmlspecialchars($row['max_players']); ?></td>
|
||||
<td>$<?php echo number_format($row['price'], 2); ?></td>
|
||||
<td>$<?php echo number_format($row['amount'], 2); ?></td>
|
||||
<td><?php echo htmlspecialchars($row['qty']); ?></td>
|
||||
<?php $rowtotal = $row['price'] * $row['qty'] * $row['max_players'];?>
|
||||
<?php $rowtotal = $row['amount'] * $row['qty'] * $row['max_players'];?>
|
||||
<?php
|
||||
// Build invoice and line item structures used later when creating PayPal order
|
||||
if (!isset($invoice) || !is_array($invoice)) $invoice = [];
|
||||
$invoice[] = [
|
||||
'serverID' => isset($row['home_id']) ? (string)$row['home_id'] : ('order'.$row['order_id']),
|
||||
'serverID' => 'invoice-' . $row['invoice_id'],
|
||||
'amount' => number_format($rowtotal, 2, '.', ''),
|
||||
'order_id' => intval($row['order_id'])
|
||||
'invoice_id' => intval($row['invoice_id'])
|
||||
];
|
||||
?>
|
||||
<?php
|
||||
// Use the previously resolved $is_admin (computed once above)
|
||||
$is_free = ((float)$row['price'] == 0.0);
|
||||
$is_free = ((float)$row['amount'] == 0.0);
|
||||
?>
|
||||
<?php if ($is_admin || $is_free): ?>
|
||||
<td>
|
||||
<form method="post" action="" class="inline-form">
|
||||
<input type="hidden" name="create_free_for" value="<?php echo (int)$row['order_id']; ?>">
|
||||
<input type="hidden" name="create_free_for" value="<?php echo (int)$row['invoice_id']; ?>">
|
||||
<button type="submit" class="gsw-btn"><?php echo $is_admin ? 'Create (Free)' : 'Claim (Free)'; ?></button>
|
||||
</form>
|
||||
<?php if ($is_admin): ?>
|
||||
|
|
@ -428,12 +425,14 @@ if (is_array($invoice) && count($invoice) === 1 && !empty($invoice[0]['order_id'
|
|||
$description = 'Game server order (' . count($lineItems) . ' item' . (count($lineItems)===1?'': 's') . ')';
|
||||
|
||||
// URLs
|
||||
// Define the site base URL
|
||||
$siteBaseUrl = 'http://gameservers.world/modules/billing';
|
||||
// Define the site base URL - detect protocol and host dynamically
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$siteBase = $protocol . $host;
|
||||
|
||||
// Generate absolute URLs for return and cancel
|
||||
$returnUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId);
|
||||
$cancelUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1';
|
||||
// Return URLs are root-relative (website will be deployed at root, not modules/billing)
|
||||
$returnUrl = $siteBase . '/payment_success.php?invoice=' . urlencode($invoiceId);
|
||||
$cancelUrl = $siteBase . '/payment_cancel.php?invoice=' . urlencode($invoiceId);
|
||||
|
||||
// API base (relative) - point to billing module API endpoints
|
||||
$apiBase = 'api';
|
||||
|
|
|
|||
76
modules/billing/check_table.php
Normal file
76
modules/billing/check_table.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
/**
|
||||
* Check ogp_billing_invoices table structure
|
||||
*/
|
||||
|
||||
require_once('../../includes/config.inc.php');
|
||||
require_once('../../includes/database_mysqli.php');
|
||||
|
||||
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
|
||||
|
||||
if (!$db) {
|
||||
die("Database connection failed: " . mysqli_connect_error());
|
||||
}
|
||||
|
||||
echo "<h2>ogp_billing_invoices Table Structure</h2>\n";
|
||||
|
||||
$result = mysqli_query($db, "DESCRIBE ogp_billing_invoices");
|
||||
|
||||
if (!$result) {
|
||||
die("Table doesn't exist or query failed: " . mysqli_error($db));
|
||||
}
|
||||
|
||||
echo "<table border='1' style='border-collapse: collapse;'>\n";
|
||||
echo "<tr><th>Field</th><th>Type</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th></tr>\n";
|
||||
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
echo "<tr>";
|
||||
echo "<td>{$row['Field']}</td>";
|
||||
echo "<td>{$row['Type']}</td>";
|
||||
echo "<td>{$row['Null']}</td>";
|
||||
echo "<td>{$row['Key']}</td>";
|
||||
echo "<td>" . ($row['Default'] ?? 'NULL') . "</td>";
|
||||
echo "<td>{$row['Extra']}</td>";
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
echo "</table>\n";
|
||||
|
||||
// Count existing invoices
|
||||
$count_result = mysqli_query($db, "SELECT COUNT(*) as cnt FROM ogp_billing_invoices");
|
||||
$count = mysqli_fetch_assoc($count_result);
|
||||
echo "<p><strong>Total invoices in table:</strong> {$count['cnt']}</p>\n";
|
||||
|
||||
// Show last 5 invoices
|
||||
echo "<h2>Last 5 Invoices</h2>\n";
|
||||
$last_result = mysqli_query($db, "SELECT * FROM ogp_billing_invoices ORDER BY invoice_id DESC LIMIT 5");
|
||||
|
||||
if (mysqli_num_rows($last_result) > 0) {
|
||||
echo "<table border='1' style='border-collapse: collapse;'>\n";
|
||||
echo "<tr>";
|
||||
$first = true;
|
||||
while ($row = mysqli_fetch_assoc($last_result)) {
|
||||
if ($first) {
|
||||
foreach (array_keys($row) as $col) {
|
||||
echo "<th>{$col}</th>";
|
||||
}
|
||||
echo "</tr>\n";
|
||||
$first = false;
|
||||
mysqli_data_seek($last_result, 0);
|
||||
}
|
||||
}
|
||||
|
||||
while ($row = mysqli_fetch_assoc($last_result)) {
|
||||
echo "<tr>";
|
||||
foreach ($row as $val) {
|
||||
echo "<td>" . htmlspecialchars($val ?? 'NULL') . "</td>";
|
||||
}
|
||||
echo "</tr>\n";
|
||||
}
|
||||
echo "</table>\n";
|
||||
} else {
|
||||
echo "<p>No invoices found.</p>\n";
|
||||
}
|
||||
|
||||
mysqli_close($db);
|
||||
?>
|
||||
33
modules/billing/create_invoices_table.sql
Normal file
33
modules/billing/create_invoices_table.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- Create billing_invoices table for invoice-first flow
|
||||
-- Run this SQL to enable the new billing system
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ogp_billing_invoices` (
|
||||
`invoice_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`order_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`service_id` INT(11) NOT NULL,
|
||||
`home_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`ip` INT(11) NOT NULL DEFAULT 0,
|
||||
`max_players` INT(11) NOT NULL DEFAULT 0,
|
||||
`remote_control_password` VARCHAR(255) NULL,
|
||||
`ftp_password` VARCHAR(255) NULL,
|
||||
`customer_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`customer_email` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`amount` FLOAT(15,2) NOT NULL DEFAULT 0,
|
||||
`currency` VARCHAR(3) NOT NULL DEFAULT 'USD',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'due',
|
||||
`invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`due_date` DATETIME NULL,
|
||||
`paid_date` DATETIME NULL,
|
||||
`payment_txid` VARCHAR(255) NULL,
|
||||
`payment_method` VARCHAR(50) NULL,
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month',
|
||||
`qty` INT(11) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`invoice_id`),
|
||||
KEY `order_id` (`order_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `status` (`status`),
|
||||
KEY `due_date` (`due_date`),
|
||||
KEY `service_id` (`service_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
||||
|
||||
//these dates are configured in the Shop Settings page
|
||||
// Date calculations
|
||||
$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);
|
||||
$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);
|
||||
|
||||
$db->logger("BILLING-CRON: Server lifecycle automation running at " . $rundate);
|
||||
|
||||
// ==================================================================================
|
||||
// STEP 1: CREATE RENEWAL INVOICES FOR SERVERS EXPIRING IN 7 DAYS
|
||||
// ==================================================================================
|
||||
// Find all ACTIVE servers (installed) that expire within 7 days and don't have an unpaid invoice
|
||||
$upcoming_expirations = $db->resultQuery("
|
||||
SELECT o.*, u.users_email, u.users_fname, u.users_lname
|
||||
FROM " . $table_prefix . "billing_orders o
|
||||
LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id
|
||||
WHERE o.status = 'installed'
|
||||
AND o.end_date IS NOT NULL
|
||||
AND UNIX_TIMESTAMP(o.end_date) < {$invoice_date}
|
||||
AND UNIX_TIMESTAMP(o.end_date) > {$today}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM " . $table_prefix . "billing_invoices i
|
||||
WHERE i.order_id = o.order_id AND i.status = 'unpaid'
|
||||
)
|
||||
");
|
||||
|
||||
if (is_array($upcoming_expirations)) {
|
||||
foreach ($upcoming_expirations as $order) {
|
||||
$user_id = $order['user_id'];
|
||||
$order_id = $order['order_id'];
|
||||
$home_id = $order['home_id'];
|
||||
$customer_name = trim(($order['users_fname'] ?? '') . ' ' . ($order['users_lname'] ?? ''));
|
||||
$customer_email = $order['users_email'] ?? '';
|
||||
|
||||
// Create renewal invoice
|
||||
$invoice_desc = "Renewal for " . $order['home_name'];
|
||||
$due_date = date('Y-m-d H:i:s', strtotime($order['end_date']));
|
||||
|
||||
$db->query("INSERT INTO " . $table_prefix . "billing_invoices
|
||||
(order_id, user_id, customer_name, customer_email, amount, currency, status,
|
||||
invoice_date, due_date, description, invoice_duration, qty)
|
||||
VALUES (
|
||||
{$order_id},
|
||||
{$user_id},
|
||||
'" . $db->realEscapeSingle($customer_name) . "',
|
||||
'" . $db->realEscapeSingle($customer_email) . "',
|
||||
" . floatval($order['price']) . ",
|
||||
'USD',
|
||||
'unpaid',
|
||||
NOW(),
|
||||
'" . $db->realEscapeSingle($due_date) . "',
|
||||
'" . $db->realEscapeSingle($invoice_desc) . "',
|
||||
'" . $db->realEscapeSingle($order['invoice_duration']) . "',
|
||||
" . intval($order['qty']) . "
|
||||
)");
|
||||
|
||||
// Send renewal notice email
|
||||
$settings = $db->getSettings();
|
||||
$subject = "Renewal Invoice for " . $order['home_name'] . " - " . $panel_settings['panel_name'];
|
||||
$message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) will expire on " .
|
||||
date('F j, Y', strtotime($order['end_date'])) .
|
||||
".<br><br>A renewal invoice has been created. Please log in to your account and pay the invoice to continue your service." .
|
||||
"<br><br>Amount Due: $" . number_format($order['price'], 2) .
|
||||
"<br>Due Date: " . date('F j, Y', strtotime($order['end_date'])) .
|
||||
"<br><br>Thank you for your business!<br>";
|
||||
|
||||
$mail = mymail($customer_email, $subject, $message, $settings);
|
||||
|
||||
$db->logger("BILLING-CRON: Created renewal invoice for order {$order_id}, home {$home_id}");
|
||||
|
||||
if (!$mail) {
|
||||
$db->logger("BILLING-CRON: Email FAILED - Renewal invoice for order {$order_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================================
|
||||
// STEP 2: SUSPEND SERVERS THAT ARE EXPIRED AND HAVE UNPAID INVOICES
|
||||
// ==================================================================================
|
||||
// Find servers that:
|
||||
// - Are currently installed (active)
|
||||
// - Have passed their end_date
|
||||
// - Have at least one unpaid invoice
|
||||
$servers_to_suspend = $db->resultQuery("
|
||||
SELECT DISTINCT o.*, u.users_email
|
||||
FROM " . $table_prefix . "billing_orders o
|
||||
LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id
|
||||
INNER JOIN " . $table_prefix . "billing_invoices i ON o.order_id = i.order_id
|
||||
WHERE o.status = 'installed'
|
||||
AND o.end_date IS NOT NULL
|
||||
AND UNIX_TIMESTAMP(o.end_date) < {$suspend_date}
|
||||
AND i.status = 'unpaid'
|
||||
");
|
||||
|
||||
if (is_array($servers_to_suspend)) {
|
||||
foreach ($servers_to_suspend as $order) {
|
||||
$user_id = $order['user_id'];
|
||||
$home_id = $order['home_id'];
|
||||
$order_id = $order['order_id'];
|
||||
|
||||
// Get home and server info
|
||||
$home_info = $db->getGameHomeWithoutMods($home_id);
|
||||
if (!$home_info) {
|
||||
$db->logger("BILLING-CRON: WARNING - Home {$home_id} not found for order {$order_id}, marking suspended anyway");
|
||||
$db->query("UPDATE " . $table_prefix . "billing_orders SET status='suspended' WHERE order_id={$order_id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
|
||||
$remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'],
|
||||
$server_info['encryption_key'], $server_info['timeout']);
|
||||
|
||||
// Disable FTP
|
||||
$ftp_login = isset($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id;
|
||||
$remote->ftp_mgr("userdel", $ftp_login);
|
||||
$db->changeFtpStatus('disabled', $home_id);
|
||||
|
||||
// Stop the server
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
$control_type = isset($server_xml->control_protocol_type) ? $server_xml->control_protocol_type : "";
|
||||
$addresses = $db->getHomeIpPorts($home_id);
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
$remote->remote_stop_server($home_id, $address['ip'], $address['port'],
|
||||
$server_xml->control_protocol, $home_info['control_password'],
|
||||
$control_type, $home_info['home_path']);
|
||||
}
|
||||
|
||||
// Unassign from user
|
||||
$db->unassignHomeFrom("user", $user_id, $home_id);
|
||||
|
||||
// Update order status
|
||||
$db->query("UPDATE " . $table_prefix . "billing_orders SET status='suspended' WHERE order_id={$order_id}");
|
||||
|
||||
$db->logger("BILLING-CRON: SUSPENDED server {$home_id} for order {$order_id} due to unpaid invoice");
|
||||
|
||||
// Send suspension email
|
||||
$settings = $db->getSettings();
|
||||
$subject = "Server Suspended - " . $order['home_name'] . " - " . $panel_settings['panel_name'];
|
||||
$message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) has been suspended due to non-payment." .
|
||||
"<br><br>Your server has been stopped and will be permanently deleted in 7 days if payment is not received." .
|
||||
"<br><br>Please log in to your account and pay your outstanding invoice to restore your server." .
|
||||
"<br><br>Thank you.";
|
||||
|
||||
$mail = mymail($order['users_email'], $subject, $message, $settings);
|
||||
|
||||
if (!$mail) {
|
||||
$db->logger("BILLING-CRON: Email FAILED - Suspension notice for order {$order_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================================
|
||||
// STEP 3: DELETE SERVERS THAT HAVE BEEN SUSPENDED FOR 7+ DAYS
|
||||
// ==================================================================================
|
||||
// Find servers that:
|
||||
// - Are currently suspended
|
||||
// - Have been suspended for at least 7 days (end_date + 7 days has passed)
|
||||
// - Still have unpaid invoices
|
||||
$servers_to_delete = $db->resultQuery("
|
||||
SELECT DISTINCT o.*, u.users_email
|
||||
FROM " . $table_prefix . "billing_orders o
|
||||
LEFT JOIN " . $table_prefix . "users u ON o.user_id = u.user_id
|
||||
INNER JOIN " . $table_prefix . "billing_invoices i ON o.order_id = i.order_id
|
||||
WHERE o.status = 'suspended'
|
||||
AND o.end_date IS NOT NULL
|
||||
AND UNIX_TIMESTAMP(o.end_date) < {$removal_date}
|
||||
AND i.status = 'unpaid'
|
||||
");
|
||||
|
||||
if (is_array($servers_to_delete)) {
|
||||
foreach ($servers_to_delete as $order) {
|
||||
$user_id = $order['user_id'];
|
||||
$home_id = $order['home_id'];
|
||||
$order_id = $order['order_id'];
|
||||
|
||||
// Get home and server info
|
||||
$home_info = $db->getGameHomeWithoutMods($home_id);
|
||||
if ($home_info) {
|
||||
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
|
||||
$remote = new OGPRemoteLibrary($server_info['agent_ip'], $server_info['agent_port'],
|
||||
$server_info['encryption_key'], $server_info['timeout']);
|
||||
|
||||
// Remove the game home from db
|
||||
$db->deleteGameHome($home_id);
|
||||
|
||||
// Remove the game home files from remote server
|
||||
$remote->remove_home($home_info['home_path']);
|
||||
|
||||
// Drop database and user if they exist
|
||||
@$db->query("DROP USER 'server_" . $home_id . "'@'%'");
|
||||
@$db->query("DROP USER 'server_" . $home_id . "'@'localhost'");
|
||||
@$db->query("DROP DATABASE IF EXISTS server_" . $home_id);
|
||||
}
|
||||
|
||||
// Update order status and clear home_id
|
||||
$db->query("UPDATE " . $table_prefix . "billing_orders
|
||||
SET status='deleted', home_id='0'
|
||||
WHERE order_id={$order_id}");
|
||||
|
||||
$db->logger("BILLING-CRON: DELETED server {$home_id} for order {$order_id} after 7 days suspended");
|
||||
|
||||
// Send deletion email
|
||||
$settings = $db->getSettings();
|
||||
$subject = "Server Permanently Deleted - " . $order['home_name'] . " - " . $panel_settings['panel_name'];
|
||||
$message = "Your server '" . $order['home_name'] . "' (ID: {$home_id}) has been permanently deleted." .
|
||||
"<br><br>The server was suspended 7 days ago due to non-payment and has now been removed." .
|
||||
"<br><br>If this was an error and you contact us immediately, we may be able to restore your server from backups." .
|
||||
"<br><br>Thank you for being a customer. We hope to serve you again in the future.";
|
||||
|
||||
$mail = mymail($order['users_email'], $subject, $message, $settings);
|
||||
|
||||
if (!$mail) {
|
||||
$db->logger("BILLING-CRON: Email FAILED - Deletion notice for order {$order_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$db->logger("BILLING-CRON: Server lifecycle automation completed");
|
||||
?>
|
||||
|
||||
|
||||
//THESE SERVERS HAVE REACHED THE DATE FOR INVOICE, FINISH_DATE - 7 (OR WHAT IS IN SETTINGS)
|
||||
//THESE SERVERS HAVE REACHED THE DATE FOR INVOICE, END_DATE - 7 (OR WHAT IS IN SETTINGS)
|
||||
//SET STATUS 'invoiced' MEANING INVOICE SHOULD BE CREATED
|
||||
//LOOP THROUGH ALL SERVERS WITH STATUS = 'paid' OR 'installed' (ACTIVE) -----------------------------------------------------------
|
||||
$user_homes = $db->resultQuery( "SELECT *
|
||||
FROM " . $table_prefix . "billing_orders
|
||||
WHERE status IN ('paid', 'installed') AND finish_date <" . $invoice_date);
|
||||
WHERE status IN ('paid', 'installed') AND end_date <" . $invoice_date);
|
||||
|
||||
if (!is_array($user_homes))
|
||||
{
|
||||
|
|
@ -84,10 +298,10 @@ else
|
|||
// Developer note:
|
||||
// In future we may want to change the renewal/invoice strategy so that a
|
||||
// new order record is created for the renewal (leaving the original order
|
||||
// intact) instead of mutating the existing order's status/finish_date.
|
||||
// intact) instead of mutating the existing order's status/end_date.
|
||||
// Creating a separate renewal order gives a clearer, immutable purchase
|
||||
// history and simplifies auditing. For now this cron job continues to
|
||||
// update the existing order (change status/finish_date) as implemented
|
||||
// update the existing order (change status/end_date) as implemented
|
||||
// below.
|
||||
|
||||
$user_id = $user_home['user_id'];
|
||||
|
|
@ -119,12 +333,12 @@ else
|
|||
}
|
||||
}
|
||||
|
||||
//THESE ARE THE SERVERS THAT HAVE NOT BEEN PAID AND THE FINISH_DATE IS TODAY
|
||||
//THESE ARE THE SERVERS THAT HAVE NOT BEEN PAID AND THE END_DATE IS TODAY
|
||||
//THESE SERVERS GET SUSPENDED
|
||||
//LOOP THROUGH ALL ORDERS WITH STATUS 'invoiced' OR 'in-cart' OR 'unknown' (INACTIVE OR INVOICED)
|
||||
$user_homes = $db->resultQuery( "SELECT *
|
||||
FROM " . $table_prefix . "billing_orders
|
||||
WHERE status IN ('invoiced', 'in-cart', 'unknown') AND finish_date < ".$today);
|
||||
WHERE status IN ('invoiced', 'in-cart', 'unknown') AND end_date < ".$today);
|
||||
|
||||
if (!is_array($user_homes))
|
||||
{
|
||||
|
|
@ -178,7 +392,7 @@ else
|
|||
//set removed servers as 'deleted'
|
||||
$user_homes = $db->resultQuery( "SELECT *
|
||||
FROM " . $table_prefix . "billing_orders
|
||||
WHERE status = 'suspended' AND finish_date < ".$removal_date );
|
||||
WHERE status = 'suspended' AND end_date < ".$removal_date );
|
||||
|
||||
if (!is_array($user_homes))
|
||||
{
|
||||
|
|
|
|||
204
modules/billing/fix_invoices_table_columns.sql
Normal file
204
modules/billing/fix_invoices_table_columns.sql
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
-- Fix missing columns / indexes for ogp_billing_invoices
|
||||
-- Safe script: checks information_schema and adds each missing column/index using prepared statements.
|
||||
-- IMPORTANT: Run on the target database (use the panel DB). Make a backup before running.
|
||||
|
||||
-- Use the current database
|
||||
SET @db = DATABASE();
|
||||
SET @tbl = 'ogp_billing_invoices';
|
||||
|
||||
-- Helper: add a column if missing
|
||||
-- Usage pattern below; repeated for every column we expect from module.php
|
||||
|
||||
-- 1) service_id
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'service_id';
|
||||
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 2) home_name
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'home_name';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `home_name` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `service_id`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 3) ip
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'ip';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `ip` INT(11) NOT NULL DEFAULT 0 AFTER `home_name`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 4) max_players
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'max_players';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `max_players` INT(11) NOT NULL DEFAULT 0 AFTER `ip`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 5) remote_control_password
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'remote_control_password';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `remote_control_password` VARCHAR(255) NULL AFTER `max_players`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 6) ftp_password
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'ftp_password';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `ftp_password` VARCHAR(255) NULL AFTER `remote_control_password`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 7) customer_name
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'customer_name';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `customer_name` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `ftp_password`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 8) customer_email
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'customer_email';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `customer_email` VARCHAR(255) NOT NULL DEFAULT '''' AFTER `customer_name`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 9) amount
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'amount';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `amount` FLOAT(15,2) NOT NULL DEFAULT 0 AFTER `customer_email`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 10) currency
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'currency';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `currency` VARCHAR(3) NOT NULL DEFAULT ''USD'' AFTER `amount`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 11) status
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'status';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT ''due'' AFTER `currency`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 12) invoice_date
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'invoice_date';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 13) due_date
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'due_date';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `due_date` DATETIME NULL AFTER `invoice_date`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 14) paid_date
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'paid_date';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `paid_date` DATETIME NULL AFTER `due_date`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 15) payment_txid
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'payment_txid';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `payment_txid` VARCHAR(255) NULL AFTER `paid_date`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 16) payment_method
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'payment_method';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `payment_method` VARCHAR(50) NULL AFTER `payment_txid`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 17) description
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'description';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `description` VARCHAR(500) NOT NULL DEFAULT '''' AFTER `payment_method`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 18) invoice_duration
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'invoice_duration';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `invoice_duration` VARCHAR(16) NOT NULL DEFAULT ''month'' AFTER `description`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 19) qty
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qty';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qty` INT(11) NOT NULL DEFAULT 1 AFTER `invoice_duration`');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- 20) indexes: service_id, order_id, user_id, status, due_date
|
||||
-- Add index helper
|
||||
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'service_id';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `service_id` (`service_id`)');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'order_id';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `order_id` (`order_id`)');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'user_id';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `user_id` (`user_id`)');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'status';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `status` (`status`)');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND INDEX_NAME = 'due_date';
|
||||
IF @cnt = 0 THEN
|
||||
SET @s = CONCAT('ALTER TABLE `', @tbl, '` ADD KEY `due_date` (`due_date`)');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
|
||||
-- Done
|
||||
SELECT 'done' as status;
|
||||
175
modules/billing/migration_to_invoices.sql
Normal file
175
modules/billing/migration_to_invoices.sql
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
-- Migration Script: Billing System with Invoice Table
|
||||
-- This script upgrades existing billing installations to the new invoice-based system
|
||||
-- Run this ONCE on existing installations (not needed for fresh installs)
|
||||
-- Compatible with MySQL 5.7+ and MariaDB 10.2+
|
||||
|
||||
-- Step 1: Add new columns to billing_orders (only if they don't exist)
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'ogp_billing_orders';
|
||||
|
||||
-- Add order_date column
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'order_date';
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `ogp_billing_orders` ADD COLUMN `order_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`',
|
||||
'SELECT "Column order_date already exists" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Add payment_txid column
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'payment_txid';
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `ogp_billing_orders` ADD COLUMN `payment_txid` VARCHAR(255) NULL AFTER `end_date`',
|
||||
'SELECT "Column payment_txid already exists" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Add paid_ts column
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'paid_ts';
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `ogp_billing_orders` ADD COLUMN `paid_ts` DATETIME NULL AFTER `payment_txid`',
|
||||
'SELECT "Column paid_ts already exists" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Step 2: Modify existing columns to use proper data types
|
||||
ALTER TABLE `ogp_billing_orders`
|
||||
MODIFY COLUMN `status` VARCHAR(16) NOT NULL DEFAULT 'in-cart',
|
||||
MODIFY COLUMN `remote_control_password` VARCHAR(255) NULL,
|
||||
MODIFY COLUMN `ftp_password` VARCHAR(255) NULL;
|
||||
|
||||
-- Convert end_date from VARCHAR to DATETIME (handle existing data)
|
||||
-- First, update any '0' values to NULL
|
||||
UPDATE `ogp_billing_orders` SET `end_date` = NULL WHERE `end_date` = '0' OR `end_date` = '';
|
||||
|
||||
-- Check current end_date type and convert if needed
|
||||
SET @col_type = '';
|
||||
SELECT DATA_TYPE INTO @col_type FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'end_date';
|
||||
|
||||
SET @sql = IF(@col_type = 'varchar',
|
||||
'ALTER TABLE `ogp_billing_orders` MODIFY COLUMN `end_date` DATETIME NULL',
|
||||
'SELECT "Column end_date already DATETIME" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Step 3: Remove obsolete columns from billing_orders
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'cart_id';
|
||||
SET @sql = IF(@col_exists > 0,
|
||||
'ALTER TABLE `ogp_billing_orders` DROP COLUMN `cart_id`',
|
||||
'SELECT "Column cart_id already removed" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = 'extended';
|
||||
SET @sql = IF(@col_exists > 0,
|
||||
'ALTER TABLE `ogp_billing_orders` DROP COLUMN `extended`',
|
||||
'SELECT "Column extended already removed" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Step 4: Add indexes to billing_orders for better performance
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_user_id';
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_user_id` (`user_id`)',
|
||||
'SELECT "Index idx_user_id already exists" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_status';
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_status` (`status`)',
|
||||
'SELECT "Index idx_status already exists" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = 'idx_home_id';
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `ogp_billing_orders` ADD INDEX `idx_home_id` (`home_id`)',
|
||||
'SELECT "Index idx_home_id already exists" AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
|
||||
-- Step 5: Create the new billing_invoices table
|
||||
CREATE TABLE IF NOT EXISTS `ogp_billing_invoices` (
|
||||
`invoice_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`order_id` INT(11) NOT NULL,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`customer_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`customer_email` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`amount` FLOAT(15,2) NOT NULL DEFAULT 0,
|
||||
`currency` VARCHAR(3) NOT NULL DEFAULT 'USD',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'unpaid',
|
||||
`invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`due_date` DATETIME NULL,
|
||||
`paid_date` DATETIME NULL,
|
||||
`payment_txid` VARCHAR(255) NULL,
|
||||
`payment_method` VARCHAR(50) NULL,
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month',
|
||||
`qty` INT(11) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`invoice_id`),
|
||||
KEY `order_id` (`order_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `status` (`status`),
|
||||
KEY `due_date` (`due_date`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Step 6: Migrate existing paid orders to create initial invoices
|
||||
-- This creates a historical invoice for each paid/installed order
|
||||
INSERT INTO `ogp_billing_invoices`
|
||||
(`order_id`, `user_id`, `customer_name`, `customer_email`, `amount`, `currency`, `status`, `invoice_date`, `paid_date`, `payment_txid`, `description`, `invoice_duration`, `qty`)
|
||||
SELECT
|
||||
o.order_id,
|
||||
o.user_id,
|
||||
CONCAT(COALESCE(u.users_fname, ''), ' ', COALESCE(u.users_lname, '')) AS customer_name,
|
||||
u.users_email AS customer_email,
|
||||
o.price AS amount,
|
||||
'USD' AS currency,
|
||||
'paid' AS status,
|
||||
COALESCE(o.order_date, NOW()) AS invoice_date,
|
||||
COALESCE(o.paid_ts, o.order_date, NOW()) AS paid_date,
|
||||
o.payment_txid,
|
||||
CONCAT('Initial invoice for ', o.home_name) AS description,
|
||||
o.invoice_duration,
|
||||
o.qty
|
||||
FROM `ogp_billing_orders` o
|
||||
LEFT JOIN `ogp_users` u ON o.user_id = u.user_id
|
||||
WHERE o.status IN ('paid', 'installed')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `ogp_billing_invoices` i
|
||||
WHERE i.order_id = o.order_id AND i.status = 'paid'
|
||||
);
|
||||
|
||||
-- Step 7: Drop the obsolete billing_carts table (replaced by invoice system)
|
||||
DROP TABLE IF EXISTS `ogp_billing_carts`;
|
||||
|
||||
-- Step 8: Update billing_services charset for consistency
|
||||
ALTER TABLE `ogp_billing_services` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
SELECT 'Migration completed successfully! Invoice-based billing system is now active.' AS Status;
|
||||
|
|
@ -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;",
|
||||
`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;",
|
||||
|
||||
"DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_orders`;",
|
||||
// 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;",
|
||||
|
||||
$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;"
|
||||
// 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[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`;"
|
||||
);
|
||||
|
||||
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ $servers_query = "SELECT
|
|||
o.price,
|
||||
o.invoice_duration,
|
||||
o.home_id,
|
||||
o.finish_date,
|
||||
o.end_date,
|
||||
bs.service_name
|
||||
FROM ogp_billing_orders o
|
||||
LEFT JOIN ogp_billing_services bs ON o.service_id = bs.service_id
|
||||
|
|
@ -325,7 +325,7 @@ $status_config = [
|
|||
</div>
|
||||
<div class="server-detail">
|
||||
<span class="server-detail-label">Expires:</span>
|
||||
<span class="server-detail-value"><?php echo !empty($server['finish_date']) && $server['finish_date'] != '0' ? date('M d, Y', strtotime($server['finish_date'])) : 'N/A'; ?></span>
|
||||
<span class="server-detail-value"><?php echo !empty($server['end_date']) && $server['end_date'] != '0' ? date('M d, Y', strtotime($server['end_date'])) : 'N/A'; ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-actions">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
51
modules/billing/payment_cancel.php
Normal file
51
modules/billing/payment_cancel.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
/**
|
||||
* Payment Cancelled Page
|
||||
* User lands here if they cancel the PayPal payment
|
||||
*/
|
||||
|
||||
session_start();
|
||||
require_once(__DIR__ . '/includes/header.php');
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
|
||||
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment Cancelled - Game Server Panel</title>
|
||||
<link rel="stylesheet" href="includes/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container" style="max-width: 800px; margin: 40px auto; padding: 20px;">
|
||||
<div class="warning-box" style="background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
|
||||
<h1 style="margin-top: 0;">Payment Cancelled</h1>
|
||||
<p>Your payment was cancelled. No charges have been made to your account.</p>
|
||||
<?php if ($invoice_ref): ?>
|
||||
<p><strong>Invoice Reference:</strong> <?php echo htmlspecialchars($invoice_ref); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="info-box" style="background: #f8f9fa; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
|
||||
<h2>What would you like to do?</h2>
|
||||
<ul>
|
||||
<li><strong>Return to Cart:</strong> Your items are still in your cart. You can complete the payment anytime.</li>
|
||||
<li><strong>Continue Shopping:</strong> Browse our game server options and add more to your cart.</li>
|
||||
<li><strong>Need Help?:</strong> Contact our support team if you encountered any issues during checkout.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="margin-top: 30px; text-align: center;">
|
||||
<a href="cart.php" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">Return to Cart</a>
|
||||
<a href="order.php" style="display: inline-block; padding: 12px 24px; background: #28a745; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">Continue Shopping</a>
|
||||
<a href="support.php" style="display: inline-block; padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 5px;">Contact Support</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,54 +1,94 @@
|
|||
<?php
|
||||
// Helper to process a persisted payment record and mark orders paid in panel DB.
|
||||
// Usage: require_once(__DIR__ . '/payment_success.php'); process_payment_record($record);
|
||||
/**
|
||||
* Payment Success Page
|
||||
* User lands here after successful PayPal payment
|
||||
*/
|
||||
|
||||
function process_payment_record(array $record) {
|
||||
// Minimal validation
|
||||
$invoice = $record['invoice'] ?? '';
|
||||
$custom = $record['custom'] ?? '';
|
||||
$txid = $record['resource_id'] ?? '';
|
||||
$ts = $record['ts'] ?? date('c');
|
||||
session_start();
|
||||
require_once(__DIR__ . '/includes/header.php');
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
require_once(__DIR__ . '/../../includes/database_mysqli.php');
|
||||
|
||||
// Attempt DB update using site DB config
|
||||
// This file lives in _website/, config is in includes/config.inc.php
|
||||
$cfg = __DIR__ . '/includes/config.inc.php';
|
||||
if (!is_file($cfg)) {
|
||||
error_log('[payment_success] missing config: ' . $cfg);
|
||||
return false;
|
||||
}
|
||||
require_once($cfg);
|
||||
// include site logging helper if available
|
||||
if (is_file(__DIR__ . '/includes/log.php')) require_once(__DIR__ . '/includes/log.php');
|
||||
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
|
||||
$user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
|
||||
|
||||
// Use variables from config.inc.php: $db_host, $db_user, $db_pass, $db_name
|
||||
$db = @mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
||||
if (!$db) {
|
||||
if (function_exists('site_log_error')) site_log_error('payment_success_db_connect_failed', ['error'=>mysqli_connect_error()]);
|
||||
else error_log('[payment_success] DB connect failed: ' . mysqli_connect_error());
|
||||
return false;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment Successful - Game Server Panel</title>
|
||||
<link rel="stylesheet" href="includes/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container" style="max-width: 800px; margin: 40px auto; padding: 20px;">
|
||||
<div class="success-box" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
|
||||
<h1 style="margin-top: 0;">✓ Payment Successful!</h1>
|
||||
<p>Thank you for your purchase. Your payment has been received and is being processed.</p>
|
||||
<?php if ($invoice_ref): ?>
|
||||
<p><strong>Invoice Reference:</strong> <?php echo htmlspecialchars($invoice_ref); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="info-box" style="background: #f8f9fa; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
|
||||
<h2>What happens next?</h2>
|
||||
<ol>
|
||||
<li><strong>Payment Confirmation:</strong> Your payment has been captured by PayPal</li>
|
||||
<li><strong>Order Creation:</strong> Your game server order has been created</li>
|
||||
<li><strong>Server Provisioning:</strong> Your server will be provisioned automatically (this may take a few minutes)</li>
|
||||
<li><strong>Email Notification:</strong> You'll receive an email with your server details and login credentials</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Show user's recent orders
|
||||
if ($user_id > 0) {
|
||||
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
|
||||
if ($db) {
|
||||
$result = mysqli_query($db, "SELECT * FROM ogp_billing_orders WHERE user_id=$user_id ORDER BY order_date DESC LIMIT 5");
|
||||
if ($result && mysqli_num_rows($result) > 0) {
|
||||
echo '<div class="orders-box" style="background: #fff; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px;">';
|
||||
echo '<h2>Your Recent Orders</h2>';
|
||||
echo '<table style="width: 100%; border-collapse: collapse;">';
|
||||
echo '<thead><tr style="background: #f8f9fa;">';
|
||||
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Order ID</th>';
|
||||
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Server</th>';
|
||||
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Status</th>';
|
||||
echo '<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Date</th>';
|
||||
echo '<th style="padding: 10px; text-align: right; border-bottom: 2px solid #dee2e6;">Price</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
while ($order = mysqli_fetch_assoc($result)) {
|
||||
$statusColor = $order['status'] === 'paid' ? '#28a745' : '#6c757d';
|
||||
echo '<tr style="border-bottom: 1px solid #dee2e6;">';
|
||||
echo '<td style="padding: 10px;">#' . htmlspecialchars($order['order_id']) . '</td>';
|
||||
echo '<td style="padding: 10px;">' . htmlspecialchars($order['home_name']) . '</td>';
|
||||
echo '<td style="padding: 10px;"><span style="color: ' . $statusColor . '; font-weight: bold;">' . htmlspecialchars(ucfirst($order['status'])) . '</span></td>';
|
||||
echo '<td style="padding: 10px;">' . htmlspecialchars($order['order_date']) . '</td>';
|
||||
echo '<td style="padding: 10px; text-align: right;">$' . htmlspecialchars(number_format($order['price'], 2)) . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
// 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) . ' = ?';
|
||||
echo '</tbody></table>';
|
||||
echo '</div>';
|
||||
}
|
||||
// placeholder for finish_date; we'll append it if we can compute it
|
||||
$sql .= ' WHERE ' . $where_sql . ' AND status <> "paid" LIMIT 1';
|
||||
mysqli_close($db);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
// If we need finish_date, attempt to compute it by selecting the row first
|
||||
$finish_date_val = null;
|
||||
<div class="actions" style="margin-top: 30px; text-align: center;">
|
||||
<a href="my_account.php" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">View My Servers</a>
|
||||
<a href="order.php" style="display: inline-block; padding: 12px 24px; background: #28a745; color: white; text-decoration: none; border-radius: 5px;">Order Another Server</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
$end_date_val = null;
|
||||
if ($has_finish) {
|
||||
// Attempt to find the target order's qty/invoice_duration using the same where clause but without LIMIT
|
||||
$sel_sql = "SELECT qty, invoice_duration FROM ogp_billing_orders WHERE " . str_replace(' AND status <> \"paid\" LIMIT 1', '', $where_sql) . " LIMIT 1";
|
||||
|
|
@ -77,17 +117,17 @@ function process_payment_record(array $record) {
|
|||
if ($months <= 0) $months = 0;
|
||||
$dt = new DateTime('now');
|
||||
if ($months > 0) $dt->modify('+' . intval($months) . ' months');
|
||||
$finish_date_val = $dt->format('Y-m-d H:i:s');
|
||||
$end_date_val = $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
$sel_stmt->close();
|
||||
}
|
||||
if ($finish_date_val !== null) {
|
||||
$sql = str_replace(' WHERE ', ', finish_date = ? WHERE ', $sql);
|
||||
if ($end_date_val !== null) {
|
||||
$sql = str_replace(' WHERE ', ', end_date = ? WHERE ', $sql);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt = $db->prepare($sql)) {
|
||||
// Build params: first any where params, then txid/ts values if present, then finish_date if present
|
||||
// Build params: first any where params, then txid/ts values if present, then end_date if present
|
||||
$types = $bind_types;
|
||||
$vals = $bind_vals;
|
||||
if ($cols) {
|
||||
|
|
@ -97,9 +137,9 @@ function process_payment_record(array $record) {
|
|||
else $vals[] = $ts;
|
||||
}
|
||||
}
|
||||
if ($finish_date_val !== null) {
|
||||
if ($end_date_val !== null) {
|
||||
$types .= 's';
|
||||
$vals[] = $finish_date_val;
|
||||
$vals[] = $end_date_val;
|
||||
}
|
||||
// bind dynamically
|
||||
if ($types) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue