diff --git a/modules/billing/admin_invoices.php b/modules/billing/admin_invoices.php new file mode 100644 index 00000000..a06f3018 --- /dev/null +++ b/modules/billing/admin_invoices.php @@ -0,0 +1,152 @@ + + + + + + Admin — Invoices + + + + + + +
+

Admin — All Invoices

+ +
+ ✓ Invoice # updated successfully. +
+ + + +

No invoices found.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Order IDUserHome IDHome NameIPPriceDurationStatusCreatedFinish DateActions
$ + + + + + +
+ +
+ + + + + + + diff --git a/modules/billing/api/capture_order.php b/modules/billing/api/capture_order.php index 7cb57d86..919aab3a 100644 --- a/modules/billing/api/capture_order.php +++ b/modules/billing/api/capture_order.php @@ -37,10 +37,49 @@ curl_close($ch); if ($http !== 201 && $http !== 200) { http_response_code($http); echo $res; exit; } -$payload = json_decode($res, true); -$status = $payload['status'] ?? 'UNKNOWN'; -$txnId = $payload['purchase_units'][0]['payments']['captures'][0]['id'] ?? null; +// Parse the capture response +$captureData = json_decode($res, true); +$captureStatus = $captureData['status'] ?? ''; -echo json_encode(['status'=>$status, 'txn_id'=>$txnId]); +// 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 ($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); + + // 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'); + } + } + + // 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; ?> diff --git a/modules/billing/api/create_order.php b/modules/billing/api/create_order.php index 9f31f310..253f2d0a 100644 --- a/modules/billing/api/create_order.php +++ b/modules/billing/api/create_order.php @@ -47,6 +47,17 @@ if ($http !== 200) { http_response_code(500); echo json_encode(['error'=>'oauth_ $access = json_decode($tok, true)['access_token'] ?? null; if (!$access) { http_response_code(500); echo json_encode(['error'=>'oauth_no_token']); exit; } +// Update site base URL to exclude 'modules/billing' +$siteBaseUrl = 'http://gameservers.world'; + +// Ensure return_url and cancel_url are absolute URLs (relative to site root) +if (strpos($return_url, 'http') !== 0) { + $return_url = $siteBaseUrl . '/' . ltrim($return_url, '/'); +} +if (strpos($cancel_url, 'http') !== 0) { + $cancel_url = $siteBaseUrl . '/' . ltrim($cancel_url, '/'); +} + $purchaseUnit = [ 'amount' => [ 'currency_code' => $currency, 'value' => $amount_value ], 'description' => $description, @@ -64,6 +75,18 @@ $body = [ 'application_context' => [ 'return_url'=>$return_url, 'cancel_url'=>$cancel_url, 'user_action'=>'PAY_NOW' ] ]; +// Log the payload for debugging +$logDir = __DIR__ . '/../data'; +@mkdir($logDir, 0775, true); +$logFile = $logDir . '/create_order_payload.log'; +$logEntry = date('Y-m-d H:i:s') . "\n" . json_encode($body, JSON_PRETTY_PRINT) . "\n\n"; +@file_put_contents($logFile, $logEntry, FILE_APPEND); + +// Log corrected URLs for debugging +$logFile = $logDir . '/corrected_urls.log'; +$logEntry = date('Y-m-d H:i:s') . "\nReturn URL: $return_url\nCancel URL: $cancel_url\n\n"; +@file_put_contents($logFile, $logEntry, FILE_APPEND); + $ch = curl_init("$api/v2/checkout/orders"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, @@ -75,7 +98,18 @@ $res = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); -if ($http !== 201) { http_response_code($http); echo $res; exit; } +if ($http !== 201) { + // Log error for debugging + $logDir = __DIR__ . '/../data'; + @mkdir($logDir, 0775, true); + $logFile = $logDir . '/create_order_errors.log'; + $logEntry = date('Y-m-d H:i:s') . " HTTP $http: " . substr($res, 0, 500) . "\n"; + @file_put_contents($logFile, $logEntry, FILE_APPEND); + + http_response_code($http); + echo $res; + exit; +} echo $res; ?> diff --git a/modules/billing/cart.php b/modules/billing/cart.php index 537fda94..d639f02f 100644 --- a/modules/billing/cart.php +++ b/modules/billing/cart.php @@ -428,10 +428,12 @@ if (is_array($invoice) && count($invoice) === 1 && !empty($invoice[0]['order_id' $description = 'Game server order (' . count($lineItems) . ' item' . (count($lineItems)===1?'': 's') . ')'; // URLs -// URLs - since the billing "website" root is the files in modules/billing, -// return.php lives alongside cart.php so use relative paths. -$returnUrl = 'return.php?invoice=' . urlencode($invoiceId); -$cancelUrl = 'return.php?invoice=' . urlencode($invoiceId) . '&cancel=1'; +// Define the site base URL +$siteBaseUrl = 'http://gameservers.world/modules/billing'; + +// Generate absolute URLs for return and cancel +$returnUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId); +$cancelUrl = $siteBaseUrl . '/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1'; // API base (relative) - point to billing module API endpoints $apiBase = 'api'; @@ -439,6 +441,19 @@ $apiBase = 'api'; + + +
+ Debug Info:
+ Grand Total: $
+ Invoice Items:
+ Line Items:
+ Amount:
+ Invoice ID:
+ Custom ID:
+
+ +
@@ -446,14 +461,14 @@ $apiBase = 'api'; (function(){ const statusEl = document.getElementById('pp-status'); - // Values from PHP - const amount = ""; - const currency = ""; - const invoice_id = ""; - const custom_id = ""; - const description = ""; - const return_url = ""; - const cancel_url = ""; + // Values from PHP - use json_encode for proper JavaScript escaping + const amount = ; + const currency = ; + const invoice_id = ; + const custom_id = ; + const description = ; + const return_url = ; + const cancel_url = ; // Line items (serverID + per-item amount) for your records and webhook correlation const line_invoices = ; @@ -461,8 +476,17 @@ $apiBase = 'api'; // PayPal "items" for purchase_units (shows on PayPal + returns in webhook under purchase_units) const items = ; + // Debug logging + console.log('PayPal cart debug:', { + amount, currency, invoice_id, custom_id, description, + line_invoices_count: line_invoices.length, + items_count: items.length, + return_url, cancel_url + }); + function setStatus(msg){ if(statusEl) statusEl.textContent = msg; } + paypal.Buttons({ createOrder: function() { setStatus('Creating order…'); @@ -477,11 +501,24 @@ $apiBase = 'api'; line_invoices // your raw cart detail, persisted in your DB if you choose }) }) - .then(res => res.json()) + .then(res => { + if (!res.ok) { + return res.text().then(errText => { + throw new Error('API error ' + res.status + ': ' + errText.substring(0, 200)); + }); + } + return res.json(); + }) .then(data => { - if (!data.id) { throw new Error(data.error || 'No order id'); } + if (!data.id) { + throw new Error(JSON.stringify(data).substring(0, 200) || 'No order id'); + } setStatus('Order created.'); return data.id; + }) + .catch(err => { + setStatus('PayPal error: ' + err.message); + throw err; }); }, diff --git a/modules/billing/css/header.css b/modules/billing/css/header.css index 51e7208c..0e3bc377 100644 --- a/modules/billing/css/header.css +++ b/modules/billing/css/header.css @@ -202,6 +202,7 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px .invoice-status{padding:4px 10px;border-radius:4px;font-size:0.85rem;font-weight:600} .invoice-status-paid{background:rgba(16,185,129,0.2);color:#10b981} .invoice-status-pending{background:rgba(245,158,11,0.2);color:#f59e0b} +.invoice-status-expired{background:rgba(239,68,68,0.2);color:#ef4444} .invoice-date{color:rgba(255,255,255,0.6);font-size:0.9rem} /* Login placeholder for non-logged-in users */ diff --git a/modules/billing/my_account.php b/modules/billing/my_account.php index 31863b3d..62a3fdfe 100644 --- a/modules/billing/my_account.php +++ b/modules/billing/my_account.php @@ -187,13 +187,26 @@ usort($invoices, function($a, $b) { return strtotime($b['ts'] ?? 0) - strtotime($a['ts'] ?? 0); }); -// Separate current (pending) and previous (paid) invoices -$current_invoices = array_filter($invoices, function($inv) { - return strtolower($inv['status'] ?? '') === 'pending' || empty($inv['status']); -}); -$previous_invoices = array_filter($invoices, function($inv) { - return strtolower($inv['status'] ?? '') === 'paid' || strtolower($inv['status'] ?? '') === 'completed'; -}); +// Organize invoices by status +$invoices_by_status = []; +foreach ($invoices as $inv) { + $status = strtolower($inv['status'] ?? 'pending'); + if (!isset($invoices_by_status[$status])) { + $invoices_by_status[$status] = []; + } + $invoices_by_status[$status][] = $inv; +} + +// Define status display order and labels +$status_config = [ + 'pending' => ['label' => 'Pending Invoices', 'class' => 'pending'], + 'paid' => ['label' => 'Paid Invoices', 'class' => 'paid'], + 'completed' => ['label' => 'Completed Invoices', 'class' => 'paid'], + 'in-cart' => ['label' => 'In Cart', 'class' => 'pending'], + 'installed' => ['label' => 'Installed/Active', 'class' => 'paid'], + 'expired' => ['label' => 'Expired Invoices', 'class' => 'expired'], + 'cancelled' => ['label' => 'Cancelled Invoices', 'class' => 'expired'], +]; ?> @@ -333,45 +346,35 @@ $previous_invoices = array_filter($invoices, function($inv) { - - -
-

Current Invoices Due

- -
-
-
Invoice #
-
+ + + $status_info): ?> + + -
- - Pending -
-
+ -
+ +
+

Invoices

+
No invoices found.
+
- - -
-

Previous Invoices

- - -
-
-
Invoice #
-
-
-
- - Paid -
-
- - -
No previous invoices found.
- -