From 4e73997a4b75c0d002024a852c698942b582ab45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:42:24 +0000 Subject: [PATCH] Refactor process_payment_record into reusable payment_processor.php helper Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- modules/billing/cart.php | 9 +- .../billing/includes/payment_processor.php | 186 ++++++++++++++++++ modules/billing/payment_success.php | 178 +---------------- modules/billing/webhook.php | 10 +- 4 files changed, 196 insertions(+), 187 deletions(-) create mode 100644 modules/billing/includes/payment_processor.php diff --git a/modules/billing/cart.php b/modules/billing/cart.php index 9f72aa1b..558ce18a 100644 --- a/modules/billing/cart.php +++ b/modules/billing/cart.php @@ -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)); // If available, process the payment record immediately so webhooks logic runs during creation - $ps = __DIR__ . '/payment_success.php'; - if (is_file($ps)) { - try { - require_once($ps); + require_once(__DIR__ . '/includes/payment_processor.php'); + try { if (function_exists('process_payment_record')) { process_payment_record($rec); } - } catch (Exception $e) { + } catch (Exception $e) { error_log('[cart create_free] process_payment_record failed: ' . $e->getMessage()); - } } header('Location: return.php?invoice=' . urlencode($rec['invoice'])); diff --git a/modules/billing/includes/payment_processor.php b/modules/billing/includes/payment_processor.php new file mode 100644 index 00000000..55544ca8 --- /dev/null +++ b/modules/billing/includes/payment_processor.php @@ -0,0 +1,186 @@ +$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; + } + } +} +?> diff --git a/modules/billing/payment_success.php b/modules/billing/payment_success.php index 01245636..6aaea156 100644 --- a/modules/billing/payment_success.php +++ b/modules/billing/payment_success.php @@ -9,183 +9,7 @@ require_once(__DIR__ . '/includes/header.php'); require_once(__DIR__ . '/includes/config.inc.php'); require_once(__DIR__ . '/../../includes/database_mysqli.php'); require_once(__DIR__ . '/includes/log.php'); - -/** - * 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 - $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; - } -} +require_once(__DIR__ . '/includes/payment_processor.php'); $invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : ''; $user_id = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0; diff --git a/modules/billing/webhook.php b/modules/billing/webhook.php index 873d1496..cf628247 100644 --- a/modules/billing/webhook.php +++ b/modules/billing/webhook.php @@ -148,10 +148,12 @@ if (in_array($type, ['PAYMENT.CAPTURE.COMPLETED','PAYMENT.SALE.COMPLETED'], true $status = 'WROTE_FILE'; // Attempt to mark order paid in DB - $ps = __DIR__ . '/payment_success.php'; - if (is_file($ps)) { - require_once($ps); - 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()); } + require_once(__DIR__ . '/includes/payment_processor.php'); + 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()); } }