diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 57cd0554..b97ca1fd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ - `modules/` — panel modules (legacy `billing/` exists; its **schema** is authoritative for multi-remote, but the **pages** are deprecated). - `includes/` — panel configuration and DB connectors. - `ogp_api.php` — internal API entry point for panel-side actions. -- `paypal/` — PayPal code if present in this branch. +- `api/` — Payment-related API code if present in this branch (previously under `paypal/` or `payments/`). ## 2) No-Code Planning Mode (default) - Do **not** emit PHP, SQL, XML, or shell commands unless a maintainer explicitly asks: **“Generate code now.”** @@ -72,7 +72,7 @@ - **Licensing:** Preserve upstream notices and ensure our additions stay license-compatible. ## 7) Validation checklist (pre-PR / pre-merge) -- Read `_website/`, `modules/config_games/server_configs/`, `modules/`, `includes/`, `paypal/` (if present), and `ogp_api.php` to anchor proposals to actual code. +- Read `_website/`, `modules/config_games/server_configs/`, `modules/`, `includes/`, `api/` (if present), and `ogp_api.php` to anchor proposals to actual code. - Catalog uses only the XML metadata; no hardcoded ports/params. - Regions/nodes are read live from the panel DB; no duplicates on the website. - Auth plan preserves panel compatibility and modernizes website hashing; **sessions remain separate**. diff --git a/_website/cart.php b/_website/cart.php deleted file mode 100644 index 0f46e69f..00000000 --- a/_website/cart.php +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - Shopping Cart - GameServers.World - - - 0) { - $stmt = $db->prepare("UPDATE ogp_billing_orders SET status = 'paid' WHERE order_id = ? LIMIT 1"); - if ($stmt) { $stmt->bind_param('i', $orderId); $stmt->execute(); $stmt->close(); } - - // write a simulated webhook file - require_once(__DIR__ . '/includes/config.inc.php'); - $dataDir = (isset($SITE_DATA_DIR) && $SITE_DATA_DIR) ? $SITE_DATA_DIR : realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'data'; - @mkdir($dataDir, 0775, true); - $rec = [ - 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', - 'status' => 'PAID', - 'amount' => 0.00, - 'currency' => 'USD', - 'payer' => $_SESSION['website_user_email'] ?? ($_SESSION['website_username'] ?? ''), - 'invoice' => 'FREE-' . $orderId . '-' . time(), - 'custom' => 'admin_free_create_order_' . $orderId, - 'resource_id' => 'FREE-' . bin2hex(random_bytes(6)), - 'items' => [], - 'ts' => date('c'), - ]; - $fname = $dataDir . DIRECTORY_SEPARATOR . $rec['invoice'] . '.json'; - file_put_contents($fname, json_encode($rec, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); - header('Location: return.php?invoice=' . urlencode($rec['invoice'])); - exit; - } - } -} - -// Include top bar and menu -include(__DIR__ . '/includes/top.php'); -include(__DIR__ . '/includes/menu.php'); - -$user_id=$_SESSION['user_id'] ?? 0; -$user_id = 186; // For testing purposes, set a default user ID - -if ($user_id <= 0) { - echo "

Please login to view your cart

"; - mysqli_close($db); - echo ""; - return; -} - - - -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); - $stmt->execute(); - $stmt->bind_result($status); - if ($stmt->fetch() && strtolower($status) === 'renew') { - $stmt->close(); - // Set status to 'expired' if currently 'renew' - $update = $db->prepare("UPDATE ogp_billing_orders SET status = 'expired' WHERE order_id = ? AND user_id = ?"); - $update->bind_param("ii", $order_id, $user_id); - $update->execute(); - $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); - $delete->execute(); - $delete->close(); - } - } -} - -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"); - - - -} - -?> - -
-

Your Cart

- - - - - - - - - - - - - - - - - - num_rows > 0) { - while ($row = $carts->fetch_assoc()) { - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Server IDGame NameLocationMax PlayersPrice per PlayerMonthsTotal
-
- -
-
$ -
- - -
-
 $
- Cart Total: - - $ -
No items in your cart.
- - -'srv123','amount'=>9.99], ['serverID'=>'srv999','amount'=>14.50]] - -// --- Sanity + normalization --- -if (!isset($grandTotal) || !is_numeric($grandTotal)) { - $grandTotal = 0.00; -} -if (!isset($invoice) || !is_array($invoice)) { - $invoice = []; -} -$currency = 'USD'; -$amount = number_format((float)$grandTotal, 2, '.', ''); -$lineItems = []; - -// Build PayPal-friendly items array (name, unit_amount, quantity, sku) -foreach ($invoice as $i) { - $sid = isset($i['serverID']) ? (string)$i['serverID'] : 'unknown'; - $amt = isset($i['amount']) && is_numeric($i['amount']) ? number_format((float)$i['amount'], 2, '.', '') : '0.00'; - $lineItems[] = [ - 'name' => "Server $sid", - 'quantity' => '1', - 'unit_amount' => ['currency_code' => $currency, 'value' => $amt], - 'sku' => $sid - ]; -} - -// Single overall invoice id for the order -$invoiceId = 'INV-' . date('Ymd-His') . '-' . bin2hex(random_bytes(3)); - -// A short custom reference derived from your line items (<= 127 chars for PayPal) -$customHash = substr(strtoupper(sha1(json_encode($invoice))), 0, 16); -$customId = "INVREF-$customHash"; - -// Text on the PayPal side -$description = 'Game server order (' . count($lineItems) . ' item' . (count($lineItems)===1?'': 's') . ')'; - -// URLs -$siteBase = 'https://panel.iaregamer.com'; -$returnUrl = $siteBase . '/_website/return.php?invoice=' . urlencode($invoiceId); -$cancelUrl = $siteBase . '/_website/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1'; - -// API base (relative) -$apiBase = '/_website/api'; -?> - - - -
-
- - - - -
- - - - - diff --git a/_website/includes/login_required.php b/_website/includes/login_required.php deleted file mode 100644 index 17745da1..00000000 --- a/_website/includes/login_required.php +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/_website/includes/top.php b/_website/includes/top.php deleted file mode 100644 index 75dd7d2d..00000000 --- a/_website/includes/top.php +++ /dev/null @@ -1,18 +0,0 @@ - - -body{background-image:url('". $bg ."');background-size:cover;background-position:center fixed;}\n"; -} -?> - -
-
- Gameservers World logo -
-
Gameservers World
-
diff --git a/_website/logout.php b/_website/logout.php deleted file mode 100644 index 7ba8429e..00000000 --- a/_website/logout.php +++ /dev/null @@ -1,57 +0,0 @@ - diff --git a/_website/payments/api/README.md b/_website/payments/api/README.md deleted file mode 100644 index fd1ff4db..00000000 --- a/_website/payments/api/README.md +++ /dev/null @@ -1 +0,0 @@ -Compatibility wrappers for payments API endpoints. Canonical implementations are under /_website/api/. diff --git a/_website/payments/config.php b/_website/payments/config.php deleted file mode 100644 index 2cb8ba34..00000000 --- a/_website/payments/config.php +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/_website/payments/pay.php b/_website/payments/pay.php deleted file mode 100644 index 6bf4793c..00000000 --- a/_website/payments/pay.php +++ /dev/null @@ -1,4 +0,0 @@ - true, - 'client_id' => '', - 'client_secret' => '', - 'webhook_id' => '', - 'data_dir' => realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data', - 'log_file' => realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'webhook.log', -]; - -if (defined('SITE_DATA_DIR') && SITE_DATA_DIR) { - $config['data_dir'] = rtrim(SITE_DATA_DIR, "\\/") . DIRECTORY_SEPARATOR; -} - -@mkdir($config['data_dir'], 0775, true); - -function log_line($m){global $config; @file_put_contents($config['log_file'],'['.date('c')."] $m\n",FILE_APPEND);} -function api_base(){global $config; return $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';} - -http_response_code(200); - -$raw = file_get_contents('php://input'); -$headers = array_change_key_case(getallheaders() ?: [], CASE_UPPER); -log_line("HIT ip=".($_SERVER['REMOTE_ADDR']??'') ." bytes=".strlen($raw)); -if (!$raw) { log_line("NO_BODY"); exit; } - -// 1) OAuth2 -$ch = curl_init(api_base().'/v1/oauth2/token'); -curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER=>true, - CURLOPT_POST=>true, - CURLOPT_POSTFIELDS=>'grant_type=client_credentials', - CURLOPT_HTTPHEADER=>['Accept: application/json'], - CURLOPT_USERPWD=>$config['client_id'].':'.$config['client_secret'], -]); -$tokenResp = curl_exec($ch); -$http = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); -if ($http!==200){ log_line("OAUTH_FAIL http=$http resp=$tokenResp"); exit; } -$access_token = json_decode($tokenResp, true)['access_token'] ?? null; -if (!$access_token){ log_line("OAUTH_NO_TOKEN"); exit; } - -// 2) Verify webhook signature -$verifyPayload = [ - 'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '', - 'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? '', - 'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '', - 'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '', - 'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '', - 'webhook_id' => $config['webhook_id'], - 'webhook_event' => json_decode($raw, true), -]; -$ch = curl_init(api_base().'/v1/notifications/verify-webhook-signature'); -curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER=>true, - CURLOPT_POST=>true, - CURLOPT_POSTFIELDS=>json_encode($verifyPayload), - CURLOPT_HTTPHEADER=>[ - 'Content-Type: application/json', - 'Authorization: Bearer '.$access_token - ], -]); -$verifyResp = curl_exec($ch); -$http = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); -$verifyJson = json_decode($verifyResp, true); -if ($http!==200 || ($verifyJson['verification_status'] ?? '') !== 'SUCCESS'){ - log_line("VERIFY_FAIL http=$http status=".($verifyJson['verification_status']??'NONE')); - exit; -} -log_line("VERIFY_OK"); - -// 3) Parse and persist (now with items) -$evt = json_decode($raw, true); -$type = $evt['event_type'] ?? ''; -$res = $evt['resource'] ?? []; - -// Extract common fields -$invoice = $res['invoice_id'] ?? ($res['invoice_number'] ?? null); -$custom = $res['custom_id'] ?? ($res['custom'] ?? null); - -// Amounts/payer -$amount = $res['amount']['value'] ?? ($res['amount']['total'] ?? null); -$currency = $res['amount']['currency_code'] ?? ($res['amount']['currency'] ?? null); -$payer = $res['payer']['email_address'] ?? ($res['payer']['payer_info']['email'] ?? null); - -// Try to capture line items if present directly in this event: -$items = []; -if (isset($res['purchase_units'][0]['items']) && is_array($res['purchase_units'][0]['items'])) { - $items = $res['purchase_units'][0]['items']; -} - -// If capture event, try to fetch the parent ORDER to get items -if (!$items && $type === 'PAYMENT.CAPTURE.COMPLETED') { - $orderId = - $res['supplementary_data']['related_ids']['order_id'] // preferred - ?? null; - - if (!$orderId && isset($res['links']) && is_array($res['links'])) { - // Fallback: look for a link to the parent order - foreach ($res['links'] as $lnk) { - if (!empty($lnk['href']) && !empty($lnk['rel']) && stripos($lnk['href'], '/v2/checkout/orders/') !== false) { - $orderId = basename(parse_url($lnk['href'], PHP_URL_PATH)); - break; - } - } - } - - if ($orderId) { - $ch = curl_init(api_base()."/v2/checkout/orders/".urlencode($orderId)); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'Authorization: Bearer '.$access_token, - 'Content-Type: application/json' - ], - ]); - $orderJson = curl_exec($ch); - $httpOrder = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if ($httpOrder === 200) { - $order = json_decode($orderJson, true); - if (isset($order['purchase_units'][0]['items']) && is_array($order['purchase_units'][0]['items'])) { - $items = $order['purchase_units'][0]['items']; - } - // If the order has invoice/custom (sometimes more reliable), prefer those: - if (!$invoice) { $invoice = $order['purchase_units'][0]['invoice_id'] ?? $invoice; } - if (!$custom) { $custom = $order['purchase_units'][0]['custom_id'] ?? $custom; } - } else { - log_line("ORDER_FETCH_FAIL id=$orderId http=$httpOrder"); - } - } -} - -$status = 'IGNORED'; - -// We persist on payment completed events -if (in_array($type, ['PAYMENT.CAPTURE.COMPLETED','PAYMENT.SALE.COMPLETED'], true)) { - $record = [ - 'event_type' => $type, - 'status' => 'PAID', - 'amount' => $amount, - 'currency' => $currency, - 'payer' => $payer, - 'invoice' => $invoice, - 'custom' => $custom, - 'resource_id' => $res['id'] ?? null, - 'items' => $items, // Persist line items for your return.php/UI - 'ts' => date('c'), - ]; - $name = $invoice ?: 'NO-INVOICE'; - @file_put_contents($config['data_dir']."/$name.json", json_encode($record, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); - $status = 'WROTE_FILE'; -} - -log_line("EVENT $type invoice=".($invoice ?: 'none')." items_count=".count($items)." status=$status"); diff --git a/_website/paypal/api/README.md b/_website/paypal/api/README.md deleted file mode 100644 index 3977577d..00000000 --- a/_website/paypal/api/README.md +++ /dev/null @@ -1 +0,0 @@ -This folder contains compatibility wrappers for PayPal API endpoints. The canonical implementations live in /_website/api/. diff --git a/_website/paypal/config.php b/_website/paypal/config.php deleted file mode 100644 index 3d7d035b..00000000 --- a/_website/paypal/config.php +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/_website/paypal/pay.php b/_website/paypal/pay.php deleted file mode 100644 index 364632b1..00000000 --- a/_website/paypal/pay.php +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - Checkout - - - - - - -

Complete your purchase

-

Amount:

-

Invoice:

-
-
- - - - - diff --git a/_website/paypal/return.php b/_website/paypal/return.php deleted file mode 100644 index 4243a345..00000000 --- a/_website/paypal/return.php +++ /dev/null @@ -1,4 +0,0 @@ - true, - 'client_id' => '', - 'client_secret' => '', - 'webhook_id' => '', - 'data_dir' => realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data', - 'log_file' => realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'webhook.log', -]; - -// Allow includes/config.inc.php to override SITE_DATA_DIR if set -if (defined('SITE_DATA_DIR') && SITE_DATA_DIR) { - $config['data_dir'] = rtrim(SITE_DATA_DIR, "\\/") . DIRECTORY_SEPARATOR; -} - -@mkdir($config['data_dir'], 0775, true); - -function log_line($m){global $config; @file_put_contents($config['log_file'],'['.date('c')."] $m\n",FILE_APPEND);} -function api_base(){global $config; return $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';} - -http_response_code(200); - -$raw = file_get_contents('php://input'); -$headers = array_change_key_case(getallheaders() ?: [], CASE_UPPER); -log_line("HIT ip=".($_SERVER['REMOTE_ADDR']??'') ." bytes=".strlen($raw)); -if (!$raw) { log_line("NO_BODY"); exit; } - -// 1) OAuth2 -$ch = curl_init(api_base().'/v1/oauth2/token'); -curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER=>true, - CURLOPT_POST=>true, - CURLOPT_POSTFIELDS=>'grant_type=client_credentials', - CURLOPT_HTTPHEADER=>['Accept: application/json'], - CURLOPT_USERPWD=>$config['client_id'].':'.$config['client_secret'], -]); -$tokenResp = curl_exec($ch); -$http = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); -if ($http!==200){ log_line("OAUTH_FAIL http=$http resp=$tokenResp"); exit; } -$access_token = json_decode($tokenResp, true)['access_token'] ?? null; -if (!$access_token){ log_line("OAUTH_NO_TOKEN"); exit; } - -// 2) Verify webhook signature -$verifyPayload = [ - 'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '', - 'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? '', - 'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '', - 'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '', - 'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '', - 'webhook_id' => $config['webhook_id'], - 'webhook_event' => json_decode($raw, true), -]; -$ch = curl_init(api_base().'/v1/notifications/verify-webhook-signature'); -curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER=>true, - CURLOPT_POST=>true, - CURLOPT_POSTFIELDS=>json_encode($verifyPayload), - CURLOPT_HTTPHEADER=>[ - 'Content-Type: application/json', - 'Authorization: Bearer '.$access_token - ], -]); -$verifyResp = curl_exec($ch); -$http = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); -$verifyJson = json_decode($verifyResp, true); -if ($http!==200 || ($verifyJson['verification_status'] ?? '') !== 'SUCCESS'){ - log_line("VERIFY_FAIL http=$http status=".($verifyJson['verification_status']??'NONE')); - exit; -} -log_line("VERIFY_OK"); - -// 3) Parse and persist (now with items) -$evt = json_decode($raw, true); -$type = $evt['event_type'] ?? ''; -$res = $evt['resource'] ?? []; - -// Extract common fields -$invoice = $res['invoice_id'] ?? ($res['invoice_number'] ?? null); -$custom = $res['custom_id'] ?? ($res['custom'] ?? null); - -// Amounts/payer -$amount = $res['amount']['value'] ?? ($res['amount']['total'] ?? null); -$currency = $res['amount']['currency_code'] ?? ($res['amount']['currency'] ?? null); -$payer = $res['payer']['email_address'] ?? ($res['payer']['payer_info']['email'] ?? null); - -// Try to capture line items if present directly in this event: -$items = []; -if (isset($res['purchase_units'][0]['items']) && is_array($res['purchase_units'][0]['items'])) { - $items = $res['purchase_units'][0]['items']; -} - -// If capture event, try to fetch the parent ORDER to get items -if (!$items && $type === 'PAYMENT.CAPTURE.COMPLETED') { - $orderId = - $res['supplementary_data']['related_ids']['order_id'] // preferred - ?? null; - - if (!$orderId && isset($res['links']) && is_array($res['links'])) { - // Fallback: look for a link to the parent order - foreach ($res['links'] as $lnk) { - if (!empty($lnk['href']) && !empty($lnk['rel']) && stripos($lnk['href'], '/v2/checkout/orders/') !== false) { - $orderId = basename(parse_url($lnk['href'], PHP_URL_PATH)); - break; - } - } - } - - if ($orderId) { - $ch = curl_init(api_base()."/v2/checkout/orders/".urlencode($orderId)); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'Authorization: Bearer '.$access_token, - 'Content-Type: application/json' - ], - ]); - $orderJson = curl_exec($ch); - $httpOrder = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if ($httpOrder === 200) { - $order = json_decode($orderJson, true); - if (isset($order['purchase_units'][0]['items']) && is_array($order['purchase_units'][0]['items'])) { - $items = $order['purchase_units'][0]['items']; - } - // If the order has invoice/custom (sometimes more reliable), prefer those: - if (!$invoice) { $invoice = $order['purchase_units'][0]['invoice_id'] ?? $invoice; } - if (!$custom) { $custom = $order['purchase_units'][0]['custom_id'] ?? $custom; } - } else { - log_line("ORDER_FETCH_FAIL id=$orderId http=$httpOrder"); - } - } -} - -$status = 'IGNORED'; - -// We persist on payment completed events -if (in_array($type, ['PAYMENT.CAPTURE.COMPLETED','PAYMENT.SALE.COMPLETED'], true)) { - $record = [ - 'event_type' => $type, - 'status' => 'PAID', - 'amount' => $amount, - 'currency' => $currency, - 'payer' => $payer, - 'invoice' => $invoice, - 'custom' => $custom, - 'resource_id' => $res['id'] ?? null, - 'items' => $items, // Persist line items for your return.php/UI - 'ts' => date('c'), - ]; - $name = $invoice ?: 'NO-INVOICE'; - @file_put_contents($config['data_dir']."/$name.json", json_encode($record, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); - $status = 'WROTE_FILE'; -} - -log_line("EVENT $type invoice=".($invoice ?: 'none')." items_count=".count($items)." status=$status"); diff --git a/_website/CONFIGURATION.md b/modules/billing/_archived/CONFIGURATION.md similarity index 100% rename from _website/CONFIGURATION.md rename to modules/billing/_archived/CONFIGURATION.md diff --git a/_website/FEATURES.md b/modules/billing/_archived/FEATURES.md similarity index 100% rename from _website/FEATURES.md rename to modules/billing/_archived/FEATURES.md diff --git a/_website/IMPLEMENTATION_SUMMARY.md b/modules/billing/_archived/IMPLEMENTATION_SUMMARY.md similarity index 100% rename from _website/IMPLEMENTATION_SUMMARY.md rename to modules/billing/_archived/IMPLEMENTATION_SUMMARY.md diff --git a/_website/README_LOGIN.md b/modules/billing/_archived/README_LOGIN.md similarity index 100% rename from _website/README_LOGIN.md rename to modules/billing/_archived/README_LOGIN.md diff --git a/_website/VISUAL_GUIDE.md b/modules/billing/_archived/VISUAL_GUIDE.md similarity index 100% rename from _website/VISUAL_GUIDE.md rename to modules/billing/_archived/VISUAL_GUIDE.md diff --git a/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt b/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt new file mode 100644 index 00000000..4e128727 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt @@ -0,0 +1,16 @@ +Archived files from _website on 2025-10-23 14:20:00 + +This folder contains a snapshot of removed documentation and test artifacts moved from the active `_website/` tree. + +Files moved here (original paths): +- VISUAL_GUIDE.md +- README_LOGIN.md +- FEATURES.md +- IMPLEMENTATION_SUMMARY.md +- CONFIGURATION.md +- test_db_connection.php +- tools/simulate_webhook.php +- ai.php +- data/SIMULATED-WEBHOOK-20251022-101500.json + +If you need to restore any of these, copy them back to the original paths. diff --git a/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md b/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md new file mode 100644 index 00000000..6c2c5ff2 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md @@ -0,0 +1,3 @@ +The detailed game docs under `_website/docs/games/` were intentionally left in place (they are product-facing). + +Top-level documentation (VISUAL_GUIDE.md, FEATURES.md, IMPLEMENTATION_SUMMARY.md, CONFIGURATION.md, README_LOGIN.md) were archived here and removed from the active site to reduce clutter. diff --git a/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json b/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json new file mode 100644 index 00000000..ea411e67 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json @@ -0,0 +1,75 @@ +{ + "moved_at": "2025-10-23T20:25:00Z", + "kept": { + "logs": "_website/logs/", + "docs": "_website/docs/" + }, + "files": [ + { + "original": "_website/ai.php", + "archived": "_website/_archived/removed-20251023-202500/ai.php", + "size_bytes": null, + "note": "archived sample and tools; size omitted" + }, + { + "original": "_website/test_db_connection.php", + "archived": "_website/_archived/removed-20251023-202500/test_db_connection.php", + "size_bytes": null + }, + { + "original": "_website/tools/simulate_webhook.php", + "archived": "_website/_archived/removed-20251023-202500/tools/simulate_webhook.php", + "size_bytes": null + }, + { + "original": "_website/tools/check_db_user.php", + "archived": "_website/_archived/removed-20251023-202500/tools/check_db_user.php", + "size_bytes": null + }, + { + "original": "_website/tools/check_invoices_redirect.php", + "archived": "_website/_archived/removed-20251023-202500/tools/check_invoices_redirect.php", + "size_bytes": null + }, + { + "original": "_website/tools/debug_invoices_redirect.php", + "archived": "_website/_archived/removed-20251023-202500/tools/debug_invoices_redirect.php", + "size_bytes": null + }, + { + "original": "_website/tools/check_logout_redirect.php", + "archived": "_website/_archived/removed-20251023-202500/tools/check_logout_redirect.php", + "size_bytes": null + }, + { + "original": "_website/data/SIMULATED-WEBHOOK-20251022-101500.json", + "archived": "_website/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json", + "size_bytes": null + }, + { + "original": "_website/data/NO-INVOICE.json", + "archived": "_website/_archived/removed-20251023-202500/data/NO-INVOICE.json", + "size_bytes": null + }, + { + "original": "_website/data/INV-20250825-174311-0a7993.json", + "archived": "_website/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json", + "size_bytes": null + }, + { + "original": "_website/data/INV-20250825-170438-e37518.json", + "archived": "_website/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json", + "size_bytes": null + }, + { + "original": "_website/data/FREE-549-1761246925.json", + "archived": "_website/_archived/removed-20251023-202500/data/FREE-549-1761246925.json", + "size_bytes": null + }, + { + "original": "_website/data/FREE-548-1761171178.json", + "archived": "_website/_archived/removed-20251023-202500/data/FREE-548-1761171178.json", + "size_bytes": null + } + ] +} diff --git a/modules/billing/_archived/removed-20251023-202500/ai.php b/modules/billing/_archived/removed-20251023-202500/ai.php new file mode 100644 index 00000000..3b62d1d0 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/ai.php @@ -0,0 +1,325 @@ += 400) { + $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown API error'; + throw new RuntimeException("OpenAI API error ({$code}): {$msg}"); + } + return is_array($data) ? $data : []; +} + +/** Create or reuse a per-visitor thread */ +function ensure_thread_id() { + if (!empty($_SESSION['thread_id'])) return $_SESSION['thread_id']; + $created = openai_request('POST', '/threads', ['metadata' => ['site' => $_SERVER['HTTP_HOST'] ?? 'unknown']]); + $tid = $created['id'] ?? null; + if (!$tid) throw new RuntimeException('Failed to create thread.'); + $_SESSION['thread_id'] = $tid; + return $tid; +} + +/** Add a user message */ +function add_user_message($thread_id, $text) { + openai_request('POST', "/threads/{$thread_id}/messages", [ + 'role' => 'user', + 'content' => $text, + ]); +} + +/** Start a run */ +function start_run($thread_id, $assistant_id) { + $run = openai_request('POST', "/threads/{$thread_id}/runs", [ + 'assistant_id' => $assistant_id, + ]); + $run_id = $run['id'] ?? null; + if (!$run_id) throw new RuntimeException('Failed to start run.'); + return $run_id; +} + +/** Wait for completion (or fail/timeout) */ +function wait_for_run($thread_id, $run_id, $max_tries, $delay_us) { + $terminal = ['completed', 'failed', 'requires_action', 'cancelled', 'expired']; + for ($i = 0; $i < $max_tries; $i++) { + usleep($delay_us); + $run = openai_request('GET', "/threads/{$thread_id}/runs/{$run_id}"); + $status = $run['status'] ?? ''; + if (in_array($status, $terminal, true)) return $run; + } + return ['status' => 'timeout']; +} + +/** Cache of file_id => filename (per request) */ +$_FILE_NAME_CACHE = []; + +/** Resolve file name from file_id (API returns "filename" or sometimes "display_name") */ +function get_file_name_by_id($file_id) { + global $_FILE_NAME_CACHE; + if (isset($_FILE_NAME_CACHE[$file_id])) return $_FILE_NAME_CACHE[$file_id]; + $file = openai_request('GET', "/files/{$file_id}"); + $name = $file['filename'] ?? ($file['display_name'] ?? ($file['name'] ?? $file_id)); + $_FILE_NAME_CACHE[$file_id] = $name; + return $name; +} + +/** + * Extract message text + citations (filename + page if available). + * Returns an array of entries: ['role' => 'user|assistant', 'text' => '...', 'refs' => [['filename'=>'','page'=>'','file_id'=>'']]] + */ +function normalize_messages($messages) { + $out = []; + if (empty($messages['data']) || !is_array($messages['data'])) return $out; + + // The API returns newest first by default if not specifying; we request 'asc' in fetch. + foreach ($messages['data'] as $m) { + $role = $m['role'] ?? ''; + if (!in_array($role, ['user', 'assistant', 'system'], true)) continue; + + if (empty($m['content']) || !is_array($m['content'])) continue; + + $all_text = []; + $refs = []; + foreach ($m['content'] as $part) { + if (($part['type'] ?? '') === 'text' && !empty($part['text']['value'])) { + $all_text[] = $part['text']['value']; + + // Parse annotations for citations (file_citation) + $anns = $part['text']['annotations'] ?? []; + if (is_array($anns)) { + foreach ($anns as $ann) { + if (($ann['type'] ?? '') === 'file_citation' && !empty($ann['file_citation']['file_id'])) { + $fid = $ann['file_citation']['file_id']; + $page = null; + + // Page can appear under different shapes depending on backend. Try common keys: + if (isset($ann['file_citation']['page'])) { + $page = $ann['file_citation']['page']; + } elseif (isset($ann['file_citation']['page_range']) && is_array($ann['file_citation']['page_range'])) { + // Example: ['start' => 5, 'end' => 6] + $start = $ann['file_citation']['page_range']['start'] ?? null; + $end = $ann['file_citation']['page_range']['end'] ?? null; + if ($start && $end && $start !== $end) $page = "{$start}-{$end}"; + elseif ($start) $page = (string)$start; + } + // Fetch filename + try { + $filename = get_file_name_by_id($fid); + } catch (Throwable $e) { + $filename = $fid; + } + $refs[] = [ + 'file_id' => $fid, + 'filename' => $filename, + 'page' => $page ?? 'n/a', + ]; + } + } + } + } + } + + if (!empty($all_text)) { + $out[] = [ + 'role' => $role, + 'text' => implode("\n", $all_text), + 'refs' => $refs, + ]; + } + } + return $out; +} + +/** Fetch conversation (ascending) */ +function fetch_history($thread_id) { + $messages = openai_request('GET', "/threads/{$thread_id}/messages", null, ['order' => 'asc', 'limit' => 50]); + return normalize_messages($messages); +} + +/* ------------------- HANDLE POST ------------------- */ +$error = null; +$history = []; + +try { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!empty($_POST['reset_thread'])) { + $_SESSION['thread_id'] = null; + } elseif (isset($_POST['user_input'])) { + $user_text = trim((string)$_POST['user_input']); + if ($user_text !== '') { + $thread_id = ensure_thread_id(); + add_user_message($thread_id, $user_text); + $run_id = start_run($thread_id, $ASSISTANT_ID); + $run = wait_for_run($thread_id, $run_id, $POLL_MAX_TRIES, $RUN_POLL_DELAY); + + if (($run['status'] ?? '') === 'failed') { + $error = 'Assistant run failed.'; + } elseif (($run['status'] ?? '') === 'requires_action') { + // If you later support tool calls, handle them here then submit outputs. + } elseif (($run['status'] ?? '') === 'timeout') { + $error = 'Assistant timed out. Please try again.'; + } + } + } + } + + if (!empty($_SESSION['thread_id'])) { + $history = fetch_history($_SESSION['thread_id']); + } +} catch (Throwable $e) { + $error = $e->getMessage(); +} +?> + + +
+

Site Assistant

+

Type a question below. Press Enter to send, Shift+Enter for a new line.

+ + +
+ Error: +
+ + + +
Thread:
+ + +
+ +
+ + +
+
+ + +
+ Question, assistant => Answer, system => (optional) + $role = $msg['role'] ?? 'assistant'; + if ($role === 'user') $label = 'Question'; + elseif ($role === 'assistant') $label = 'Answer'; + else $label = ucfirst($role); // e.g., System + $text = str_replace("\r\n", "\n", $msg['text'] ?? ''); + $refs = $msg['refs'] ?? []; + ?> +
+
+
+ + +
+ References: + +
+ +
+ +
+ +
No messages yet.
+ + +
+ Conversation persists until you click “Reset Conversation”. +
+
+ + + diff --git a/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json b/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json new file mode 100644 index 00000000..12661d0a --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json @@ -0,0 +1,12 @@ +{ + "event_type": "PAYMENT.CAPTURE.COMPLETED", + "status": "PAID", + "amount": 0, + "currency": "USD", + "payer": "iaretechnician@gmail.com", + "invoice": "FREE-548-1761171178", + "custom": "admin_free_create_order_548", + "resource_id": "FREE-439c594e1e65", + "items": [], + "ts": "2025-10-23T00:12:58+02:00" +} diff --git a/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json b/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json new file mode 100644 index 00000000..764d6832 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json @@ -0,0 +1,12 @@ +{ + "event_type": "PAYMENT.CAPTURE.COMPLETED", + "status": "PAID", + "amount": 0, + "currency": "USD", + "payer": "iaretechnician@gmail.com", + "invoice": "FREE-549-1761246925", + "custom": "admin_free_create_order_549", + "resource_id": "FREE-439c594e1e65", + "items": [], + "ts": "2025-10-23T00:12:58+02:00" +} diff --git a/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json b/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json new file mode 100644 index 00000000..071c7e79 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json @@ -0,0 +1,11 @@ +{ + "event_type": "PAYMENT.CAPTURE.COMPLETED", + "status": "PAID", + "amount": "19.99", + "currency": "USD", + "payer": null, + "invoice": "INV-20250825-170438-e37518", + "custom": "user_1234_order_5678", + "resource_id": "2V315801FX904340P", + "ts": "2025-08-25T17:05:27-04:00" +} diff --git a/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json b/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json new file mode 100644 index 00000000..f14c95a7 --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json @@ -0,0 +1,11 @@ +{ + "event_type": "PAYMENT.CAPTURE.COMPLETED", + "status": "PAID", + "amount": "19.99", + "currency": "USD", + "payer": null, + "invoice": "INV-20250825-174311-0a7993", + "custom": "user_1234_order_5678", + "resource_id": "2V315801FX904340P", + "ts": "2025-08-25T17:05:27-04:00" +} diff --git a/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json b/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json new file mode 100644 index 00000000..338554da --- /dev/null +++ b/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json @@ -0,0 +1,10 @@ +{ + "event_type": "PAYMENT.SALE.COMPLETED", + "status": "PAID", + "amount": "0.48", + "currency": "USD", + "payer": null, + "invoice": null, + "custom": null, + "ts": "2025-08-25T16:46:11-04:00" +} diff --git a/_website/data/SIMULATED-WEBHOOK-20251022-101500.json b/modules/billing/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json similarity index 100% rename from _website/data/SIMULATED-WEBHOOK-20251022-101500.json rename to modules/billing/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json diff --git a/modules/billing/add_to_cart.php b/modules/billing/add_to_cart.php index 4b834545..0a2dc514 100644 --- a/modules/billing/add_to_cart.php +++ b/modules/billing/add_to_cart.php @@ -1,142 +1,175 @@ getSettings(); +// Start session if not already +if (session_status() === PHP_SESSION_NONE) session_start(); - //The service id should also be cast to an int. - $service_id = intval($_REQUEST['service_id']); +// Immediate request tracing log (helps confirm the script is hit) +@mkdir(__DIR__ . '/logs', 0775, true); +$trace_file = __DIR__ . '/logs/add_to_cart_requests.log'; +file_put_contents($trace_file, date('c') . " - REQUEST_METHOD=" . ($_SERVER['REQUEST_METHOD'] ?? '') . " URI=" . ($_SERVER['REQUEST_URI'] ?? '') . "\n", FILE_APPEND); - // Query for Selected service info. - $qry_service = "SELECT DISTINCT service_id, home_cfg_id, mod_cfg_id, service_name, remote_server_id, slot_max_qty, slot_min_qty, price_daily, price_monthly, price_year, description, img_url FROM OGP_DB_PREFIXbilling_services WHERE service_id=".$db->realEscapeSingle($service_id); - $result_service = $db->resultQuery($qry_service); - $row_service = $result_service[0]; - //Compiling info about invoice to create an invoice order. - - /* - Check if it's numeric before used in the WHERE clause... otherwise an SQL error is possible currently. - If it's not an int (or if it's 0 after casting and or not vaild service) redirect to the shop page. - */ - if ($service_id <= 0 || $result_service === false){ - $view->refresh("home.php?m=billing&p=shop"); - return; - } - - // remote server value - //is now held in the the IP_ID value - //$remote_server_id = $row_service['remote_server_id']; - $remote_server_id = $_POST['ip_id']; - - // request ogp user to create a home path. - $r_server = $db->getRemoteServer($remote_server_id); - $ogp_user = $r_server['ogp_user']; - - // request the user name and the game name to generate a game home name. - $home_name = $_POST['home_name']; - - //Calculating Price - if ($_POST['invoice_duration'] == "day") - { - $price_slot=$row_service['price_daily']; - } - elseif ($_POST['invoice_duration'] == "month") - { - $price_slot=$row_service['price_monthly']; - } - elseif ($_POST['invoice_duration'] == "year") - { - $price_slot=$row_service['price_year']*12; - } - else - { - $price_slot=$row_service['price_monthly']; - } - - - //Game Server Values - $ip_id = $_POST['ip_id']; - $ip = $db->getIpById($ip_id); - $max_players = $_POST['max_players']; - $qty = $_POST['qty']; - $invoice_duration = $_POST['invoice_duration']; - $user_id = $_SESSION['user_id']; - $remote_control_password = $_POST['remote_control_password']; - $ftp_password = $_POST['ftp_password']; - $tax_amount = $settings['tax_amount']; - $currency = $settings['currency']; - - /* - Cast $_REQUEST['service_id'] to an int and then check if its value is higher than 0 before using it in the WHERE clause. - Checking if it's higher than 0 because if it's a non-numeric value, after casting it to an int it'll be 0. - */ - if($service_id !== 0) $where_service_id = " WHERE service_id=".$db->realEscapeSingle($service_id); else $where_service_id = ""; - $qry_services = "SELECT * FROM OGP_DB_PREFIXbilling_services".$where_service_id; - $services = $db->resultQuery($qry_services); - foreach ($services as $key => $row) { - if($max_players < $row['slot_min_qty'] || $qty < 1){ - $max_players = $row['slot_min_qty']; - $qty = 1; - } - /* - An extra check added for the inverse: check max_players against slot_max_qty. - It would be good to do in the event someone is only selling a max of 16 slots per server. - */ - elseif ($max_players > $row['slot_max_qty']) - { - $max_players = $row['slot_max_qty']; - } - } - - - if( isset( $_POST["add_to_cart"] ) ) - { - if( isset( $_SESSION['CART'] ) ) - { - $i = count( $_SESSION['CART'] ); - $i++; - } - else - { - $i = 0; - } - - $_SESSION['CART'][$i] = array( "cart_id" => $i, - "service_id" => $service_id, - "home_name" => $home_name, - "ip" => $ip_id, - "max_players" => $max_players, - "qty" => $qty, - "invoice_duration" => $invoice_duration, - "price" => $price_slot, - "remote_control_password" => $remote_control_password, - "ftp_password" => $ftp_password, - "tax_amount" => $tax_amount, - "currency" => $currency, - "paid" => 0); - echo ''; - } +// Prefer website session id if set (login.php sets website_user_id in debug mode) +$user_id = 0; +if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) { + $user_id = intval($_SESSION['website_user_id']); +} elseif (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) { + $user_id = intval($_SESSION['user_id']); } +// If we don't have a numeric user_id but have a username, try to resolve it from the panel DB +if ($user_id <= 0 && isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])) { + $uname = trim((string)$_SESSION['website_username']); + // attempt to lookup in DB (if connection available later we will set session after connecting) + // We'll set a temporary flag to resolve after DB connection is established below + $resolve_username_for_user_id = $uname; +} else { + $resolve_username_for_user_id = null; +} +/* +if ($user_id <= 0) { + // Not logged in - redirect to login with return + $return = urlencode('/' . trim(str_replace('\\', '/', $_SERVER['REQUEST_URI']), '/')); + header('Location: ' . (isset($SITE_BASE_URL) ? $SITE_BASE_URL : '') . '/_website/login.php?return_to=' . $return); + exit; +}*/ + +// Basic validation and normalization +$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0; +$home_name = isset($_POST['home_name']) ? trim($_POST['home_name']) : ''; +$ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0; +$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0; +$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1; +$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month'; +$remote_control_password = isset($_POST['remote_control_password']) ? $_POST['remote_control_password'] : ''; +$ftp_password = isset($_POST['ftp_password']) ? $_POST['ftp_password'] : ''; + +// Price lookup: try to find service price_monthly +$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name); +if (!$db) { + // Log connection error and exit + @mkdir(__DIR__ . '/logs', 0775, true); + $trace = __DIR__ . '/logs/add_to_cart.log'; + file_put_contents($trace, date('c') . " - mysqli_connect failed: " . mysqli_connect_error() . "\n", FILE_APPEND); + die('DB connection failed'); +} else { + // Log that config was loaded (mask password) + @mkdir(__DIR__ . '/logs', 0775, true); + $trace = __DIR__ . '/logs/add_to_cart.log'; + $masked_pass = strlen($db_pass) ? '***' : ''; + file_put_contents($trace, date('c') . " - DB connected host={$db_host} user={$db_user} pass={$masked_pass} db={$db_name}\n", FILE_APPEND); +} + +// If we deferred resolving username to user_id, do it now with the DB connection +if (!empty($resolve_username_for_user_id) && $db) { + $safe_uname = mysqli_real_escape_string($db, $resolve_username_for_user_id); + // users_login is the correct column name in this schema + $q = mysqli_query($db, "SELECT user_id FROM ogp_users WHERE users_login = '$safe_uname' LIMIT 1"); + if ($q && mysqli_num_rows($q) === 1) { + $r = mysqli_fetch_assoc($q); + $user_id = intval($r['user_id'] ?? 0); + // persist into session for subsequent requests + if ($user_id > 0) { + $_SESSION['website_user_id'] = $user_id; + site_log_info('resolved_user_id_from_username', ['username'=>$resolve_username_for_user_id,'user_id'=>$user_id]); + // Also resolve and persist the user's role so menus and admin checks are consistent + $role_q = mysqli_query($db, "SELECT users_role FROM ogp_users WHERE user_id = " . intval($user_id) . " LIMIT 1"); + if ($role_q && mysqli_num_rows($role_q) === 1) { + $role_row = mysqli_fetch_assoc($role_q); + $_SESSION['website_user_role'] = $role_row['users_role'] ?? ''; + } + } + } else { + site_log_warn('resolve_user_failed', ['username'=>$resolve_username_for_user_id]); + } +} + +$price = 0.0; +if ($service_id > 0) { + $stmt = $db->prepare('SELECT price_monthly, slot_min_qty, slot_max_qty FROM ogp_billing_services WHERE service_id = ? LIMIT 1'); + if ($stmt) { + $stmt->bind_param('i', $service_id); + $stmt->execute(); + $stmt->bind_result($price_monthly, $slot_min_qty, $slot_max_qty); + if ($stmt->fetch()) { + $price = floatval($price_monthly); + // constrain slots + if ($max_players < $slot_min_qty) $max_players = $slot_min_qty; + if ($max_players > $slot_max_qty) $max_players = $slot_max_qty; + } + $stmt->close(); + } +} + +// Insert into ogp_billing_orders +$now = date('Y-m-d H:i:s'); +$status = 'in-cart'; + +// 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'); + +// Build and execute a simple INSERT using mysqli_query for debugging clarity +@mkdir(__DIR__ . '/logs', 0775, true); +$logfile = __DIR__ . '/logs/add_to_cart.log'; +site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]); + +// Escape values +$esc_user_id = intval($user_id); +$esc_service_id = intval($service_id); +$esc_home_name = mysqli_real_escape_string($db, $home_name); +$esc_ip_id = intval($ip_id); +$esc_max_players = intval($max_players); +$esc_qty = intval($qty); +$esc_invoice_duration = mysqli_real_escape_string($db, $invoice_duration); +$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); + +$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); +} + +site_log_info('add_to_cart_sql', ['sql'=>$sql]); + +$res = mysqli_query($db, $sql); +if (!$res) { + $err_no = mysqli_errno($db); + $err = mysqli_error($db); + site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]); + // Log table existence check + $tbl_check = mysqli_query($db, "SHOW TABLES LIKE 'ogp_billing_orders'"); + $tbl_exists = ($tbl_check && mysqli_num_rows($tbl_check) > 0) ? 'yes' : 'no'; + site_log_warn('ogp_billing_orders_exists', ['exists'=>$tbl_exists]); +} 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]); +} + +// Redirect to cart page +header('Location: cart.php'); +exit; + ?> diff --git a/_website/admin.php b/modules/billing/admin.php similarity index 91% rename from _website/admin.php rename to modules/billing/admin.php index 738dc177..529fa897 100644 --- a/_website/admin.php +++ b/modules/billing/admin.php @@ -36,7 +36,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }

Sandbox account (testing)

-

Use PayPal sandbox credentials when testing payments. Set your sandbox client_id and client_secret in the runtime config that the payment handlers use (for this site those are in the respective files under _website/paypal/ and _website/payments/ or in a central config if you moved credentials).

+

Use PayPal sandbox credentials when testing payments. Set your sandbox client_id and client_secret in the runtime config that the payment handlers use (for this site those are in the respective files under _website/api/ or in a central config if you moved credentials).