connect_error) { throw new Exception('Database connection failed: ' . $conn->connect_error); } $conn->set_charset($dbConfig['charset']); return $conn; } // Handle AJAX requests if (isset($_POST['action'])) writeLog("POST action: ".$_POST['action']); if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { header('Content-Type: application/json'); writeLog(__DIR__ . '/config.php'); $config = require __DIR__ . '/config.php'; require __DIR__ . '/lib/PayPalClient.php'; $dataDir = __DIR__ . '/data'; writeLog("dataDir: $dataDir"); if (!is_dir($dataDir)) { mkdir($dataDir, 0755, true); } try { writeLog("new PayPalClient"); $client = new PayPalClient($config); writeLog("new PayPalClient - complete"); // ACTION: Create connection and redirect to PayPal writeLog($_POST['action']); if ($_POST['action'] === 'connect') { $schoolName = trim($_POST['school_name'] ?? ''); if ($schoolName === '') { echo json_encode(['success' => false, 'error' => 'School name required']); exit(); } writeLog("schoolName: $schoolName"); $trackingId = 'school_' . substr(md5($schoolName . time()), 0, 10); writeLog("trackingId: $trackingId"); // base_url already resolves to the directory this script is served from // (auto-detected from SCRIPT_NAME). Don't append a hardcoded subdir like // '/sm' or the return URL points to a path that doesn't exist (404). $baseUrl = rtrim($config['base_url'], '/'); writeLog("baseUrl: $baseUrl"); $returnUrl = $baseUrl . '/onboard.php?callback=1&school=' . urlencode($schoolName) . '&track=' . urlencode($trackingId); writeLog("returnUrl: $returnUrl"); $payload = [ 'tracking_id' => $trackingId, 'operations' => [[ 'operation' => 'API_INTEGRATION', 'api_integration_preference' => [ 'rest_api_integration' => [ 'integration_method' => 'PAYPAL', 'integration_type' => 'THIRD_PARTY', 'third_party_details' => ['features' => ['PAYMENT', 'REFUND', 'VAULT', 'BILLING_AGREEMENT']] ] ] ]], 'products' => ['PPCP', 'PAYMENT_METHODS', 'ADVANCED_VAULTING'], 'capabilities' => ['APPLE_PAY', 'GOOGLE_PAY', 'PAYPAL_WALLET_VAULTING_ADVANCED'], 'legal_consents' => [['type' => 'SHARE_DATA_CONSENT', 'granted' => true]], 'partner_config_override' => [ 'return_url' => $returnUrl, 'return_url_description' => 'Return to School Setup', 'show_add_credit_card' => false ] ]; writeLog("api call"); $resp = $client->api('POST', '/v2/customer/partner-referrals', $payload); writeLog('Partner referral status: ' . $resp['status']); writeLog('Partner referral body: ' . json_encode($resp['body'])); writeLog("api call - complete"); writeLog("resp status: ".$resp['status']); if ($resp['status'] < 200 || $resp['status'] >= 300) { echo json_encode(['success' => false, 'error' => 'Partner referral failed']); exit(); } $body = $resp['body']; writeLog("body partner_referral_id: ".$body['partner_referral_id']); $partnerReferralId = $body['partner_referral_id'] ?? $body['referral_id'] ?? null; $actionUrl = null; foreach ($body['links'] ?? [] as $link) { if (($link['rel'] ?? '') === 'action_url') { $actionUrl = $link['href']; break; } } writeLog("actionUrl: $actionUrl"); if (!$actionUrl) { writeLog("No onboarding URL from PayPal"); echo json_encode(['success' => false, 'error' => 'No onboarding URL from PayPal']); exit(); } $pendingFile = $dataDir . '/pending_onboarding.json'; writeLog("pendingFile: $pendingFile"); $pending = file_exists($pendingFile) ? json_decode(file_get_contents($pendingFile), true) ?: [] : []; $pending[$trackingId] = [ 'school' => $schoolName, 'created_at' => time(), 'partner_referral_id' => $partnerReferralId, 'status' => 'pending' ]; file_put_contents($pendingFile, json_encode($pending, JSON_PRETTY_PRINT)); writeLog("json_encode call"); echo json_encode(['success' => true, 'redirect' => $actionUrl]); exit(); writeLog("still going..."); } // ACTION: Sync pending merchants if ($_POST['action'] === 'sync') { $pendingFile = $dataDir . '/pending_onboarding.json'; if (!file_exists($pendingFile)) { echo json_encode(['success' => false, 'error' => 'No pending schools']); exit(); } $pending = json_decode(file_get_contents($pendingFile), true) ?: []; $synced = 0; $db = getDbConnection(); foreach ($pending as $trackingId => $data) { if (!in_array($data['status'] ?? '', ['pending', 'waiting_for_paypal', 'incomplete_onboarding'])) { continue; } $school = $data['school'] ?? 'Unknown'; $partnerReferralId = $data['partner_referral_id'] ?? null; $merchantId = null; // Try 3 approaches $path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?tracking_id=' . urlencode($trackingId); $resp = $client->api('GET', $path); if ($resp['status'] >= 200 && $resp['status'] < 300) { $foundId = $resp['body']['merchant_id'] ?? null; if ($foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; } if (!$merchantId) { foreach ($resp['body']['merchant_integrations'] ?? [] as $integration) { $foundId = $integration['merchant_id'] ?? null; if ($foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break; } } } } if (!$merchantId && $partnerReferralId) { $path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?onboarded=true'; $resp = $client->api('GET', $path); if ($resp['status'] >= 200 && $resp['status'] < 300) { foreach ($resp['body']['merchant_integrations'] ?? [] as $integration) { $refId = $integration['partner_referral_id'] ?? $integration['referral_id'] ?? null; $foundId = $integration['merchant_id'] ?? null; if ($refId && strcasecmp($refId, $partnerReferralId) === 0 && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break; } } } } if (!$merchantId) { $path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?page_size=200'; $resp = $client->api('GET', $path); if ($resp['status'] >= 200 && $resp['status'] < 300) { foreach ($resp['body']['merchant_integrations'] ?? [] as $integration) { $tid = $integration['tracking_id'] ?? null; $refId = $integration['partner_referral_id'] ?? $integration['referral_id'] ?? null; $foundId = $integration['merchant_id'] ?? null; if ((($tid && strcasecmp($tid, $trackingId) === 0) || ($partnerReferralId && $refId && strcasecmp($refId, $partnerReferralId) === 0)) && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break; } } } } if ($merchantId) { // Save to database. Match by school_name — $school_num is not // defined in this sync scope; the callback path keys on school_name // too, so this keeps both write paths consistent. $stmt = $db->prepare("UPDATE schools SET paypal_merchant_id = ?, paypal_connected_at = NOW(), paypal_tracking_id = ? WHERE school_name = ?"); $stmt->bind_param('sss', $merchantId, $trackingId, $school); $stmt->execute(); $stmt->close(); // Check seller status $sellerStatus = $client->getSellerStatus($config['partner_merchant_id'], $merchantId); writeLog('Seller status [sync] ' . $school . ': paypal_ready=' . ($sellerStatus['paypal_ready'] ? 'true' : 'false') . ' debug_id=' . ($sellerStatus['debug_id'] ?? 'n/a')); if (!empty($sellerStatus['issues'])) { writeLog('Seller status issues [sync] ' . $school . ': ' . implode(' | ', $sellerStatus['issues'])); } // Also keep in JSON as backup $merchantsFile = $dataDir . '/school_merchants.json'; $merchants = file_exists($merchantsFile) ? json_decode(file_get_contents($merchantsFile), true) ?: [] : []; $merchants[$school] = [ 'merchant_id' => $merchantId, 'tracking_id' => $trackingId, 'connected_at' => time(), 'paypal_ready' => $sellerStatus['paypal_ready'], 'email_confirmed' => $sellerStatus['email_confirmed'], 'payments_receivable' => $sellerStatus['payments_receivable'], 'oauth_ok' => $sellerStatus['oauth_ok'], 'seller_email' => $sellerStatus['seller_email'], 'status_issues' => $sellerStatus['issues'], 'status_checked_at' => time(), ]; file_put_contents($merchantsFile, json_encode($merchants, JSON_PRETTY_PRINT)); $pending[$trackingId]['merchant_id'] = $merchantId; $pending[$trackingId]['status'] = 'connected'; $pending[$trackingId]['connected_at'] = time(); $pending[$trackingId]['paypal_ready'] = $sellerStatus['paypal_ready']; $synced++; } } $db->close(); file_put_contents($pendingFile, json_encode($pending, JSON_PRETTY_PRINT)); echo json_encode([ 'success' => true, 'synced' => $synced, 'message' => $synced > 0 ? "Connected $synced school(s)" : 'No merchant IDs found yet' ]); exit(); } // ACTION: Re-check seller status for a specific school if ($_POST['action'] === 'check_status') { $schoolName = trim($_POST['school_name'] ?? ''); if ($schoolName === '') { echo json_encode(['success' => false, 'error' => 'School name required']); exit(); } $merchantsFile = $dataDir . '/school_merchants.json'; if (!file_exists($merchantsFile)) { echo json_encode(['success' => false, 'error' => 'No merchant data found']); exit(); } $merchants = json_decode(file_get_contents($merchantsFile), true) ?: []; $merchantId = $merchants[$schoolName]['merchant_id'] ?? null; if (!$merchantId) { echo json_encode(['success' => false, 'error' => 'School not found or not yet connected']); exit(); } $sellerStatus = $client->getSellerStatus($config['partner_merchant_id'], $merchantId); writeLog('Seller status [manual] ' . $schoolName . ': paypal_ready=' . ($sellerStatus['paypal_ready'] ? 'true' : 'false') . ' debug_id=' . ($sellerStatus['debug_id'] ?? 'n/a')); if (!empty($sellerStatus['issues'])) { writeLog('Seller status issues [manual] ' . $schoolName . ': ' . implode(' | ', $sellerStatus['issues'])); } $merchants[$schoolName]['paypal_ready'] = $sellerStatus['paypal_ready']; $merchants[$schoolName]['email_confirmed'] = $sellerStatus['email_confirmed']; $merchants[$schoolName]['payments_receivable'] = $sellerStatus['payments_receivable']; $merchants[$schoolName]['oauth_ok'] = $sellerStatus['oauth_ok']; $merchants[$schoolName]['seller_email'] = $sellerStatus['seller_email']; $merchants[$schoolName]['status_issues'] = $sellerStatus['issues']; $merchants[$schoolName]['status_checked_at'] = time(); file_put_contents($merchantsFile, json_encode($merchants, JSON_PRETTY_PRINT)); echo json_encode([ 'success' => true, 'paypal_ready' => $sellerStatus['paypal_ready'], 'email_confirmed' => $sellerStatus['email_confirmed'], 'payments_receivable' => $sellerStatus['payments_receivable'], 'oauth_ok' => $sellerStatus['oauth_ok'], 'seller_email' => $sellerStatus['seller_email'], 'issues' => $sellerStatus['issues'], ]); exit(); } } catch (Exception $e) { echo json_encode(['success' => false, 'error' => $e->getMessage()]); exit(); } } // Handle PayPal callback if (isset($_GET['callback']) && $_GET['callback'] == '1') { writeLog('PayPal callback received: ' . ($_SERVER['QUERY_STRING'] ?? '')); $config = require __DIR__ . '/config.php'; require __DIR__ . '/lib/PayPalClient.php'; $school = $_GET['school'] ?? ''; $trackingId = $_GET['track'] ?? ''; // NOTE: PayPal's "merchantId" return param is OUR tracking_id, not the PayPal // merchant ID. The real PayPal merchant ID is "merchantIdInPayPal". $merchantIdFromUrl = $_GET['merchantIdInPayPal'] ?? $_GET['merchant_id'] ?? null; writeLog("callback parsed: school=$school track=$trackingId merchantIdInPayPal=" . ($merchantIdFromUrl ?? 'null')); $dataDir = __DIR__ . '/data'; if (!is_dir($dataDir)) { @mkdir($dataDir, 0755, true); writeLog("created dataDir: $dataDir"); } if (!is_writable($dataDir)) { writeLog("WARNING: dataDir not writable: $dataDir"); } if ($school && $trackingId) { try { $client = new PayPalClient($config); $merchantId = $merchantIdFromUrl; $pendingFile = $dataDir . '/pending_onboarding.json'; $partnerReferralId = null; if (file_exists($pendingFile)) { $pending = json_decode(file_get_contents($pendingFile), true) ?: []; $partnerReferralId = $pending[$trackingId]['partner_referral_id'] ?? null; } if (!$merchantId) { for ($i = 0; $i < 15; $i++) { $path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?tracking_id=' . urlencode($trackingId); $resp = $client->api('GET', $path); if ($resp['status'] >= 200 && $resp['status'] < 300) { $foundId = $resp['body']['merchant_id'] ?? null; if ($foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break; } foreach ($resp['body']['merchant_integrations'] ?? [] as $integration) { $foundId = $integration['merchant_id'] ?? null; if ($foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break 2; } } } if (!$merchantId && $partnerReferralId) { $path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?onboarded=true'; $resp = $client->api('GET', $path); if ($resp['status'] >= 200 && $resp['status'] < 300) { foreach ($resp['body']['merchant_integrations'] ?? [] as $integration) { $refId = $integration['partner_referral_id'] ?? $integration['referral_id'] ?? null; $foundId = $integration['merchant_id'] ?? null; if ($refId && strcasecmp($refId, $partnerReferralId) === 0 && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break 2; } } } } if (!$merchantId) { $path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?page_size=200'; $resp = $client->api('GET', $path); if ($resp['status'] >= 200 && $resp['status'] < 300) { foreach ($resp['body']['merchant_integrations'] ?? [] as $integration) { $tid = $integration['tracking_id'] ?? null; $refId = $integration['partner_referral_id'] ?? $integration['referral_id'] ?? null; $foundId = $integration['merchant_id'] ?? null; if ((($tid && strcasecmp($tid, $trackingId) === 0) || ($partnerReferralId && $refId && strcasecmp($refId, $partnerReferralId) === 0)) && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) { $merchantId = $foundId; break 2; } } } } if ($i < 14) sleep(1); } } if ($merchantId) { // Persist the merchant ID immediately. We already have it from the // callback URL, so the connection MUST be recorded (and the dashboard // updated) even if the follow-up seller-status check fails or PayPal // hasn't finished propagating the new integration yet. $merchantsFile = $dataDir . '/school_merchants.json'; $merchants = file_exists($merchantsFile) ? json_decode(file_get_contents($merchantsFile), true) ?: [] : []; $merchants[$school] = array_merge($merchants[$school] ?? [], [ 'merchant_id' => $merchantId, 'tracking_id' => $trackingId, 'connected_at' => time(), ]); $written = file_put_contents($merchantsFile, json_encode($merchants, JSON_PRETTY_PRINT)); writeLog('Merchant saved [callback] ' . $school . ' => ' . $merchantId . ' (' . ($written === false ? 'WRITE FAILED to ' . $merchantsFile : $written . ' bytes to ' . $merchantsFile) . ')'); // If we can't persist, do NOT show a fake success page — surface the // real problem (almost always: the data/ directory is not writable // by the web-server user) so it can be fixed. if ($written === false) { $writable = is_writable($dataDir) ? 'writable' : 'NOT writable'; header('Location: onboard.php?status=error&msg=' . urlencode( 'Could not save merchant data. data/ directory (' . $dataDir . ') is ' . $writable . '. Fix filesystem permissions so PHP can write to it.')); exit(); } if (file_exists($pendingFile)) { $pending = json_decode(file_get_contents($pendingFile), true) ?: []; if (isset($pending[$trackingId])) { $pending[$trackingId]['merchant_id'] = $merchantId; $pending[$trackingId]['status'] = 'connected'; $pending[$trackingId]['connected_at'] = time(); file_put_contents($pendingFile, json_encode($pending, JSON_PRETTY_PRINT)); } } // Save to database (non-fatal — a DB failure must not lose the JSON record) try { $db = getDbConnection(); $stmt = $db->prepare("UPDATE schools SET paypal_merchant_id = ?, paypal_connected_at = NOW(), paypal_tracking_id = ? WHERE school_name = ?"); $stmt->bind_param('sss', $merchantId, $trackingId, $school); $stmt->execute(); writeLog('DB update [callback] ' . $school . ': affected_rows=' . $stmt->affected_rows); $stmt->close(); $db->close(); } catch (Exception $e) { writeLog('DB Error [callback]: ' . $e->getMessage()); } // Enrich with seller status — guarded so a status-check failure // never discards the connection we just saved above. try { $sellerStatus = $client->getSellerStatus($config['partner_merchant_id'], $merchantId); writeLog('Seller status [callback] ' . $school . ': paypal_ready=' . ($sellerStatus['paypal_ready'] ? 'true' : 'false') . ' debug_id=' . ($sellerStatus['debug_id'] ?? 'n/a')); if (!empty($sellerStatus['issues'])) { writeLog('Seller status issues [callback] ' . $school . ': ' . implode(' | ', $sellerStatus['issues'])); } $merchants[$school] = array_merge($merchants[$school], [ 'paypal_ready' => $sellerStatus['paypal_ready'], 'email_confirmed' => $sellerStatus['email_confirmed'], 'payments_receivable' => $sellerStatus['payments_receivable'], 'oauth_ok' => $sellerStatus['oauth_ok'], 'seller_email' => $sellerStatus['seller_email'], 'status_issues' => $sellerStatus['issues'], 'status_checked_at' => time(), ]); file_put_contents($merchantsFile, json_encode($merchants, JSON_PRETTY_PRINT)); if (file_exists($pendingFile)) { $pending = json_decode(file_get_contents($pendingFile), true) ?: []; if (isset($pending[$trackingId])) { $pending[$trackingId]['paypal_ready'] = $sellerStatus['paypal_ready']; file_put_contents($pendingFile, json_encode($pending, JSON_PRETTY_PRINT)); } } } catch (Exception $e) { writeLog('Seller status check failed [callback] ' . $school . ': ' . $e->getMessage()); } header('Location: onboard.php?success=1&school=' . urlencode($school)); exit(); } else { if (file_exists($pendingFile)) { $pending = json_decode(file_get_contents($pendingFile), true) ?: []; if (isset($pending[$trackingId])) { $pending[$trackingId]['status'] = 'incomplete_onboarding'; $pending[$trackingId]['last_callback'] = time(); file_put_contents($pendingFile, json_encode($pending, JSON_PRETTY_PRINT)); } } header('Location: onboard.php?status=error&msg=' . urlencode('Please complete PayPal onboarding or sync later')); exit(); } } catch (Exception $e) { header('Location: onboard.php?status=error&msg=' . urlencode($e->getMessage())); exit(); } } } ?>
You have successfully connected your PayPal account.
Your merchant ID has been saved to the database.
Connect your school's PayPal account