refactor(billing): clean architecture with payment gateway abstraction

- Add PaymentGatewayInterface contract for all payment providers
- Add PayPalGateway (reads credentials from config, not hardcoded)
- Add ManualGateway for admin-triggered payments
- Add StripeGateway stub for future implementation
- Add GatewayFactory for gateway instantiation by name
- Add BillingRepository: parameterized-SQL data layer
- Add BillingService: pricing, invoice creation, payment processing
- Add gsp_billing_transactions table (DB version 2) for audit trail
- Add new columns to gsp_billing_invoices (home_id, rate_type, players, period_start/end, subtotal, total_due, payment_status)
- Add gsp_billing_service_remote_servers mapping table
- Move PayPal credentials from api files into config.inc.php
- Fix double session_start() bug in capture_order.php
- Replace raw SQL with prepared statements throughout
- Refactor admin_invoices.php to use billing_invoices + BillingRepository
- Refactor admin_payments.php to read from gsp_billing_transactions
- Update admin.php with links to Transaction Log and Manage Invoices

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-02 12:17:36 +00:00 committed by GitHub
parent b0e00c9370
commit 986a4e53b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1227 additions and 749 deletions

View file

@ -1,26 +1,30 @@
<?php
/**
* PayPal Order Capture Endpoint
* Processes PayPal payment, marks invoices paid, creates order records
* Standalone billing module - uses only standard PHP mysqli
* Uses PayPalGateway, BillingService, and BillingRepository.
* Credentials come from config NOT hardcoded here.
*/
require_once(__DIR__ . '/../includes/config_loader.php');
// Prevent any output before JSON
ob_start();
ini_set('display_errors', '0');
error_reporting(E_ALL);
ob_start();
// Setup logging
$logDir = __DIR__ . '/../logs';
require_once __DIR__ . '/../includes/config_loader.php';
require_once __DIR__ . '/../classes/PaymentGatewayInterface.php';
require_once __DIR__ . '/../classes/PayPalGateway.php';
require_once __DIR__ . '/../classes/GatewayFactory.php';
require_once __DIR__ . '/../classes/BillingRepository.php';
require_once __DIR__ . '/../classes/BillingService.php';
// Logging setup
$logDir = __DIR__ . '/../logs';
@mkdir($logDir, 0755, true);
$logFile = $logDir . '/payment_capture.log';
$logFile = $logDir . '/payment_capture.log';
$requestId = uniqid('req_', true);
function log_payment($label, $data) {
function cap_log(string $label, $data): void {
global $logFile, $requestId;
$entry = "[" . date('Y-m-d H:i:s') . "] [$requestId] $label\n";
$entry = '[' . date('Y-m-d H:i:s') . "] [$requestId] $label\n";
$entry .= is_array($data) || is_object($data) ? print_r($data, true) : (string)$data;
$entry .= "\n" . str_repeat('-', 80) . "\n";
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
@ -28,365 +32,140 @@ function log_payment($label, $data) {
header('Content-Type: application/json');
// Parse input
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
log_payment('JSON_ERROR', json_last_error_msg());
ob_clean();
echo json_encode(['error' => 'invalid_json', 'request_id' => $requestId]);
exit;
}
$paypal_order_id = $input['order_id'] ?? null;
if (!$paypal_order_id) {
log_payment('MISSING_ORDER_ID', $input);
ob_clean();
echo json_encode(['error' => 'missing_order_id', 'request_id' => $requestId]);
exit;
}
log_payment('REQUEST_START', ['order_id' => $paypal_order_id]);
// PayPal API configuration
$sandbox = true;
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
$api = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
// Get OAuth token
$ch = curl_init("$api/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 => "$client_id:$client_secret",
]);
$tokenResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
log_payment('OAUTH_FAILED', ['http_code' => $httpCode, 'response' => $tokenResponse]);
ob_clean();
echo json_encode(['error' => 'oauth_failed', 'request_id' => $requestId]);
exit;
}
$tokenData = json_decode($tokenResponse, true);
$accessToken = $tokenData['access_token'] ?? null;
if (!$accessToken) {
log_payment('NO_ACCESS_TOKEN', $tokenData);
ob_clean();
echo json_encode(['error' => 'no_access_token', 'request_id' => $requestId]);
exit;
}
// Capture the PayPal order
$ch = curl_init("$api/v2/checkout/orders/$paypal_order_id/capture");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer $accessToken"
],
]);
$captureResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 201 && $httpCode !== 200) {
log_payment('CAPTURE_FAILED', ['http_code' => $httpCode, 'response' => substr($captureResponse, 0, 500)]);
ob_clean();
echo json_encode(['error' => 'capture_failed', 'http_code' => $httpCode, 'request_id' => $requestId]);
exit;
}
$captureData = json_decode($captureResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
log_payment('CAPTURE_JSON_ERROR', json_last_error_msg());
ob_clean();
echo json_encode(['error' => 'capture_json_error', 'request_id' => $requestId]);
exit;
}
$status = $captureData['status'] ?? null;
if ($status !== 'COMPLETED') {
log_payment('NOT_COMPLETED', ['status' => $status]);
ob_clean();
echo json_encode(['error' => 'payment_not_completed', 'status' => $status, 'request_id' => $requestId]);
exit;
}
// Extract transaction ID
$txid = $captureData['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
$payer_email = $captureData['payer']['email_address'] ?? '';
$payer_name = ($captureData['payer']['name']['given_name'] ?? '') . ' ' . ($captureData['payer']['name']['surname'] ?? '');
// Store full PayPal response as JSON for admin/refund tracking
$paypal_json = json_encode($captureData);
log_payment('PAYMENT_CAPTURED', [
'txid' => $txid,
'payer_email' => $payer_email,
'payer_name' => trim($payer_name)
]);
// Start session to get user_id (use billing website session name)
// Session (single call)
if (session_status() === PHP_SESSION_NONE) {
session_name("opengamepanel_web");
session_name('opengamepanel_web');
session_start();
}
$user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) :
(isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0);
if ($user_id <= 0) {
log_payment('NO_USER_SESSION', $_SESSION);
$userId = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0);
if ($userId <= 0) {
cap_log('NO_USER_SESSION', ['session_keys' => array_keys($_SESSION)]);
ob_clean();
echo json_encode(['error' => 'no_user_session', 'request_id' => $requestId]);
exit;
}
// Connect to database
$mysqli = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
// Parse input
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
ob_clean();
echo json_encode(['error' => 'invalid_json', 'request_id' => $requestId]);
exit;
}
$paypalOrderId = $input['order_id'] ?? null;
if (!$paypalOrderId) {
ob_clean();
echo json_encode(['error' => 'missing_order_id', 'request_id' => $requestId]);
exit;
}
cap_log('REQUEST', ['order_id' => $paypalOrderId, 'user_id' => $userId]);
// DB connection
$port = intval($db_port ?? 3306) ?: 3306;
$mysqli = @mysqli_connect($db_host, $db_user, $db_pass, $db_name, $port);
if (!$mysqli) {
log_payment('DB_CONNECTION_FAILED', mysqli_connect_error());
cap_log('DB_FAILED', mysqli_connect_error());
ob_clean();
echo json_encode(['error' => 'db_connection_failed', 'request_id' => $requestId]);
exit;
}
mysqli_set_charset($mysqli, 'utf8mb4');
$now = date('Y-m-d H:i:s');
$esc_txid = mysqli_real_escape_string($mysqli, $txid);
$esc_paypal_json = mysqli_real_escape_string($mysqli, $paypal_json);
$prefix = $table_prefix ?? 'gsp_';
$repo = new BillingRepository($mysqli, $prefix);
$svc = new BillingService($repo);
// Apply coupon from session to invoices before marking paid
session_start();
$coupon_id = isset($_SESSION['cart_coupon_id']) ? intval($_SESSION['cart_coupon_id']) : 0;
if ($coupon_id > 0) {
// Get unpaid invoices for this user to apply coupon
$invoices_query = "SELECT invoice_id, amount FROM {$table_prefix}billing_invoices
WHERE user_id=$user_id AND status='due'";
$invoices_result = mysqli_query($mysqli, $invoices_query);
// Get coupon details
$coupon_query = "SELECT discount_percent FROM {$table_prefix}billing_coupons
WHERE coupon_id=$coupon_id AND is_active=1 LIMIT 1";
$coupon_result = mysqli_query($mysqli, $coupon_query);
if ($coupon_result && mysqli_num_rows($coupon_result) === 1) {
$coupon_row = mysqli_fetch_assoc($coupon_result);
$discount_percent = floatval($coupon_row['discount_percent']);
// Update each invoice with coupon
while ($inv_row = mysqli_fetch_assoc($invoices_result)) {
$inv_id = intval($inv_row['invoice_id']);
$inv_amount = floatval($inv_row['amount']);
$discount_amt = $inv_amount * ($discount_percent / 100);
$new_amount = $inv_amount - $discount_amt;
$update_coupon_sql = "UPDATE {$table_prefix}billing_invoices
SET coupon_id=$coupon_id,
discount_amount=" . number_format($discount_amt, 2, '.', '') . ",
amount=" . number_format($new_amount, 2, '.', '') . "
WHERE invoice_id=$inv_id";
mysqli_query($mysqli, $update_coupon_sql);
log_payment('COUPON_APPLIED', ['invoice_id' => $inv_id, 'discount' => $discount_amt]);
}
// Increment coupon usage
$update_usage_sql = "UPDATE {$table_prefix}billing_coupons
SET current_uses = current_uses + 1
WHERE coupon_id=$coupon_id";
mysqli_query($mysqli, $update_usage_sql);
// Clear coupon from session
unset($_SESSION['cart_coupon_code']);
unset($_SESSION['cart_coupon_id']);
}
}
// Mark all due invoices for this user as paid.
// Note: billing_invoices is the pre-purchase cart table and uses its own
// status vocabulary (due -> paid). This is separate from gsp_invoices
// (renewal invoices) and server_homes.billing_status (Active/Invoiced/Expired).
$updateInvoicesSql = "UPDATE {$table_prefix}billing_invoices
SET status='paid', paid_date='$now', payment_txid='$esc_txid', payment_method='paypal'
WHERE user_id=$user_id AND status='due'";
$updateResult = mysqli_query($mysqli, $updateInvoicesSql);
if (!$updateResult) {
log_payment('UPDATE_INVOICES_FAILED', mysqli_error($mysqli));
mysqli_close($mysqli);
// Capture payment via PayPal gateway
try {
$gateway = GatewayFactory::make('paypal');
} catch (Exception $e) {
cap_log('GATEWAY_ERROR', $e->getMessage());
ob_clean();
echo json_encode(['error' => 'update_invoices_failed', 'request_id' => $requestId]);
echo json_encode(['error' => 'gateway_init_failed', 'request_id' => $requestId]);
mysqli_close($mysqli);
exit;
}
$affectedInvoices = mysqli_affected_rows($mysqli);
log_payment('INVOICES_MARKED_PAID', ['count' => $affectedInvoices]);
$capture = $gateway->handleCallback(['order_id' => $paypalOrderId]);
cap_log('CAPTURE_RESULT', ['success' => $capture['success'], 'txid' => $capture['transaction_id'] ?? null]);
// Get all invoices we just marked paid
$getInvoicesSql = "SELECT * FROM {$table_prefix}billing_invoices
WHERE user_id=$user_id AND payment_txid='$esc_txid'";
$invoicesResult = mysqli_query($mysqli, $getInvoicesSql);
if (!$capture['success']) {
cap_log('CAPTURE_FAILED', $capture);
ob_clean();
echo json_encode(['error' => $capture['error'] ?? 'capture_failed', 'request_id' => $requestId]);
mysqli_close($mysqli);
exit;
}
$txid = $capture['transaction_id'] ?? '';
$capture['payment_method'] = 'paypal';
// Process each unpaid invoice for this user
$invoices = $repo->getUnpaidInvoicesForUser($userId);
$invoicesPaid = 0;
$ordersCreated = 0;
$renewedOrders = 0;
$newOrderIds = [];
while ($inv = mysqli_fetch_assoc($invoicesResult)) {
$invoice_id = intval($inv['invoice_id']);
$existing_order_id = intval($inv['order_id'] ?? 0);
// Handle renewals by extending the existing order
if ($existing_order_id > 0) {
$durationUnit = strtolower(trim($inv['invoice_duration'] ?? 'month'));
$allowedDurations = ['day','month','year'];
if (!in_array($durationUnit, $allowedDurations, true)) {
$durationUnit = 'month';
}
$qty = max(1, intval($inv['qty'] ?? 1));
$orderInfoSql = "SELECT end_date FROM {$table_prefix}billing_orders WHERE order_id=$existing_order_id LIMIT 1";
$orderInfoRes = mysqli_query($mysqli, $orderInfoSql);
$currentEnd = null;
if ($orderInfoRes && mysqli_num_rows($orderInfoRes) === 1) {
$infoRow = mysqli_fetch_assoc($orderInfoRes);
$currentEnd = $infoRow['end_date'] ?? null;
}
$baseTs = time();
if (!empty($currentEnd)) {
$parsed = strtotime($currentEnd);
if ($parsed !== false && $parsed > time()) {
$baseTs = $parsed;
}
}
$newEndDate = date('Y-m-d H:i:s', strtotime("+$qty $durationUnit", $baseTs));
$renewSql = "UPDATE {$table_prefix}billing_orders
SET status='Active', end_date='$newEndDate', paid_ts='$now', payment_txid='$esc_txid'
WHERE order_id=$existing_order_id LIMIT 1";
if (mysqli_query($mysqli, $renewSql)) {
$renewedOrders++;
log_payment('ORDER_RENEWED', [
'order_id' => $existing_order_id,
'invoice_id' => $invoice_id,
'new_end_date' => $newEndDate
]);
$newOrderIds = [];
$now = date('Y-m-d H:i:s');
// Also update server_homes.billing_status and next_invoice_date
$homeIdRow = mysqli_query($mysqli, "SELECT home_id FROM {$table_prefix}billing_orders WHERE order_id=$existing_order_id LIMIT 1");
if ($homeIdRow && mysqli_num_rows($homeIdRow) === 1) {
$homeData = mysqli_fetch_assoc($homeIdRow);
$home_id_upd = intval($homeData['home_id'] ?? 0);
if ($home_id_upd > 0) {
$next_inv_date = mysqli_real_escape_string($mysqli, $newEndDate);
mysqli_query($mysqli, "UPDATE {$table_prefix}server_homes
SET billing_status = 'Active',
next_invoice_date = '$next_inv_date',
server_expiration_date = NULL
WHERE home_id = $home_id_upd");
// Mark the matching gsp_invoices renewal invoice as Active
mysqli_query($mysqli, "UPDATE {$table_prefix}invoices
SET billing_status = 'Active',
paid_at = '$now',
payment_id = '$esc_txid'
WHERE home_id = $home_id_upd AND billing_status = 'Invoiced'");
log_payment('SERVER_HOME_ACTIVATED', ['home_id' => $home_id_upd]);
}
}
} else {
log_payment('ORDER_RENEWAL_FAILED', [
'order_id' => $existing_order_id,
'invoice_id' => $invoice_id,
'error' => mysqli_error($mysqli)
]);
}
continue;
if (empty($invoices)) {
cap_log('NO_INVOICES', ['user_id' => $userId]);
}
foreach ($invoices as $inv) {
$invoiceId = intval($inv['invoice_id']);
$homeId = intval($inv['home_id'] ?? 0);
$result = $svc->processPaymentSuccess($capture, $invoiceId, $userId, $homeId, $inv);
if ($result['success']) {
$invoicesPaid++;
cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid]);
}
// Create new order
$service_id = intval($inv['service_id']);
$home_name = mysqli_real_escape_string($mysqli, $inv['home_name']);
$ip = intval($inv['ip']);
$max_players = intval($inv['max_players']);
$qty = intval($inv['qty']);
$duration = mysqli_real_escape_string($mysqli, $inv['invoice_duration']);
$amount = floatval($inv['amount']);
$rcon_pw = mysqli_real_escape_string($mysqli, $inv['remote_control_password']);
$ftp_pw = mysqli_real_escape_string($mysqli, $inv['ftp_password']);
// Calculate end_date
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
// Insert order with status='Active' (server will be provisioned automatically)
$insertOrderSql = "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, paypal_data
) VALUES (
$user_id, $service_id, '$home_name', $ip, $max_players, $qty, '$duration',
$amount, '$rcon_pw', '$ftp_pw', 'Active', '$now', '$end_date',
'$esc_txid', '$now', '$esc_paypal_json'
)";
log_payment('INSERT_ORDER_SQL', substr($insertOrderSql, 0, 300));
if (mysqli_query($mysqli, $insertOrderSql)) {
$new_order_id = mysqli_insert_id($mysqli);
log_payment('ORDER_CREATED', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]);
$newOrderIds[] = $new_order_id;
// Link invoice to order
$linkSql = "UPDATE {$table_prefix}billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id";
mysqli_query($mysqli, $linkSql);
$ordersCreated++;
} else {
log_payment('INSERT_ORDER_FAILED', mysqli_error($mysqli));
// Handle legacy billing_orders linkage (backward compatibility)
$orderId = intval($inv['order_id'] ?? 0);
if ($orderId > 0) {
$order = $repo->getOrder($orderId);
if ($order) {
$dur = strtolower($inv['rate_type'] ?? $order['invoice_duration'] ?? 'month');
$durMap = [
'daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year',
'day' => '+1 day', 'month' => '+1 month', 'year' => '+1 year',
];
$fromTs = (strtotime($order['end_date'] ?? '') > time()) ? strtotime($order['end_date']) : time();
$newEnd = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month', $fromTs));
$repo->extendOrder($orderId, $newEnd, $txid, $now);
$ordersCreated++;
}
}
}
// Auto-provision new servers (orders without a home_id)
$autoProvision = ['provisioned_count' => 0, 'failed_count' => 0];
if (!empty($newOrderIds)) {
require_once __DIR__ . '/../includes/panel_bridge.php';
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db'])) {
$GLOBALS['db'] = $panelCtx['db'];
$GLOBALS['settings'] = $panelCtx['settings'];
require_once __DIR__ . '/../create_servers.php';
$autoProvision = billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]);
}
}
mysqli_close($mysqli);
$autoProvisionResult = ['provisioned_count' => 0, 'failed_count' => 0, 'orders' => []];
if (!empty($newOrderIds)) {
require_once __DIR__ . '/../includes/panel_bridge.php';
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db']) && $panelCtx['db'] instanceof OGPDatabase) {
$GLOBALS['db'] = $panelCtx['db'];
$GLOBALS['settings'] = $panelCtx['settings'];
require_once __DIR__ . '/../create_servers.php';
$autoProvisionResult = billing_invoke_provision([
'order_ids' => $newOrderIds,
'user_id' => $user_id,
'is_admin' => true
]);
log_payment('AUTO_PROVISION_COMPLETE', $autoProvisionResult);
} else {
log_payment('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed');
}
}
cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid]);
log_payment('PROCESSING_COMPLETE', [
'invoices_paid' => $affectedInvoices,
'orders_created' => $ordersCreated,
'orders_renewed' => $renewedOrders,
'txid' => $txid
]);
// Return success response
ob_clean();
echo json_encode([
'status' => 'COMPLETED',
'order_id' => $paypal_order_id,
'txid' => $txid,
'invoices_paid' => $affectedInvoices,
'status' => 'COMPLETED',
'txid' => $txid,
'invoices_paid' => $invoicesPaid,
'orders_created' => $ordersCreated,
'orders_renewed' => $renewedOrders,
'provisioned' => $autoProvisionResult['provisioned_count'] ?? 0
'provisioned' => $autoProvision['provisioned_count'] ?? 0,
'request_id' => $requestId,
]);

View file

@ -1,266 +1,84 @@
<?php
/**
* PayPal Create Order API Endpoint
* Enhanced with comprehensive logging for debugging
* Uses PayPalGateway class. Credentials come from config NOT hardcoded here.
*/
// Ensure all errors are logged, not displayed (to prevent JSON corruption)
ini_set('display_errors', '0');
error_reporting(E_ALL);
require_once(__DIR__ . '/../includes/config_loader.php');
// create_order for PayPal — adapted to run from _website/api
$sandbox = true; // flip to false for Live
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
require_once __DIR__ . '/../includes/config_loader.php';
require_once __DIR__ . '/../classes/PaymentGatewayInterface.php';
require_once __DIR__ . '/../classes/PayPalGateway.php';
require_once __DIR__ . '/../classes/GatewayFactory.php';
// Setup comprehensive logging
$logDir = __DIR__ . '/../logs';
// Logging
$logDir = __DIR__ . '/../logs';
@mkdir($logDir, 0755, true);
$logFile = $logDir . '/paypal_create_order.log';
$requestId = uniqid('req_', true); // Unique request identifier for tracking
$logFile = $logDir . '/paypal_create_order.log';
$requestId = uniqid('req_', true);
function create_order_log($label, $data) {
function co_log(string $label, $data): void {
global $logFile, $requestId;
$timestamp = date('Y-m-d H:i:s');
$entry = "[$timestamp] [$requestId] $label\n";
if (is_array($data) || is_object($data)) {
$entry .= print_r($data, true);
} else {
$entry .= (string)$data;
}
$entry = '[' . date('Y-m-d H:i:s') . "] [$requestId] $label\n";
$entry .= is_array($data) || is_object($data) ? print_r($data, true) : (string)$data;
$entry .= "\n" . str_repeat('-', 80) . "\n";
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
}
create_order_log('REQUEST_START', [
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN',
]);
header('Content-Type: application/json');
// Read and parse input
$rawInput = file_get_contents('php://input');
create_order_log('RAW_INPUT', substr($rawInput, 0, 2000)); // Log first 2000 chars
$in = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
create_order_log('JSON_DECODE_ERROR', [
'error' => json_last_error_msg(),
'raw_input_length' => strlen($rawInput),
'raw_input_preview' => substr($rawInput, 0, 500)
]);
$in = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE || !$in) {
http_response_code(400);
echo json_encode(['error' => 'invalid_json', 'message' => json_last_error_msg(), 'request_id' => $requestId]);
echo json_encode(['error' => 'invalid_json', 'request_id' => $requestId]);
exit;
}
if (!$in) {
$in = [];
co_log('REQUEST', ['amount' => $in['amount'] ?? null, 'invoice_id' => $in['invoice_id'] ?? null]);
// Resolve site base for return/cancel URLs
$siteBase = rtrim($GLOBALS['SITE_BASE_URL'] ?? '', '/');
$returnUrl = $in['return_url'] ?? '/payment_success.php';
$cancelUrl = $in['cancel_url'] ?? '/payment_cancel.php';
// Ensure absolute URLs
if (strpos($returnUrl, 'http') !== 0) {
$returnUrl = $siteBase . '/' . ltrim($returnUrl, '/');
}
if (strpos($cancelUrl, 'http') !== 0) {
$cancelUrl = $siteBase . '/' . ltrim($cancelUrl, '/');
}
$amount_in = $in['amount'] ?? '0.00';
$currency = $in['currency'] ?? 'USD';
$invoice_id = $in['invoice_id'] ?? null;
$custom_id = $in['custom_id'] ?? null;
$description = $in['description'] ?? 'Order';
$return_url = $in['return_url'] ?? null;
$cancel_url = $in['cancel_url'] ?? null;
$items = (isset($in['items']) && is_array($in['items'])) ? $in['items'] : null;
$line_invoices= (isset($in['line_invoices']) && is_array($in['line_invoices'])) ? $in['line_invoices'] : null;
create_order_log('PARSED_INPUT', [
'amount' => $amount_in,
'currency' => $currency,
'invoice_id' => $invoice_id,
'custom_id' => $custom_id,
'items_count' => $items ? count((array)$items) : 0,
'line_invoices_count' => $line_invoices ? count((array)$line_invoices) : 0
]);
$amount_value = number_format((float)$amount_in, 2, '.', '');
if ($items) {
$sum = 0.00;
foreach ((array)$items as $it) {
$qty = isset($it['quantity']) ? (int)$it['quantity'] : 1;
$val = isset($it['unit_amount']['value']) ? (float)$it['unit_amount']['value'] : 0.00;
$sum += $qty * $val;
}
$amount_value = number_format($sum, 2, '.', '');
create_order_log('AMOUNT_CALCULATED', [
'original_amount' => $amount_in,
'calculated_from_items' => $amount_value,
'items_sum' => $sum
]);
}
$api = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
create_order_log('PAYPAL_API_CONFIG', [
'sandbox_mode' => $sandbox,
'api_base' => $api,
'has_client_id' => !empty($client_id),
'has_client_secret' => !empty($client_secret)
]);
// Step 1: Get OAuth token
create_order_log('OAUTH_REQUEST_START', ['endpoint' => "$api/v1/oauth2/token"]);
$ch = curl_init("$api/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 => $client_id . ':' . $client_secret,
]);
$tok = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_errno = curl_errno($ch);
$curl_error = curl_error($ch);
curl_close($ch);
create_order_log('OAUTH_RESPONSE', [
'http_code' => $http,
'curl_errno' => $curl_errno,
'curl_error' => $curl_error,
'response_length' => strlen($tok),
'response_preview' => substr($tok, 0, 200)
]);
if ($curl_errno !== 0) {
create_order_log('OAUTH_CURL_ERROR', ['errno' => $curl_errno, 'error' => $curl_error]);
http_response_code(502);
echo json_encode(['error' => 'oauth_curl_fail', 'details' => $curl_error, 'request_id' => $requestId]);
exit;
}
if ($http !== 200) {
create_order_log('OAUTH_HTTP_ERROR', ['http_code' => $http, 'response' => $tok]);
http_response_code(500);
echo json_encode(['error' => 'oauth_fail', 'http_code' => $http, 'request_id' => $requestId]);
exit;
}
$access = json_decode($tok, true)['access_token'] ?? null;
if (!$access) {
create_order_log('OAUTH_NO_TOKEN', ['response' => $tok]);
http_response_code(500);
echo json_encode(['error' => 'oauth_no_token', 'request_id' => $requestId]);
exit;
}
create_order_log('OAUTH_SUCCESS', ['token_length' => strlen($access)]);
// Update site base URL to exclude 'modules/billing'
$siteBaseUrl = 'http://gameservers.world';
create_order_log('URL_PROCESSING_BEFORE', [
'return_url' => $return_url,
'cancel_url' => $cancel_url,
'site_base' => $siteBaseUrl
]);
// 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, '/');
}
create_order_log('URL_PROCESSING_AFTER', [
'return_url' => $return_url,
'cancel_url' => $cancel_url
]);
$purchaseUnit = [
'amount' => [ 'currency_code' => $currency, 'value' => $amount_value ],
'description' => $description,
'invoice_id' => $invoice_id,
'custom_id' => $custom_id
];
if ($items) {
$purchaseUnit['items'] = $items;
$purchaseUnit['amount']['breakdown'] = [ 'item_total' => ['currency_code'=>$currency,'value'=>$amount_value] ];
}
$body = [
'intent' => 'CAPTURE',
'purchase_units' => [ $purchaseUnit ],
'application_context' => [ 'return_url'=>$return_url, 'cancel_url'=>$cancel_url, 'user_action'=>'PAY_NOW' ]
// Build gateway params
$params = [
'amount' => $in['amount'] ?? '0.00',
'currency' => $in['currency'] ?? 'USD',
'invoice_id' => $in['invoice_id'] ?? null,
'custom_id' => $in['custom_id'] ?? $in['invoice_id'] ?? null,
'description' => $in['description'] ?? 'Game Server Order',
'return_url' => $returnUrl,
'cancel_url' => $cancelUrl,
'items' => $in['items'] ?? null,
];
create_order_log('PAYPAL_ORDER_PAYLOAD', $body);
// Step 2: Create PayPal order
create_order_log('CREATE_ORDER_REQUEST_START', ['endpoint' => "$api/v2/checkout/orders"]);
$ch = curl_init("$api/v2/checkout/orders");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $access ],
]);
$res = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_errno = curl_errno($ch);
$curl_error = curl_error($ch);
curl_close($ch);
create_order_log('CREATE_ORDER_RESPONSE', [
'http_code' => $http,
'curl_errno' => $curl_errno,
'curl_error' => $curl_error,
'response_length' => strlen($res),
'response' => substr($res, 0, 1000) // First 1000 chars of response
]);
if ($curl_errno !== 0) {
create_order_log('CREATE_ORDER_CURL_ERROR', ['errno' => $curl_errno, 'error' => $curl_error]);
http_response_code(502);
echo json_encode(['error' => 'create_order_curl_fail', 'details' => $curl_error, 'request_id' => $requestId]);
try {
$gateway = GatewayFactory::make('paypal');
$result = $gateway->createPayment($params);
} catch (Exception $e) {
co_log('EXCEPTION', $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'gateway_error', 'message' => $e->getMessage(), 'request_id' => $requestId]);
exit;
}
if ($http !== 201) {
create_order_log('CREATE_ORDER_HTTP_ERROR', [
'http_code' => $http,
'response' => $res,
'payload_sent' => $body
]);
// Try to parse PayPal error response
$errorData = json_decode($res, true);
http_response_code($http);
echo json_encode([
'error' => 'create_order_failed',
'http_code' => $http,
'paypal_error' => $errorData,
'request_id' => $requestId
]);
if (!$result['success']) {
co_log('CREATE_FAILED', $result);
http_response_code(500);
echo json_encode(['error' => $result['error'] ?? 'create_failed', 'request_id' => $requestId]);
exit;
}
// Success - parse and validate response
$orderData = json_decode($res, true);
if (json_last_error() !== JSON_ERROR_NONE) {
create_order_log('CREATE_ORDER_INVALID_JSON', [
'json_error' => json_last_error_msg(),
'response' => $res
]);
http_response_code(502);
echo json_encode(['error' => 'invalid_paypal_response', 'request_id' => $requestId]);
exit;
}
create_order_log('CREATE_ORDER_SUCCESS', [
'order_id' => $orderData['id'] ?? 'UNKNOWN',
'status' => $orderData['status'] ?? 'UNKNOWN'
]);
echo $res;
?>
co_log('CREATE_SUCCESS', ['provider_order_id' => $result['provider_order_id']]);
echo json_encode($result['raw_response']);