array_keys($_SESSION)]); ob_clean(); echo json_encode(['error' => 'no_user_session', 'request_id' => $requestId]); exit; } // 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) { 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'); $prefix = $table_prefix ?? 'gsp_'; $repo = new BillingRepository($mysqli, $prefix); $svc = new BillingService($repo); // 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' => 'gateway_init_failed', 'request_id' => $requestId]); mysqli_close($mysqli); exit; } $capture = $gateway->handleCallback(['order_id' => $paypalOrderId]); cap_log('CAPTURE_RESULT', ['success' => $capture['success'], 'txid' => $capture['transaction_id'] ?? null]); 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; $newOrderIds = []; $now = date('Y-m-d H:i:s'); 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']) { cap_log('INVOICE_PAY_FAILED', ['invoice_id' => $invoiceId, 'error' => $result['error'] ?? '']); continue; } $invoicesPaid++; cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid]); // Resolve (or create) the billing_orders row for this invoice so the provisioner can run. // billing_orders.status='Active' is what create_servers.php queries. $orderId = intval($inv['order_id'] ?? 0); $durMap = [ 'daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year', 'day' => '+1 day', 'month' => '+1 month', 'year' => '+1 year', ]; $dur = strtolower($inv['rate_type'] ?? $inv['invoice_duration'] ?? 'month'); $newEnd = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month')); if ($orderId > 0) { // Existing order linked to this invoice — extend it and mark Active. $order = $repo->getOrder($orderId); if ($order) { $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++; // Queue for provisioning only if not yet provisioned (home_id still '0' / empty). $currentHomeId = (string)($order['home_id'] ?? '0'); if ($currentHomeId === '' || $currentHomeId === '0') { $newOrderIds[] = $orderId; cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId]); } } } else { // No billing_orders row yet — create one now so the provisioner can run. $newOrderId = $repo->createOrder([ 'user_id' => intval($inv['user_id']), 'service_id' => intval($inv['service_id']), 'home_name' => $inv['home_name'] ?? '', 'ip' => (string)($inv['ip'] ?? '0'), 'qty' => intval($inv['qty'] ?? 1), 'invoice_duration' => $inv['invoice_duration'] ?? 'month', 'max_players' => intval($inv['max_players'] ?? 0), 'price' => (float)($inv['amount'] ?? $inv['total_due'] ?? 0), 'remote_control_password' => $inv['remote_control_password'] ?? '', 'ftp_password' => $inv['ftp_password'] ?? '', 'status' => 'Active', 'end_date' => $newEnd, 'payment_txid' => $txid, 'paid_ts' => $now, 'coupon_id' => intval($inv['coupon_id'] ?? 0), ]); if ($newOrderId > 0) { // Link invoice → order so retried captures are idempotent. $repo->updateInvoiceOrderId($invoiceId, $newOrderId); $newOrderIds[] = $newOrderId; $ordersCreated++; cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]); } else { cap_log('ORDER_CREATE_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]); } } } // 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]); if (($autoProvision['failed_count'] ?? 0) > 0) { cap_log('AUTO_PROVISION_PARTIAL_FAILURE', $autoProvision); } } else { cap_log('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed — orders require manual provisioning: ' . implode(',', $newOrderIds)); } } mysqli_close($mysqli); cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid]); ob_clean(); echo json_encode([ 'status' => 'COMPLETED', 'txid' => $txid, 'invoices_paid' => $invoicesPaid, 'orders_created' => $ordersCreated, 'provisioned' => $autoProvision['provisioned_count'] ?? 0, 'request_id' => $requestId, ]);