From 93d47dba4021b8dc27b5dc010b8dfa8e91c81441 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:59:31 +0000 Subject: [PATCH] Add coupon application logic to cart.php with discount calculation Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- modules/billing/cart.php | 121 +++++- modules/billing/cart.php.backup | 641 ++++++++++++++++++++++++++++++++ 2 files changed, 760 insertions(+), 2 deletions(-) create mode 100644 modules/billing/cart.php.backup diff --git a/modules/billing/cart.php b/modules/billing/cart.php index 558ce18a..83f2ca5a 100644 --- a/modules/billing/cart.php +++ b/modules/billing/cart.php @@ -271,6 +271,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_single'])) { } } +// Handle coupon application +$coupon_message = ''; +$coupon_error = ''; +$applied_coupon = null; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['apply_coupon'])) { + $coupon_code = trim($_POST['coupon_code']); + + if (!empty($coupon_code)) { + // Validate and fetch coupon + $safe_code = mysqli_real_escape_string($db, $coupon_code); + $coupon_query = "SELECT * FROM {$table_prefix}billing_coupons + WHERE code = '$safe_code' + AND is_active = 1 + AND (expires IS NULL OR expires > NOW()) + LIMIT 1"; + $coupon_result = mysqli_query($db, $coupon_query); + + if ($coupon_result && mysqli_num_rows($coupon_result) === 1) { + $coupon = mysqli_fetch_assoc($coupon_result); + + // Check usage limits + if ($coupon['max_uses'] !== null && intval($coupon['current_uses']) >= intval($coupon['max_uses'])) { + $coupon_error = "This coupon has reached its usage limit."; + } else { + // Store coupon in session for later use + $_SESSION['applied_coupon'] = $coupon; + $applied_coupon = $coupon; + $coupon_message = "Coupon '{$coupon['code']}' applied successfully! " . + number_format($coupon['discount_percent'], 2) . "% discount " . + ($coupon['usage_type'] === 'permanent' ? '(permanent - applies to all renewals)' : '(one-time only)'); + } + } else { + $coupon_error = "Invalid or expired coupon code."; + } + } +} + +// Handle coupon removal +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_coupon'])) { + unset($_SESSION['applied_coupon']); + $coupon_message = "Coupon removed."; +} + +// Check if there's a coupon in session +if (isset($_SESSION['applied_coupon']) && !$applied_coupon) { + $applied_coupon = $_SESSION['applied_coupon']; +} + if ($db){ $carts = $db->query("SELECT * FROM ogp_billing_invoices AS cart WHERE status = 'due' AND user_id = " . $user_id . " ORDER BY invoice_id ASC"); @@ -301,6 +350,28 @@ if ($db){ num_rows > 0) { while ($row = $carts->fetch_assoc()) { @@ -319,14 +390,28 @@ if ($db){ $ - + 'invoice-' . $row['invoice_id'], 'amount' => number_format($rowtotal, 2, '.', ''), - 'invoice_id' => intval($row['invoice_id']) + 'invoice_id' => intval($row['invoice_id']), + 'discount' => number_format($itemDiscount, 2, '.', ''), + 'original_amount' => number_format($row['amount'] * $row['qty'] * $row['max_players'], 2, '.', ''), + 'coupon_id' => $applied_coupon ? intval($applied_coupon['coupon_id']) : null ]; ?> + +
+ +
+ +
+ + + +
+ +
+ + + +
+ Active Coupon: + (% off) +
+ +
+
+ +
+ + + +
+ +
+ + + + + + Shopping Cart - GameServers.World + + + 0) { + $ar = mysqli_query($db, "SELECT users_role FROM ogp_users WHERE user_id = " . intval($actor_id) . " LIMIT 1"); + if ($ar && mysqli_num_rows($ar) === 1) { + $arr = mysqli_fetch_assoc($ar); + if (strtolower((string)($arr['users_role'] ?? '')) === 'admin') { + $is_admin = true; + $_SESSION['website_user_role'] = 'admin'; + } + } + } elseif (isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])) { + $safe_un = mysqli_real_escape_string($db, $_SESSION['website_username']); + $ar = mysqli_query($db, "SELECT user_id, users_role FROM ogp_users WHERE users_login = '$safe_un' LIMIT 1"); + if ($ar && mysqli_num_rows($ar) === 1) { + $arr = mysqli_fetch_assoc($ar); + if (strtolower((string)($arr['users_role'] ?? '')) === 'admin') { + $is_admin = true; + $_SESSION['website_user_role'] = 'admin'; + $_SESSION['website_user_id'] = intval($arr['user_id'] ?? 0); + } + } + } + } + $orderId = (int)$_POST['create_free_for']; + if ($orderId > 0) { + // load invoice to verify ownership/price (invoice-first flow) + $stmt = $db->prepare("SELECT user_id, amount, status, qty, invoice_duration, service_id, home_name, ip, max_players, remote_control_password, ftp_password FROM " . $table_prefix . "billing_invoices WHERE invoice_id = ? LIMIT 1"); + if ($stmt) { + $stmt->bind_param('i', $orderId); + $stmt->execute(); + $stmt->bind_result($owner_id, $order_price, $prev_status, $order_qty, $order_invoice_duration, $service_id, $home_name, $ip, $max_players, $remote_control_password, $ftp_password); + $found = $stmt->fetch(); + $stmt->close(); + } else { + $found = false; + } + + $audit_file = __DIR__ . '/logs/free_create_audit.log'; + + if ($found) { + $allowed = false; + $reason = ''; + // Admin may force-create paid records for testing + if ($is_admin) { + $allowed = true; + $reason = 'admin_create'; + } + // Owner may claim a free order if the price is zero + elseif ($actor_id > 0 && $actor_id === intval($owner_id) && floatval($order_price) == 0.0) { + $allowed = true; + $reason = 'user_claim_free'; + } + + if ($allowed) { + // Mark invoice as paid + $upd_inv = $db->prepare("UPDATE " . $table_prefix . "billing_invoices SET status = 'paid', paid_date = NOW() WHERE invoice_id = ? LIMIT 1"); + if ($upd_inv) { + $upd_inv->bind_param('i', $orderId); + $upd_inv->execute(); + $upd_inv->close(); + } + + // Now create the order record (invoice -> order after payment) + // Compute end_date: months based on invoice_duration and qty + $months = 0; + $q = intval($order_qty ?? 0); + $invdur = strtolower(trim($order_invoice_duration ?? '')); + if (strpos($invdur, 'year') !== false) { + $months = $q * 12; + } else { + // default to months for anything else (month, monthly, etc.) + $months = $q; + } + $end_date = null; + if ($months > 0) { + $dt = new DateTime('now'); + $dt->modify('+' . intval($months) . ' months'); + $end_date = $dt->format('Y-m-d H:i:s'); + } else { + // if no months specified, set to now + $end_date = date('Y-m-d H:i:s'); + } + + // INSERT new order record (invoice->order after payment) + $esc_service_id = intval($service_id); + $esc_home_name = mysqli_real_escape_string($db, $home_name); + $esc_ip = intval($ip); + $esc_max_players = intval($max_players); + $esc_qty = intval($order_qty); + $esc_inv_dur = mysqli_real_escape_string($db, $order_invoice_duration); + $esc_price = floatval($order_price); + $esc_rc_pass = mysqli_real_escape_string($db, $remote_control_password); + $esc_ftp_pass = mysqli_real_escape_string($db, $ftp_password); + $esc_user_id = intval($owner_id); + $esc_end_date = mysqli_real_escape_string($db, $end_date); + + $insert_sql = "INSERT INTO " . $table_prefix . "billing_orders + (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, end_date, payment_txid, paid_ts) + VALUES + ({$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip}, {$esc_max_players}, {$esc_qty}, '{$esc_inv_dur}', {$esc_price}, '{$esc_rc_pass}', '{$esc_ftp_pass}', 'paid', '{$esc_end_date}', 'FREE-{$orderId}', NOW())"; + + $insert_res = mysqli_query($db, $insert_sql); + $new_order_id = 0; + if ($insert_res) { + $new_order_id = mysqli_insert_id($db); + // Update invoice with the new order_id + $upd_inv_order = $db->prepare("UPDATE " . $table_prefix . "billing_invoices SET order_id = ? WHERE invoice_id = ? LIMIT 1"); + if ($upd_inv_order) { + $upd_inv_order->bind_param('ii', $new_order_id, $orderId); + $upd_inv_order->execute(); + $upd_inv_order->close(); + } + } + + // write audit log (include end_date if set) + site_log_info('free_create', ['actor'=>$actor_id, 'role'=>$actor_role, 'action'=>$reason, 'invoice'=>$orderId, 'new_order'=>$new_order_id, 'owner'=>$owner_id, 'price'=>$order_price, 'prev_status'=>$prev_status, 'end_date'=>$end_date ?? '']); + + // write a simulated webhook file (same behavior as previous admin flow) + $dataDir = (isset($SITE_DATA_DIR) && $SITE_DATA_DIR) ? $SITE_DATA_DIR : realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'data'; + @mkdir($dataDir, 0775, true); + $rec = [ + 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', + 'status' => 'PAID', + 'amount' => floatval($order_price), + 'currency' => 'USD', + 'payer' => $_SESSION['website_user_email'] ?? ($_SESSION['website_username'] ?? ''), + 'invoice' => 'FREE-' . $orderId . '-' . time(), + // process_payment_record matches numeric custom values to order_id; use numeric order id here to ensure matching + 'custom' => (string)$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)); + + // If available, process the payment record immediately so webhooks logic runs during creation + require_once(__DIR__ . '/includes/payment_processor.php'); + try { + if (function_exists('process_payment_record')) { + process_payment_record($rec); + } + } catch (Exception $e) { + error_log('[cart create_free] process_payment_record failed: ' . $e->getMessage()); + } + + header('Location: return.php?invoice=' . urlencode($rec['invoice'])); + exit; + } else { + // unauthorized attempt - log and continue + site_log_warn('unauthorized_free_create', ['actor'=>$actor_id, 'role'=>$actor_role, 'order'=>$orderId, 'owner'=>$owner_id, 'price'=>$order_price]); + } + } + } +} + +// Include top bar and menu +include(__DIR__ . '/includes/top.php'); +include(__DIR__ . '/includes/menu.php'); + +// Use session user_id where available +// Use session user_id where available; if not present but website_username exists, try to resolve it from DB +$user_id = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0); +if ($user_id <= 0 && isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])) { + // try to resolve username to user_id in DB and persist into session + $safe_uname = mysqli_real_escape_string($db, $_SESSION['website_username']); + $qr = mysqli_query($db, "SELECT user_id FROM ogp_users WHERE users_login = '$safe_uname' LIMIT 1"); + if ($qr && mysqli_num_rows($qr) === 1) { + $rr = mysqli_fetch_assoc($qr); + $user_id = intval($rr['user_id'] ?? 0); + if ($user_id > 0) { + $_SESSION['website_user_id'] = $user_id; + site_log_info('cart_resolved_user_id', ['username'=>$_SESSION['website_username'],'user_id'=>$user_id]); + // Resolve and persist the user's role to avoid extra DB lookups later + $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_r = mysqli_fetch_assoc($role_q); + $_SESSION['website_user_role'] = $role_r['users_role'] ?? ''; + } + } + } else { + site_log_warn('cart_resolve_user_failed', ['username'=>$_SESSION['website_username']]); + } +} + +if ($user_id <= 0) { + echo "

Please login to view your cart

"; + mysqli_close($db); + echo ""; + return; +} + +// Determine admin status for UI: prefer session role, otherwise check DB +$is_admin = false; +if (isset($_SESSION['website_user_role']) && !empty($_SESSION['website_user_role'])) { + $is_admin = (strtolower($_SESSION['website_user_role']) === 'admin'); +} elseif ($user_id > 0) { + $rr = mysqli_query($db, "SELECT users_role FROM ogp_users WHERE user_id = " . intval($user_id) . " LIMIT 1"); + if ($rr && mysqli_num_rows($rr) === 1) { + $rrow = mysqli_fetch_assoc($rr); + $is_admin = (strtolower((string)($rrow['users_role'] ?? '')) === 'admin'); + } +} + + + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_single'])) { + $invoice_id = intval($_POST['delete_single']); + if ($invoice_id > 0) { + // Check if this invoice is linked to an order (renewal case) + $stmt = $db->prepare("SELECT order_id FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?"); + $stmt->bind_param("ii", $invoice_id, $user_id); + $stmt->execute(); + $stmt->bind_result($linked_order_id); + $found = $stmt->fetch(); + $stmt->close(); + + if ($found && $linked_order_id > 0) { + // This is a renewal invoice - just delete the invoice, keep the order + $delete = $db->prepare("DELETE FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?"); + $delete->bind_param("ii", $invoice_id, $user_id); + $delete->execute(); + if (isset($db) && method_exists($db, 'logger')) { + $db->logger("USER-CART: User " . intval($user_id) . " deleted renewal invoice " . intval($invoice_id)); + } + $delete->close(); + } else { + // New order invoice - delete it + $delete = $db->prepare("DELETE FROM ogp_billing_invoices WHERE invoice_id = ? AND user_id = ?"); + $delete->bind_param("ii", $invoice_id, $user_id); + $delete->execute(); + if (isset($db) && method_exists($db, 'logger')) { + $db->logger("USER-CART: User " . intval($user_id) . " deleted invoice " . intval($invoice_id)); + } + $delete->close(); + } + } +} + +// Handle coupon application +$coupon_message = ''; +$coupon_error = ''; +$applied_coupon = null; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['apply_coupon'])) { + $coupon_code = trim($_POST['coupon_code']); + + if (!empty($coupon_code)) { + // Validate and fetch coupon + $safe_code = mysqli_real_escape_string($db, $coupon_code); + $coupon_query = "SELECT * FROM {$table_prefix}billing_coupons + WHERE code = '$safe_code' + AND is_active = 1 + AND (expires IS NULL OR expires > NOW()) + LIMIT 1"; + $coupon_result = mysqli_query($db, $coupon_query); + + if ($coupon_result && mysqli_num_rows($coupon_result) === 1) { + $coupon = mysqli_fetch_assoc($coupon_result); + + // Check usage limits + if ($coupon['max_uses'] !== null && intval($coupon['current_uses']) >= intval($coupon['max_uses'])) { + $coupon_error = "This coupon has reached its usage limit."; + } else { + // Store coupon in session for later use + $_SESSION['applied_coupon'] = $coupon; + $applied_coupon = $coupon; + $coupon_message = "Coupon '{$coupon['code']}' applied successfully! " . + number_format($coupon['discount_percent'], 2) . "% discount " . + ($coupon['usage_type'] === 'permanent' ? '(permanent - applies to all renewals)' : '(one-time only)'); + } + } else { + $coupon_error = "Invalid or expired coupon code."; + } + } +} + +// Handle coupon removal +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_coupon'])) { + unset($_SESSION['applied_coupon']); + $coupon_message = "Coupon removed."; +} + +// Check if there's a coupon in session +if (isset($_SESSION['applied_coupon']) && !$applied_coupon) { + $applied_coupon = $_SESSION['applied_coupon']; +} + +if ($db){ + $carts = $db->query("SELECT * FROM ogp_billing_invoices AS cart + WHERE status = 'due' AND user_id = " . $user_id . " ORDER BY invoice_id ASC"); +} + +?> + +
+

Your Cart

+ + + + + + + + + + + + + + + + + + num_rows > 0) { + while ($row = $carts->fetch_assoc()) { + ?> + + + + + + + + + + 'invoice-' . $row['invoice_id'], + 'amount' => number_format($rowtotal, 2, '.', ''), + 'invoice_id' => intval($row['invoice_id']) + ]; + ?> + + + + + + + + + + + + + + + + + + + + + + +
Server IDGame NameLocationMax PlayersPrice per PlayerMonthsTotal
+
+ +
+
$ +
+ + +
+ +
Admin: force-create a paid record for testing.
+ +
 $
+ Cart Total: + + $ +
No items in your cart.
+ + +
+ +
+ +
+ + + +
+ +
+ + + +
+ Active Coupon: + (% off) +
+ +
+
+ +
+ + + +
+ +
+ + +'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"; +// If the cart contains a single order, set custom_id to the numeric order id so webhooks +// can match the order directly (payment_success matches numeric custom -> order_id). +if (is_array($invoice) && count($invoice) === 1 && !empty($invoice[0]['order_id'])) { + $customId = (string) intval($invoice[0]['order_id']); +} + +// Text on the PayPal side +$description = 'Game server order (' . count($lineItems) . ' item' . (count($lineItems)===1?'': 's') . ')'; + +// URLs +// Define the site base URL - detect protocol and host dynamically +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; +$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; +$siteBase = $protocol . $host; + +// Return URLs are root-relative (website will be deployed at root, not modules/billing) +$returnUrl = $siteBase . '/payment_success.php?invoice=' . urlencode($invoiceId); +$cancelUrl = $siteBase . '/payment_cancel.php?invoice=' . urlencode($invoiceId); + +// API base (relative) - point to billing module API endpoints +$apiBase = 'api'; +?> + + + + + +
+ Debug Info:
+ Grand Total: $
+ Invoice Items:
+ Line Items:
+ Amount:
+ Invoice ID:
+ Custom ID:
+
+ + +
+
+ + + + +
+ + + + +