Merge pull request #33 from GameServerPanel/copilot/fix-invoice-cart-disconnect
Fix invoice/order payment flow: JSON corruption, cart persistence, and missing payment processing
This commit is contained in:
commit
5437e09b7e
7 changed files with 854 additions and 138 deletions
242
modules/billing/BILLING_FIX_SUMMARY.md
Normal file
242
modules/billing/BILLING_FIX_SUMMARY.md
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
# Billing Invoice/Order Flow - Fix Summary
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The billing system had several critical issues:
|
||||||
|
|
||||||
|
1. **JSON Error**: "Failed to execute 'json' on 'Response': Unexpected end of JSON input" when returning from PayPal payment
|
||||||
|
2. **Cart not clearing**: Items remained in cart after payment (invoices stayed as status='due')
|
||||||
|
3. **No order creation**: Orders were not being created after successful payment
|
||||||
|
4. **Missing renewal flow**: Renewal invoices (linked to existing orders) were not handled
|
||||||
|
5. **Free button errors**: The free/claim button was also experiencing errors
|
||||||
|
|
||||||
|
## Invoice-First Flow (Intended Design)
|
||||||
|
|
||||||
|
The system uses an invoice-first architecture:
|
||||||
|
|
||||||
|
1. **Add to Cart**: Creates INVOICE with status='due', order_id=0 (no order yet)
|
||||||
|
2. **View Cart**: Shows all invoices WHERE status='due'
|
||||||
|
3. **Payment**:
|
||||||
|
- For NEW orders (order_id=0): Mark invoice paid + CREATE new order
|
||||||
|
- For RENEWALS (order_id>0): Mark invoice paid + EXTEND existing order's end_date
|
||||||
|
4. **Provisioning**: Separate step that provisions servers for paid orders
|
||||||
|
|
||||||
|
## Root Causes Identified
|
||||||
|
|
||||||
|
### 1. Missing Function
|
||||||
|
- `process_payment_record()` was called but never defined
|
||||||
|
- Referenced in webhook.php, cart.php (free button), but didn't exist
|
||||||
|
- This prevented any payment processing from completing
|
||||||
|
|
||||||
|
### 2. JSON Response Corruption
|
||||||
|
- `capture_order.php` had PHP errors/warnings during DB operations
|
||||||
|
- These were being output to the response, corrupting the JSON
|
||||||
|
- JavaScript couldn't parse the malformed JSON → "Unexpected end of JSON input"
|
||||||
|
|
||||||
|
### 3. Incomplete Payment Processing
|
||||||
|
- `capture_order.php` was supposed to:
|
||||||
|
- Mark invoices as paid (status: 'due' → 'paid')
|
||||||
|
- Create new orders OR extend existing orders
|
||||||
|
- Link invoices to orders
|
||||||
|
- But the logic was incomplete and had issues
|
||||||
|
|
||||||
|
### 4. Session Compatibility
|
||||||
|
- capture_order.php used `$_SESSION['user_id']`
|
||||||
|
- cart.php used `$_SESSION['website_user_id']`
|
||||||
|
- This mismatch meant user couldn't be identified for payment processing
|
||||||
|
|
||||||
|
### 5. Hardcoded Table Names
|
||||||
|
- capture_order.php used hardcoded "ogp_billing_invoices" and "ogp_billing_orders"
|
||||||
|
- Should use `$table_prefix . "billing_invoices"` for flexibility
|
||||||
|
- Could cause failures if table prefix is different
|
||||||
|
|
||||||
|
## Solutions Implemented
|
||||||
|
|
||||||
|
### 1. Created payment_processor.php Helper
|
||||||
|
**File**: `modules/billing/includes/payment_processor.php`
|
||||||
|
|
||||||
|
**Function**: `process_payment_record($record)`
|
||||||
|
- Accepts payment record from webhook or direct capture
|
||||||
|
- Finds invoices to process by custom_id (invoice_id) or invoice reference
|
||||||
|
- For each invoice:
|
||||||
|
- Marks invoice as paid (status='due' → 'paid')
|
||||||
|
- If NEW order (order_id=0): Creates new order with calculated end_date
|
||||||
|
- If RENEWAL (order_id>0): Extends existing order's end_date by invoice duration
|
||||||
|
- Links invoice to order
|
||||||
|
- Returns true/false and logs all operations
|
||||||
|
- No HTML output (safe to require from webhook/API endpoints)
|
||||||
|
|
||||||
|
### 2. Fixed capture_order.php
|
||||||
|
**File**: `modules/billing/api/capture_order.php`
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
- **Disabled error display**: `ini_set('display_errors', '0')` to prevent JSON corruption
|
||||||
|
- **Session compatibility**: Checks both `website_user_id` and `user_id`
|
||||||
|
- **Proper JSON errors**: Returns structured JSON on DB connection failure
|
||||||
|
- **Table prefix usage**: Uses `$table_prefix` instead of hardcoded names
|
||||||
|
- **Complete invoice processing**:
|
||||||
|
- Marks all due invoices as paid
|
||||||
|
- Handles both NEW orders and RENEWALS
|
||||||
|
- Proper end_date calculation (months from qty + invoice_duration)
|
||||||
|
- Links invoices to orders
|
||||||
|
|
||||||
|
### 3. Fixed payment_success.php
|
||||||
|
**File**: `modules/billing/payment_success.php`
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
- Requires `payment_processor.php` helper
|
||||||
|
- Displays payment confirmation page
|
||||||
|
- Shows user's recent orders
|
||||||
|
- No longer contains duplicate/incomplete function definitions
|
||||||
|
|
||||||
|
### 4. Fixed webhook.php
|
||||||
|
**File**: `modules/billing/webhook.php`
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
- Uses `payment_processor.php` instead of requiring full payment_success.php
|
||||||
|
- Prevents HTML output that would interfere with webhook response
|
||||||
|
- Processes payment record after verification
|
||||||
|
|
||||||
|
### 5. Fixed cart.php Free Button
|
||||||
|
**File**: `modules/billing/cart.php`
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
- Uses `payment_processor.php` for consistent processing
|
||||||
|
- Free button now properly:
|
||||||
|
- Marks invoice as paid
|
||||||
|
- Creates order record
|
||||||
|
- Calculates end_date
|
||||||
|
- Processes payment record through shared function
|
||||||
|
|
||||||
|
## Payment Flow (After Fixes)
|
||||||
|
|
||||||
|
### PayPal Payment Flow
|
||||||
|
```
|
||||||
|
1. User clicks "Pay with PayPal" in cart.php
|
||||||
|
↓
|
||||||
|
2. JavaScript calls api/create_order.php
|
||||||
|
→ Creates PayPal order with custom_id = invoice_id
|
||||||
|
↓
|
||||||
|
3. User approves payment on PayPal
|
||||||
|
↓
|
||||||
|
4. JavaScript calls api/capture_order.php
|
||||||
|
→ PayPal captures payment
|
||||||
|
→ capture_order.php:
|
||||||
|
a) Marks invoices as paid (status='due' → 'paid')
|
||||||
|
b) For NEW: Creates order in billing_orders
|
||||||
|
c) For RENEW: Extends existing order's end_date
|
||||||
|
d) Links invoice to order (sets invoice.order_id)
|
||||||
|
→ Returns JSON: { status: "COMPLETED", ... }
|
||||||
|
↓
|
||||||
|
5. JavaScript redirects to payment_success.php
|
||||||
|
→ Shows confirmation page
|
||||||
|
→ Displays order details
|
||||||
|
↓
|
||||||
|
6. PayPal sends webhook to webhook.php (parallel)
|
||||||
|
→ Verifies signature
|
||||||
|
→ Calls process_payment_record()
|
||||||
|
→ Same processing as step 4 (idempotent)
|
||||||
|
↓
|
||||||
|
7. Cart is empty (invoices now have status='paid', not shown)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Free/Claim Flow
|
||||||
|
```
|
||||||
|
1. User clicks "Claim (Free)" button in cart.php
|
||||||
|
↓
|
||||||
|
2. Cart.php POST handler:
|
||||||
|
→ Marks invoice as paid
|
||||||
|
→ Creates order record with calculated end_date
|
||||||
|
→ Links invoice to order
|
||||||
|
→ Creates simulated webhook file
|
||||||
|
→ Calls process_payment_record() for consistency
|
||||||
|
↓
|
||||||
|
3. Redirects to return.php
|
||||||
|
→ Shows payment confirmation
|
||||||
|
↓
|
||||||
|
4. Cart is empty (invoice marked paid)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renewal Flow
|
||||||
|
```
|
||||||
|
1. User has existing order (order_id > 0)
|
||||||
|
↓
|
||||||
|
2. System creates renewal invoice:
|
||||||
|
→ status = 'due'
|
||||||
|
→ order_id = <existing_order_id>
|
||||||
|
→ qty = renewal months
|
||||||
|
↓
|
||||||
|
3. Invoice appears in cart
|
||||||
|
↓
|
||||||
|
4. User pays (PayPal or Free)
|
||||||
|
↓
|
||||||
|
5. process_payment_record():
|
||||||
|
→ Detects order_id > 0 (renewal)
|
||||||
|
→ Fetches current end_date from existing order
|
||||||
|
→ Calculates new end_date:
|
||||||
|
- If current end_date > now: extend from current end_date
|
||||||
|
- Otherwise: extend from now
|
||||||
|
→ Updates order with new end_date
|
||||||
|
→ Marks invoice as paid
|
||||||
|
↓
|
||||||
|
6. Order subscription extended by renewal period
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Before deployment, verify:
|
||||||
|
|
||||||
|
- [ ] Config setup: Copy `config.inc.php.orig` to `config.inc.php` and configure
|
||||||
|
- [ ] Database: Ensure `ogp_billing_invoices` and `ogp_billing_orders` tables exist
|
||||||
|
- [ ] Test NEW order flow:
|
||||||
|
- [ ] Add item to cart (creates invoice with status='due')
|
||||||
|
- [ ] View cart (item appears)
|
||||||
|
- [ ] Click "Claim (Free)" for $0 item (creates order, clears cart)
|
||||||
|
- [ ] Verify order created in billing_orders
|
||||||
|
- [ ] Verify invoice marked paid, linked to order
|
||||||
|
- [ ] Test PayPal flow:
|
||||||
|
- [ ] Add paid item to cart
|
||||||
|
- [ ] Click PayPal button
|
||||||
|
- [ ] Complete payment on PayPal sandbox
|
||||||
|
- [ ] Verify returns to payment_success.php without errors
|
||||||
|
- [ ] Verify order created
|
||||||
|
- [ ] Verify invoice marked paid
|
||||||
|
- [ ] Verify cart is empty
|
||||||
|
- [ ] Test RENEWAL flow:
|
||||||
|
- [ ] Create renewal invoice for existing order
|
||||||
|
- [ ] Pay renewal invoice
|
||||||
|
- [ ] Verify order end_date extended correctly
|
||||||
|
- [ ] Verify invoice marked paid
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
All code changes maintain or improve security:
|
||||||
|
|
||||||
|
1. **SQL Injection Protection**: Uses prepared statements where possible
|
||||||
|
2. **Input Validation**: Validates all user inputs (invoice_id, user_id, etc.)
|
||||||
|
3. **Session Security**: Maintains separate website/panel sessions
|
||||||
|
4. **Webhook Verification**: PayPal signature verification still in place
|
||||||
|
5. **Error Logging**: Errors logged, not displayed to users (prevents information leakage)
|
||||||
|
6. **Database Credentials**: Configuration file outside web root (best practice)
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
1. `modules/billing/includes/payment_processor.php` - NEW
|
||||||
|
2. `modules/billing/api/capture_order.php` - MODIFIED
|
||||||
|
3. `modules/billing/payment_success.php` - MODIFIED
|
||||||
|
4. `modules/billing/webhook.php` - MODIFIED
|
||||||
|
5. `modules/billing/cart.php` - MODIFIED
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
1. **Config file required**: System requires `includes/config.inc.php` to be created from .orig template
|
||||||
|
2. **Multi-item cart matching**: If cart has multiple items, all are processed together (could improve to match specific invoice_id)
|
||||||
|
3. **No transaction rollback**: If order creation fails, invoice may still be marked paid (could improve with DB transactions)
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. Add database transactions for atomic invoice→order operations
|
||||||
|
2. Improve invoice matching in process_payment_record (more specific matching)
|
||||||
|
3. Add unit tests for payment processing logic
|
||||||
|
4. Add admin UI for viewing/managing invoice-order relationships
|
||||||
|
5. Add email notifications for payment confirmations
|
||||||
339
modules/billing/TESTING_CHECKLIST.md
Normal file
339
modules/billing/TESTING_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
# Testing Checklist for Billing Invoice/Order Flow Fixes
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Database Setup**
|
||||||
|
- [ ] Verify `ogp_billing_invoices` table exists
|
||||||
|
- [ ] Verify `ogp_billing_orders` table exists
|
||||||
|
- [ ] Verify tables have all required columns (see create_invoices_table.sql)
|
||||||
|
|
||||||
|
2. **Configuration**
|
||||||
|
- [ ] Copy `modules/billing/includes/config.inc.php.orig` to `modules/billing/includes/config.inc.php`
|
||||||
|
- [ ] Update database credentials in config.inc.php
|
||||||
|
- [ ] Verify `$table_prefix` is set correctly (default: "ogp_")
|
||||||
|
- [ ] Verify `$SITE_DATA_DIR` path is writable
|
||||||
|
|
||||||
|
3. **PayPal Configuration**
|
||||||
|
- [ ] Verify sandbox client_id and client_secret in api/create_order.php
|
||||||
|
- [ ] Verify sandbox client_id and client_secret in api/capture_order.php
|
||||||
|
- [ ] Verify webhook_id in webhook.php
|
||||||
|
|
||||||
|
## Test 1: Add to Cart (Invoice Creation)
|
||||||
|
|
||||||
|
**Test NEW Order Flow**
|
||||||
|
|
||||||
|
1. Navigate to order.php
|
||||||
|
2. Select a game server configuration
|
||||||
|
3. Set price to $0.00 for testing (or use regular price)
|
||||||
|
4. Fill in all required fields
|
||||||
|
5. Click "Add to Cart"
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- [ ] Redirects to cart.php
|
||||||
|
- [ ] Item appears in cart
|
||||||
|
- [ ] Database check: Invoice created in `ogp_billing_invoices`
|
||||||
|
- [ ] status = 'due'
|
||||||
|
- [ ] order_id = 0 (no order yet)
|
||||||
|
- [ ] user_id matches logged-in user
|
||||||
|
- [ ] amount, qty, service_id populated correctly
|
||||||
|
|
||||||
|
**Verification SQL:**
|
||||||
|
```sql
|
||||||
|
SELECT * FROM ogp_billing_invoices WHERE status='due' ORDER BY invoice_id DESC LIMIT 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test 2: Free Button (Manual Order Creation)
|
||||||
|
|
||||||
|
**Test Free/Claim Flow**
|
||||||
|
|
||||||
|
1. Ensure you have item in cart with amount = 0.00
|
||||||
|
2. Click "Claim (Free)" button
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- [ ] Redirects to return.php
|
||||||
|
- [ ] Shows payment confirmation
|
||||||
|
- [ ] Invoice marked as paid
|
||||||
|
- [ ] Order created
|
||||||
|
- [ ] Cart is empty
|
||||||
|
|
||||||
|
**Verification SQL:**
|
||||||
|
```sql
|
||||||
|
-- Check invoice was marked paid
|
||||||
|
SELECT invoice_id, status, paid_date, order_id FROM ogp_billing_invoices
|
||||||
|
WHERE status='paid' ORDER BY invoice_id DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- Check order was created
|
||||||
|
SELECT order_id, user_id, status, end_date, payment_txid FROM ogp_billing_orders
|
||||||
|
ORDER BY order_id DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- Verify link
|
||||||
|
SELECT i.invoice_id, i.order_id, o.order_id
|
||||||
|
FROM ogp_billing_invoices i
|
||||||
|
LEFT JOIN ogp_billing_orders o ON i.order_id = o.order_id
|
||||||
|
WHERE i.status='paid' ORDER BY i.invoice_id DESC LIMIT 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check Logs:**
|
||||||
|
```bash
|
||||||
|
tail -50 modules/billing/logs/site.log | grep -E "(payment|free_create)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test 3: PayPal Payment Flow
|
||||||
|
|
||||||
|
**Test PayPal Checkout**
|
||||||
|
|
||||||
|
1. Add paid item to cart (e.g., $5.00)
|
||||||
|
2. Click PayPal button in cart
|
||||||
|
3. Should redirect to PayPal sandbox
|
||||||
|
4. Login with sandbox buyer account
|
||||||
|
5. Approve payment
|
||||||
|
6. Should return to payment_success.php
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- [ ] PayPal button renders correctly
|
||||||
|
- [ ] Creates PayPal order (check browser console for order ID)
|
||||||
|
- [ ] Redirects to PayPal sandbox
|
||||||
|
- [ ] After approval, returns to payment_success.php
|
||||||
|
- [ ] No JavaScript errors in console
|
||||||
|
- [ ] No "Unexpected end of JSON input" error
|
||||||
|
- [ ] Invoice marked as paid
|
||||||
|
- [ ] Order created
|
||||||
|
- [ ] Cart is empty
|
||||||
|
|
||||||
|
**Browser Console Checks:**
|
||||||
|
```
|
||||||
|
Look for:
|
||||||
|
✓ "PayPal cart debug: ..." - Shows cart data
|
||||||
|
✓ "Creating order..." - Order creation started
|
||||||
|
✓ "Order created." - Order creation succeeded
|
||||||
|
✓ "Capturing payment..." - Capture started
|
||||||
|
✗ Any errors - Should be none
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification SQL:**
|
||||||
|
```sql
|
||||||
|
-- Check invoice
|
||||||
|
SELECT invoice_id, status, paid_date, payment_txid, payment_method, order_id
|
||||||
|
FROM ogp_billing_invoices
|
||||||
|
WHERE payment_method='paypal'
|
||||||
|
ORDER BY invoice_id DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- Check order
|
||||||
|
SELECT order_id, user_id, status, price, end_date, payment_txid
|
||||||
|
FROM ogp_billing_orders
|
||||||
|
WHERE payment_txid LIKE '%'
|
||||||
|
ORDER BY order_id DESC LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check API Logs:**
|
||||||
|
```bash
|
||||||
|
# Check create_order.php payload
|
||||||
|
cat modules/billing/data/create_order_payload.log
|
||||||
|
|
||||||
|
# Check corrected URLs
|
||||||
|
cat modules/billing/data/corrected_urls.log
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
cat modules/billing/data/create_order_errors.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test 4: Webhook Processing
|
||||||
|
|
||||||
|
**Test Webhook Handler**
|
||||||
|
|
||||||
|
1. Trigger a PayPal payment (from Test 3)
|
||||||
|
2. PayPal will send webhook to webhook.php
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- [ ] Webhook receives POST from PayPal
|
||||||
|
- [ ] Signature verification succeeds
|
||||||
|
- [ ] Payment record processed
|
||||||
|
- [ ] Invoice marked paid (if not already)
|
||||||
|
- [ ] Order created/updated (if not already)
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
```bash
|
||||||
|
# Check webhook log
|
||||||
|
tail -50 modules/billing/data/webhook.log
|
||||||
|
|
||||||
|
# Check for payment processing
|
||||||
|
grep "process_payment" modules/billing/data/webhook.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check Data Files:**
|
||||||
|
```bash
|
||||||
|
ls -lah modules/billing/data/*.json
|
||||||
|
cat modules/billing/data/INV-*.json # Check payment record format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test 5: Renewal Flow
|
||||||
|
|
||||||
|
**Setup Renewal Invoice**
|
||||||
|
|
||||||
|
1. Create a test order manually:
|
||||||
|
```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, order_date, end_date,
|
||||||
|
payment_txid, paid_ts
|
||||||
|
) VALUES (
|
||||||
|
1, 1, 'Test Server', 1, 10, 1, 'month',
|
||||||
|
5.00, 'rconpass', 'ftppass', 'paid', NOW(), DATE_ADD(NOW(), INTERVAL 1 MONTH),
|
||||||
|
'TEST-INITIAL', NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Get the order_id from the insert:
|
||||||
|
```sql
|
||||||
|
SELECT LAST_INSERT_ID();
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create renewal invoice:
|
||||||
|
```sql
|
||||||
|
INSERT INTO ogp_billing_invoices (
|
||||||
|
order_id, user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||||
|
amount, status, customer_name, customer_email, due_date, description
|
||||||
|
) VALUES (
|
||||||
|
LAST_INSERT_ID(), -- Use order_id from step 2
|
||||||
|
1, 1, 'Test Server', 1, 10, 1, 'month',
|
||||||
|
5.00, 'due', 'Test User', 'test@test.com', DATE_ADD(NOW(), INTERVAL 3 DAY),
|
||||||
|
'Renewal invoice'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Renewal Payment**
|
||||||
|
|
||||||
|
1. Log in as user who owns the order
|
||||||
|
2. View cart - should show renewal invoice
|
||||||
|
3. Pay using free button or PayPal
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- [ ] Invoice marked as paid
|
||||||
|
- [ ] Original order's end_date extended by 1 month
|
||||||
|
- [ ] No duplicate order created
|
||||||
|
- [ ] Invoice.order_id still points to original order
|
||||||
|
|
||||||
|
**Verification SQL:**
|
||||||
|
```sql
|
||||||
|
-- Check order end_date was extended
|
||||||
|
SELECT order_id, end_date, status, payment_txid
|
||||||
|
FROM ogp_billing_orders
|
||||||
|
WHERE order_id = <order_id_from_step_2>;
|
||||||
|
|
||||||
|
-- Should show end_date = original end_date + 1 month
|
||||||
|
|
||||||
|
-- Check invoice
|
||||||
|
SELECT invoice_id, order_id, status, paid_date
|
||||||
|
FROM ogp_billing_invoices
|
||||||
|
WHERE order_id = <order_id_from_step_2>;
|
||||||
|
|
||||||
|
-- Should show paid invoice linked to same order_id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test 6: Error Handling
|
||||||
|
|
||||||
|
**Test Invalid Scenarios**
|
||||||
|
|
||||||
|
1. **Missing session**: Try to pay without being logged in
|
||||||
|
- [ ] Should redirect to login or show error
|
||||||
|
|
||||||
|
2. **Database connection failure**: Temporarily break DB config
|
||||||
|
- [ ] capture_order.php should return JSON error, not crash
|
||||||
|
- [ ] Error should be logged
|
||||||
|
|
||||||
|
3. **PayPal API failure**: Use invalid credentials
|
||||||
|
- [ ] Should show error in console
|
||||||
|
- [ ] Should log error
|
||||||
|
- [ ] Should not corrupt database
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue: "Config file not found"
|
||||||
|
**Solution**: Copy config.inc.php.orig to config.inc.php
|
||||||
|
|
||||||
|
### Issue: "Table doesn't exist"
|
||||||
|
**Solution**: Run create_invoices_table.sql
|
||||||
|
|
||||||
|
### Issue: "Permission denied writing to data/"
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
chmod 775 modules/billing/data
|
||||||
|
chown www-data:www-data modules/billing/data # Or your web server user
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "PayPal button doesn't render"
|
||||||
|
**Solution**: Check browser console for errors, verify client_id
|
||||||
|
|
||||||
|
### Issue: "Unexpected end of JSON input"
|
||||||
|
**Solution**:
|
||||||
|
- Check PHP error log: `tail -f /var/log/php/error.log`
|
||||||
|
- Verify display_errors=0 in capture_order.php
|
||||||
|
- Check for syntax errors: `php -l api/capture_order.php`
|
||||||
|
|
||||||
|
### Issue: "Cart still shows items after payment"
|
||||||
|
**Solution**:
|
||||||
|
- Check if invoice status changed to 'paid'
|
||||||
|
- Check if process_payment_record was called
|
||||||
|
- Check logs for errors
|
||||||
|
|
||||||
|
## Performance Testing
|
||||||
|
|
||||||
|
**Test with Multiple Items**
|
||||||
|
|
||||||
|
1. Add 5 items to cart
|
||||||
|
2. Pay with PayPal
|
||||||
|
3. Verify all 5 invoices marked paid
|
||||||
|
4. Verify all 5 orders created
|
||||||
|
5. Verify all linked correctly
|
||||||
|
|
||||||
|
**Test Concurrent Payments**
|
||||||
|
|
||||||
|
1. Add item to cart in two different browsers (same user)
|
||||||
|
2. Attempt to pay both simultaneously
|
||||||
|
3. Verify both process correctly
|
||||||
|
4. Check for race conditions
|
||||||
|
|
||||||
|
## Security Testing
|
||||||
|
|
||||||
|
**Test SQL Injection**
|
||||||
|
|
||||||
|
1. Try adding special characters to form fields
|
||||||
|
2. Try manipulating invoice_id in POST requests
|
||||||
|
3. Verify all inputs are sanitized/escaped
|
||||||
|
|
||||||
|
**Test Session Hijacking**
|
||||||
|
|
||||||
|
1. Try accessing cart with invalid session
|
||||||
|
2. Try paying for someone else's invoice
|
||||||
|
3. Verify proper authorization checks
|
||||||
|
|
||||||
|
**Test Webhook Signature**
|
||||||
|
|
||||||
|
1. Send fake webhook without valid signature
|
||||||
|
2. Verify it's rejected
|
||||||
|
3. Check logs for security events
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
After testing, clean up test data:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Remove test invoices
|
||||||
|
DELETE FROM ogp_billing_invoices WHERE customer_email = 'test@test.com';
|
||||||
|
|
||||||
|
-- Remove test orders
|
||||||
|
DELETE FROM ogp_billing_orders WHERE remote_control_password = 'rconpass';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sign-off
|
||||||
|
|
||||||
|
- [ ] All tests passed
|
||||||
|
- [ ] No errors in logs
|
||||||
|
- [ ] Documentation reviewed
|
||||||
|
- [ ] Security checks completed
|
||||||
|
- [ ] Ready for production deployment
|
||||||
|
|
||||||
|
**Tested by**: _______________
|
||||||
|
**Date**: _______________
|
||||||
|
**Environment**: _______________ (Dev/Staging/Production)
|
||||||
|
**Notes**: _______________
|
||||||
|
|
@ -5,6 +5,10 @@ $sandbox = true; // flip to false for Live
|
||||||
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||||
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||||
|
|
||||||
|
// Ensure all errors are logged, not output (to prevent JSON corruption)
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$in = json_decode(file_get_contents('php://input'), true) ?: [];
|
$in = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
$order_id = $in['order_id'] ?? null;
|
$order_id = $in['order_id'] ?? null;
|
||||||
|
|
@ -78,7 +82,7 @@ if ($captureStatus === 'COMPLETED' && $custom_id) {
|
||||||
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
|
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
|
||||||
if (!$db) {
|
if (!$db) {
|
||||||
error_log('capture_order.php: DB connection failed');
|
error_log('capture_order.php: DB connection failed');
|
||||||
echo $res;
|
echo json_encode(['error' => 'db_connection_failed', 'status' => $captureStatus]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,25 +90,28 @@ if ($captureStatus === 'COMPLETED' && $custom_id) {
|
||||||
// For now, we'll mark ALL due invoices for the logged-in user as paid
|
// 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
|
// TODO: Improve to match specific invoice_id from custom_id if cart sends it
|
||||||
session_start();
|
session_start();
|
||||||
$user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
|
// Check both website_user_id and user_id for compatibility
|
||||||
|
$user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) :
|
||||||
|
(isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0);
|
||||||
|
|
||||||
if ($user_id > 0) {
|
if ($user_id > 0) {
|
||||||
// Mark all due invoices for this user as paid
|
// Mark all due invoices for this user as paid
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$esc_txid = mysqli_real_escape_string($db, $txid);
|
$esc_txid = mysqli_real_escape_string($db, $txid);
|
||||||
|
|
||||||
$updateInvoices = "UPDATE ogp_billing_invoices
|
$updateInvoices = "UPDATE {$table_prefix}billing_invoices
|
||||||
SET status='paid', paid_date='$now', payment_txid='$esc_txid', payment_method='paypal'
|
SET status='paid', paid_date='$now', payment_txid='$esc_txid', payment_method='paypal'
|
||||||
WHERE user_id=$user_id AND status='due'";
|
WHERE user_id=$user_id AND status='due'";
|
||||||
mysqli_query($db, $updateInvoices);
|
mysqli_query($db, $updateInvoices);
|
||||||
|
|
||||||
// Get all invoices we just marked paid
|
// Get all invoices we just marked paid
|
||||||
$getInvoices = "SELECT * FROM ogp_billing_invoices WHERE user_id=$user_id AND payment_txid='$esc_txid'";
|
$getInvoices = "SELECT * FROM {$table_prefix}billing_invoices WHERE user_id=$user_id AND payment_txid='$esc_txid'";
|
||||||
$invoicesResult = mysqli_query($db, $getInvoices);
|
$invoicesResult = mysqli_query($db, $getInvoices);
|
||||||
|
|
||||||
// For each invoice, create an order
|
// For each invoice, either create a new order or extend existing one (renewal)
|
||||||
while ($inv = mysqli_fetch_assoc($invoicesResult)) {
|
while ($inv = mysqli_fetch_assoc($invoicesResult)) {
|
||||||
$invoice_id = intval($inv['invoice_id']);
|
$invoice_id = intval($inv['invoice_id']);
|
||||||
|
$existing_order_id = intval($inv['order_id'] ?? 0);
|
||||||
$service_id = intval($inv['service_id']);
|
$service_id = intval($inv['service_id']);
|
||||||
$home_name = mysqli_real_escape_string($db, $inv['home_name']);
|
$home_name = mysqli_real_escape_string($db, $inv['home_name']);
|
||||||
$ip = intval($inv['ip']);
|
$ip = intval($inv['ip']);
|
||||||
|
|
@ -115,30 +122,72 @@ if ($captureStatus === 'COMPLETED' && $custom_id) {
|
||||||
$rcon_pw = mysqli_real_escape_string($db, $inv['remote_control_password']);
|
$rcon_pw = mysqli_real_escape_string($db, $inv['remote_control_password']);
|
||||||
$ftp_pw = mysqli_real_escape_string($db, $inv['ftp_password']);
|
$ftp_pw = mysqli_real_escape_string($db, $inv['ftp_password']);
|
||||||
|
|
||||||
// Calculate end_date based on qty * duration
|
// Check if this is a renewal (existing order_id > 0) or new order (order_id = 0)
|
||||||
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
|
if ($existing_order_id > 0) {
|
||||||
|
// RENEWAL: Extend the existing order's end_date
|
||||||
|
// Calculate months to add based on qty and duration
|
||||||
|
$months = 0;
|
||||||
|
$q = intval($qty);
|
||||||
|
$invdur = strtolower(trim($duration));
|
||||||
|
if (strpos($invdur, 'year') !== false) {
|
||||||
|
$months = $q * 12;
|
||||||
|
} else {
|
||||||
|
// default to months for anything else (month, monthly, etc.)
|
||||||
|
$months = $q;
|
||||||
|
}
|
||||||
|
|
||||||
// Insert order
|
// Get current end_date and extend it
|
||||||
$insertOrder = "INSERT INTO ogp_billing_orders (
|
$getEndDate = "SELECT end_date FROM {$table_prefix}billing_orders WHERE order_id=$existing_order_id LIMIT 1";
|
||||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
$endDateResult = mysqli_query($db, $getEndDate);
|
||||||
price, remote_control_password, ftp_password, status, order_date, end_date,
|
if ($endDateResult && mysqli_num_rows($endDateResult) === 1) {
|
||||||
payment_txid, paid_ts
|
$endRow = mysqli_fetch_assoc($endDateResult);
|
||||||
) VALUES (
|
$current_end = $endRow['end_date'] ?? date('Y-m-d H:i:s');
|
||||||
$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)) {
|
// Extend from current end_date or now (whichever is later)
|
||||||
$new_order_id = mysqli_insert_id($db);
|
$extend_from = (strtotime($current_end) > time()) ? $current_end : date('Y-m-d H:i:s');
|
||||||
|
$dt = new DateTime($extend_from);
|
||||||
|
if ($months > 0) {
|
||||||
|
$dt->modify('+' . intval($months) . ' months');
|
||||||
|
}
|
||||||
|
$new_end_date = $dt->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
// Link invoice to order
|
// Update order with new end_date and mark as paid/active
|
||||||
$linkInvoice = "UPDATE ogp_billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id";
|
$updateOrder = "UPDATE {$table_prefix}billing_orders
|
||||||
mysqli_query($db, $linkInvoice);
|
SET end_date='$new_end_date', status='paid', payment_txid='$esc_txid', paid_ts='$now'
|
||||||
|
WHERE order_id=$existing_order_id";
|
||||||
error_log("capture_order.php: Created order $new_order_id for invoice $invoice_id");
|
if (mysqli_query($db, $updateOrder)) {
|
||||||
|
error_log("capture_order.php: Extended order $existing_order_id end_date to $new_end_date for invoice $invoice_id");
|
||||||
|
} else {
|
||||||
|
error_log("capture_order.php: Failed to extend order $existing_order_id: " . mysqli_error($db));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log("capture_order.php: Failed to create order for invoice $invoice_id: " . mysqli_error($db));
|
// NEW ORDER: Create a new order record
|
||||||
|
// Calculate end_date based on qty * duration
|
||||||
|
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
|
||||||
|
|
||||||
|
// Insert order
|
||||||
|
$insertOrder = "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, 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 {$table_prefix}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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,16 +169,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['create_free_for']))
|
||||||
file_put_contents($fname, json_encode($rec, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
file_put_contents($fname, json_encode($rec, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
// If available, process the payment record immediately so webhooks logic runs during creation
|
// If available, process the payment record immediately so webhooks logic runs during creation
|
||||||
$ps = __DIR__ . '/payment_success.php';
|
require_once(__DIR__ . '/includes/payment_processor.php');
|
||||||
if (is_file($ps)) {
|
try {
|
||||||
try {
|
|
||||||
require_once($ps);
|
|
||||||
if (function_exists('process_payment_record')) {
|
if (function_exists('process_payment_record')) {
|
||||||
process_payment_record($rec);
|
process_payment_record($rec);
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('[cart create_free] process_payment_record failed: ' . $e->getMessage());
|
error_log('[cart create_free] process_payment_record failed: ' . $e->getMessage());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: return.php?invoice=' . urlencode($rec['invoice']));
|
header('Location: return.php?invoice=' . urlencode($rec['invoice']));
|
||||||
|
|
|
||||||
186
modules/billing/includes/payment_processor.php
Normal file
186
modules/billing/includes/payment_processor.php
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Payment Processing Helper
|
||||||
|
* Handles marking invoices as paid and creating/extending orders
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!function_exists('process_payment_record')) {
|
||||||
|
/**
|
||||||
|
* Process payment record from webhook or capture
|
||||||
|
* Marks invoices as paid and creates/extends orders
|
||||||
|
*
|
||||||
|
* @param array $record Payment record with invoice, custom, amount, txid, etc.
|
||||||
|
* @return bool True if successful, false otherwise
|
||||||
|
*/
|
||||||
|
function process_payment_record($record) {
|
||||||
|
global $db_host, $db_user, $db_pass, $db_name, $db_port, $table_prefix;
|
||||||
|
|
||||||
|
// Extract payment details
|
||||||
|
$invoice = $record['invoice'] ?? null;
|
||||||
|
$custom = $record['custom'] ?? null;
|
||||||
|
$txid = $record['resource_id'] ?? null;
|
||||||
|
$amount = $record['amount'] ?? 0;
|
||||||
|
|
||||||
|
// Require database connection
|
||||||
|
require_once(__DIR__ . '/../../../includes/database_mysqli.php');
|
||||||
|
$db = createDatabaseConnection($db_host, $db_user, $db_pass, $db_name, $db_port);
|
||||||
|
if (!$db) {
|
||||||
|
if (function_exists('site_log_error')) site_log_error('process_payment_db_fail', ['invoice'=>$invoice]);
|
||||||
|
else error_log('[payment_success] DB connection failed for invoice=' . $invoice);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$esc_txid = mysqli_real_escape_string($db, (string)$txid);
|
||||||
|
|
||||||
|
// Find invoices to mark as paid
|
||||||
|
$invoices_to_process = [];
|
||||||
|
|
||||||
|
// Try to match by custom_id (which should be invoice_id for single-item carts)
|
||||||
|
if ($custom && ctype_digit((string)$custom)) {
|
||||||
|
$invoice_id = intval($custom);
|
||||||
|
$stmt = $db->prepare("SELECT * FROM " . $table_prefix . "billing_invoices WHERE invoice_id = ? AND status = 'due' LIMIT 1");
|
||||||
|
if ($stmt) {
|
||||||
|
$stmt->bind_param('i', $invoice_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if ($result && $row = $result->fetch_assoc()) {
|
||||||
|
$invoices_to_process[] = $row;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match by custom_id, try matching all unpaid invoices for this payment amount
|
||||||
|
// (This handles multi-item carts where custom_id isn't a single invoice_id)
|
||||||
|
if (empty($invoices_to_process) && $invoice) {
|
||||||
|
// Match by invoice reference from PayPal
|
||||||
|
$esc_invoice = mysqli_real_escape_string($db, $invoice);
|
||||||
|
$query = "SELECT * FROM " . $table_prefix . "billing_invoices WHERE status = 'due' AND description LIKE '%$esc_invoice%'";
|
||||||
|
$result = mysqli_query($db, $query);
|
||||||
|
if ($result) {
|
||||||
|
while ($row = mysqli_fetch_assoc($result)) {
|
||||||
|
$invoices_to_process[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each invoice
|
||||||
|
$processed_count = 0;
|
||||||
|
foreach ($invoices_to_process as $inv) {
|
||||||
|
$invoice_id = intval($inv['invoice_id']);
|
||||||
|
$existing_order_id = intval($inv['order_id'] ?? 0);
|
||||||
|
$user_id = intval($inv['user_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']);
|
||||||
|
$invoice_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'] ?? '');
|
||||||
|
|
||||||
|
// Mark invoice as paid
|
||||||
|
$upd_inv = $db->prepare("UPDATE " . $table_prefix . "billing_invoices SET status = 'paid', paid_date = ?, payment_txid = ?, payment_method = 'paypal' WHERE invoice_id = ? LIMIT 1");
|
||||||
|
if ($upd_inv) {
|
||||||
|
$upd_inv->bind_param('ssi', $now, $esc_txid, $invoice_id);
|
||||||
|
$upd_inv->execute();
|
||||||
|
$upd_inv->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a renewal (existing order_id > 0) or new order (order_id = 0)
|
||||||
|
if ($existing_order_id > 0) {
|
||||||
|
// RENEWAL: Extend the existing order's end_date
|
||||||
|
// Calculate months to add
|
||||||
|
$months = 0;
|
||||||
|
$q = intval($qty);
|
||||||
|
$invdur = strtolower(trim($duration));
|
||||||
|
if (strpos($invdur, 'year') !== false) {
|
||||||
|
$months = $q * 12;
|
||||||
|
} else {
|
||||||
|
$months = $q;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current end_date and extend it
|
||||||
|
$getEndDate = "SELECT end_date FROM " . $table_prefix . "billing_orders WHERE order_id = $existing_order_id LIMIT 1";
|
||||||
|
$endDateResult = mysqli_query($db, $getEndDate);
|
||||||
|
if ($endDateResult && mysqli_num_rows($endDateResult) === 1) {
|
||||||
|
$endRow = mysqli_fetch_assoc($endDateResult);
|
||||||
|
$current_end = $endRow['end_date'] ?? date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// Extend from current end_date or now (whichever is later)
|
||||||
|
$extend_from = (strtotime($current_end) > time()) ? $current_end : date('Y-m-d H:i:s');
|
||||||
|
$dt = new DateTime($extend_from);
|
||||||
|
if ($months > 0) {
|
||||||
|
$dt->modify('+' . intval($months) . ' months');
|
||||||
|
}
|
||||||
|
$new_end_date = $dt->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// Update order with new end_date and payment info
|
||||||
|
$updateOrder = "UPDATE " . $table_prefix . "billing_orders
|
||||||
|
SET end_date = '$new_end_date', status = 'paid', payment_txid = '$esc_txid', paid_ts = '$now'
|
||||||
|
WHERE order_id = $existing_order_id";
|
||||||
|
if (mysqli_query($db, $updateOrder)) {
|
||||||
|
if (function_exists('site_log_info')) site_log_info('payment_renewal_processed', ['order_id'=>$existing_order_id, 'invoice_id'=>$invoice_id, 'new_end_date'=>$new_end_date]);
|
||||||
|
else error_log("[payment_success] Extended order $existing_order_id to $new_end_date for invoice $invoice_id");
|
||||||
|
$processed_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// NEW ORDER: Create a new order record
|
||||||
|
// Calculate months for end_date
|
||||||
|
$months = 0;
|
||||||
|
$q = intval($qty);
|
||||||
|
$invdur = strtolower(trim($duration));
|
||||||
|
if (strpos($invdur, 'year') !== false) {
|
||||||
|
$months = $q * 12;
|
||||||
|
} else {
|
||||||
|
$months = $q;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dt = new DateTime('now');
|
||||||
|
if ($months > 0) {
|
||||||
|
$dt->modify('+' . intval($months) . ' months');
|
||||||
|
}
|
||||||
|
$end_date = $dt->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// Insert order
|
||||||
|
$insertOrder = "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, order_date, end_date,
|
||||||
|
payment_txid, paid_ts
|
||||||
|
) VALUES (
|
||||||
|
$user_id, $service_id, '$home_name', $ip, $max_players, $qty, '$duration',
|
||||||
|
$invoice_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 " . $table_prefix . "billing_invoices SET order_id = $new_order_id WHERE invoice_id = $invoice_id";
|
||||||
|
mysqli_query($db, $linkInvoice);
|
||||||
|
|
||||||
|
if (function_exists('site_log_info')) site_log_info('payment_new_order_created', ['order_id'=>$new_order_id, 'invoice_id'=>$invoice_id, 'end_date'=>$end_date]);
|
||||||
|
else error_log("[payment_success] Created order $new_order_id for invoice $invoice_id");
|
||||||
|
$processed_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_close($db);
|
||||||
|
|
||||||
|
if ($processed_count > 0) {
|
||||||
|
if (function_exists('site_log_info')) site_log_info('payment_success_processed', ['count'=>$processed_count,'invoice'=>$invoice,'custom'=>$custom]);
|
||||||
|
else error_log('[payment_success] Processed ' . $processed_count . ' invoice(s) - invoice=' . $invoice . ' custom=' . $custom);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (function_exists('site_log_warn')) site_log_warn('payment_success_no_match', ['invoice'=>$invoice,'custom'=>$custom]);
|
||||||
|
else error_log('[payment_success] No matching invoices found for invoice=' . $invoice . ' custom=' . $custom);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -8,6 +8,8 @@ session_start();
|
||||||
require_once(__DIR__ . '/includes/header.php');
|
require_once(__DIR__ . '/includes/header.php');
|
||||||
require_once(__DIR__ . '/includes/config.inc.php');
|
require_once(__DIR__ . '/includes/config.inc.php');
|
||||||
require_once(__DIR__ . '/../../includes/database_mysqli.php');
|
require_once(__DIR__ . '/../../includes/database_mysqli.php');
|
||||||
|
require_once(__DIR__ . '/includes/log.php');
|
||||||
|
require_once(__DIR__ . '/includes/payment_processor.php');
|
||||||
|
|
||||||
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
|
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
|
||||||
$user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
|
$user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
|
||||||
|
|
@ -88,104 +90,3 @@ $user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0;
|
||||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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";
|
|
||||||
// Note: this simple substitution assumes the where_sql is of the form 'col = ?' used earlier
|
|
||||||
if ($sel_stmt = $db->prepare($sel_sql)) {
|
|
||||||
// bind where params
|
|
||||||
if ($bind_types) {
|
|
||||||
$refs = [];
|
|
||||||
$vals = $bind_vals;
|
|
||||||
foreach ($vals as $k => $v) $refs[$k] = &$vals[$k];
|
|
||||||
array_unshift($refs, $bind_types);
|
|
||||||
call_user_func_array([$sel_stmt, 'bind_param'], $refs);
|
|
||||||
}
|
|
||||||
$sel_stmt->execute();
|
|
||||||
$sel_stmt->bind_result($sel_qty, $sel_invdur);
|
|
||||||
if ($sel_stmt->fetch()) {
|
|
||||||
// compute months
|
|
||||||
$months = 0;
|
|
||||||
$q = intval($sel_qty ?? 0);
|
|
||||||
$invdur = strtolower(trim($sel_invdur ?? ''));
|
|
||||||
if (strpos($invdur, 'year') !== false) {
|
|
||||||
$months = $q * 12;
|
|
||||||
} else {
|
|
||||||
$months = $q;
|
|
||||||
}
|
|
||||||
if ($months <= 0) $months = 0;
|
|
||||||
$dt = new DateTime('now');
|
|
||||||
if ($months > 0) $dt->modify('+' . intval($months) . ' months');
|
|
||||||
$end_date_val = $dt->format('Y-m-d H:i:s');
|
|
||||||
}
|
|
||||||
$sel_stmt->close();
|
|
||||||
}
|
|
||||||
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 end_date if present
|
|
||||||
$types = $bind_types;
|
|
||||||
$vals = $bind_vals;
|
|
||||||
if ($cols) {
|
|
||||||
foreach ($cols as $c) {
|
|
||||||
$types .= 's';
|
|
||||||
if ($c === 'payment_txid') $vals[] = $txid;
|
|
||||||
else $vals[] = $ts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($end_date_val !== null) {
|
|
||||||
$types .= 's';
|
|
||||||
$vals[] = $end_date_val;
|
|
||||||
}
|
|
||||||
// bind dynamically
|
|
||||||
if ($types) {
|
|
||||||
$refs = [];
|
|
||||||
foreach ($vals as $k => $v) $refs[$k] = &$vals[$k];
|
|
||||||
array_unshift($refs, $types);
|
|
||||||
call_user_func_array([$stmt, 'bind_param'], $refs);
|
|
||||||
}
|
|
||||||
$stmt->execute();
|
|
||||||
$affected = $stmt->affected_rows;
|
|
||||||
$stmt->close();
|
|
||||||
return $affected;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
$affected = 0;
|
|
||||||
// Try match by invoice column (if present)
|
|
||||||
if ($invoice) {
|
|
||||||
// some invoices may include paths or file names; use exact match
|
|
||||||
$affected = $update_paid('invoice = ?', 's', [$invoice]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not matched, try numeric custom (order_id)
|
|
||||||
if (!$affected && $custom) {
|
|
||||||
if (ctype_digit((string)$custom)) {
|
|
||||||
$affected = $update_paid('order_id = ?', 'i', [(int)$custom]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still not matched, try matching the custom text field
|
|
||||||
if (!$affected && $custom) {
|
|
||||||
$affected = $update_paid('custom = ?', 's', [$custom]);
|
|
||||||
}
|
|
||||||
|
|
||||||
mysqli_close($db);
|
|
||||||
|
|
||||||
if ($affected) {
|
|
||||||
if (function_exists('site_log_info')) site_log_info('payment_success_marked_paid', ['affected'=>intval($affected),'invoice'=>$invoice,'custom'=>$custom]);
|
|
||||||
else error_log('[payment_success] Marked order paid (affected=' . intval($affected) . ') invoice=' . $invoice . ' custom=' . $custom);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (function_exists('site_log_warn')) site_log_warn('payment_success_no_match', ['invoice'=>$invoice,'custom'=>$custom]);
|
|
||||||
else error_log('[payment_success] No matching order found for invoice=' . $invoice . ' custom=' . $custom);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,12 @@ if (in_array($type, ['PAYMENT.CAPTURE.COMPLETED','PAYMENT.SALE.COMPLETED'], true
|
||||||
$status = 'WROTE_FILE';
|
$status = 'WROTE_FILE';
|
||||||
|
|
||||||
// Attempt to mark order paid in DB
|
// Attempt to mark order paid in DB
|
||||||
$ps = __DIR__ . '/payment_success.php';
|
require_once(__DIR__ . '/includes/payment_processor.php');
|
||||||
if (is_file($ps)) {
|
try {
|
||||||
require_once($ps);
|
process_payment_record($record);
|
||||||
try { process_payment_record($record); } catch (Exception $e) { if (function_exists('site_log_error')) site_log_error('process_payment_fail',['err'=>$e->getMessage()]); else log_line('PROC_FAIL '.$e->getMessage()); }
|
} catch (Exception $e) {
|
||||||
|
if (function_exists('site_log_error')) site_log_error('process_payment_fail',['err'=>$e->getMessage()]);
|
||||||
|
else log_line('PROC_FAIL '.$e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue