800 lines
36 KiB
PHP
800 lines
36 KiB
PHP
<?php
|
|
ini_set('display_errors', 0);
|
|
|
|
$ValidAccess = 2010;
|
|
|
|
// require_once("../include/globals.php");
|
|
$cdir = "/m/data1/app.hotlunches.net";
|
|
$AppDir = $cdir;
|
|
|
|
ob_start();
|
|
|
|
require_once("../include/logging-functions.php");
|
|
startLog("includes");
|
|
|
|
require_once("../include/date-time.php");
|
|
require_once("../include/config.php");
|
|
require_once("../include/school-functions.php");
|
|
require_once("../include/database.php");
|
|
require_once("../include/session.php");
|
|
|
|
|
|
$sessionName = 'hotlunch';
|
|
$sessionTimeout = 60 * 30;
|
|
$cookiePath = '/';
|
|
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
session_name($sessionName);
|
|
session_start();
|
|
}
|
|
|
|
// ── Diagnostic endpoint ──────────────────────────────────────────────────
|
|
// Visit onboard.php?debug=1 in a browser to see exactly where data is stored,
|
|
// whether it is writable, and what is currently saved. No login required.
|
|
if (isset($_GET['debug'])) {
|
|
header('Content-Type: text/plain');
|
|
$dataDir = __DIR__ . '/data';
|
|
$merchantsFile = $dataDir . '/school_merchants.json';
|
|
$pendingFile = $dataDir . '/pending_onboarding.json';
|
|
|
|
echo "=== onboard.php diagnostics ===\n";
|
|
echo "__DIR__ : " . __DIR__ . "\n";
|
|
echo "realpath(__DIR__) : " . realpath(__DIR__) . "\n";
|
|
echo "SCRIPT_FILENAME : " . ($_SERVER['SCRIPT_FILENAME'] ?? 'n/a') . "\n";
|
|
echo "dataDir : $dataDir\n";
|
|
echo "dataDir realpath : " . (realpath($dataDir) ?: '(does not exist)') . "\n";
|
|
echo "dataDir is_dir : " . (is_dir($dataDir) ? 'yes' : 'NO') . "\n";
|
|
echo "dataDir is_writable : " . (is_writable($dataDir) ? 'yes' : 'NO') . "\n";
|
|
|
|
$probe = $dataDir . '/_write_probe.txt';
|
|
$ok = @file_put_contents($probe, 'probe ' . date('c'));
|
|
echo "test write : " . ($ok === false ? 'FAILED' : "ok ($ok bytes)") . "\n";
|
|
if ($ok !== false) @unlink($probe);
|
|
|
|
echo "\n--- school_merchants.json (" . (file_exists($merchantsFile) ? filesize($merchantsFile) . ' bytes' : 'MISSING') . ") ---\n";
|
|
echo file_exists($merchantsFile) ? file_get_contents($merchantsFile) : '(none)';
|
|
echo "\n\n--- pending_onboarding.json (" . (file_exists($pendingFile) ? filesize($pendingFile) . ' bytes' : 'MISSING') . ") ---\n";
|
|
echo file_exists($pendingFile) ? file_get_contents($pendingFile) : '(none)';
|
|
exit();
|
|
}
|
|
|
|
// PayPal redirects the merchant's browser back to this URL when onboarding
|
|
// finishes. That is an external, top-level return from paypal.com — the app
|
|
// session is often no longer active by then, so the auth gate below must NOT
|
|
// apply to it or the callback never runs and the merchant is never saved.
|
|
// The callback is authenticated instead by the tracking_id we generated.
|
|
$isPayPalCallback = isset($_GET['callback']) && $_GET['callback'] == '1';
|
|
|
|
if (!$isPayPalCallback) {
|
|
if (SessionActive() != 1 || ! UserLoggedIn()) {
|
|
header("Location: {$FormTarget}");
|
|
exit();
|
|
}
|
|
|
|
|
|
$AllowedUserTypes = array('SuperUser','System Administrator');
|
|
if (checkPermission($AllowedUserTypes) != 1) {
|
|
$error_message = getLanguageText('ERRORMSG-ACCESS-DENIED');
|
|
writeLog($error_message);
|
|
$naction = '';
|
|
header("Location: {$FormTarget}?action=$action&naction=$naction$headerArgs&form_message=" . urlencode($form_message) . "&error_message=" . urlencode($error_message));
|
|
exit();
|
|
}
|
|
}
|
|
|
|
$buffer = ob_get_contents();
|
|
if (strlen($buffer)) writeLog("buffer: $buffer");
|
|
ob_end_clean();
|
|
writeLog("Header complete");
|
|
startLog("pp-integration");
|
|
|
|
|
|
// Database connection helper
|
|
function getDbConnection() {
|
|
writeLog("getDbConnection: ".__DIR__ . '/config.php');
|
|
$dbConfig = require __DIR__ . '/db_config.php';
|
|
$conn = new mysqli(
|
|
$dbConfig['host'],
|
|
$dbConfig['username'],
|
|
$dbConfig['password'],
|
|
$dbConfig['database']
|
|
);
|
|
|
|
if ($conn->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();
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>School Merchant ID Setup</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
padding: 40px;
|
|
max-width: 500px;
|
|
width: 100%;
|
|
}
|
|
h1 { color: #333; margin-bottom: 10px; font-size: 28px; }
|
|
.subtitle { color: #666; margin-bottom: 30px; font-size: 14px; }
|
|
.form-group { margin-bottom: 20px; }
|
|
label { display: block; margin-bottom: 8px; color: #555; font-weight: 500; }
|
|
input[type="text"] {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 6px;
|
|
font-size: 16px;
|
|
}
|
|
input[type="text"]:focus { outline: none; border-color: #667eea; }
|
|
.btn {
|
|
width: 100%;
|
|
padding: 14px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
.btn:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); }
|
|
.btn:disabled { background: #ccc; cursor: not-allowed; }
|
|
.btn.success { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); }
|
|
.alert {
|
|
padding: 12px;
|
|
border-radius: 6px;
|
|
margin-bottom: 20px;
|
|
font-size: 14px;
|
|
}
|
|
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
|
.alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
|
.success-page {
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
.success-icon {
|
|
font-size: 64px;
|
|
color: #28a745;
|
|
margin-bottom: 20px;
|
|
}
|
|
.success-page h2 {
|
|
color: #28a745;
|
|
margin-bottom: 15px;
|
|
font-size: 24px;
|
|
}
|
|
.success-page p {
|
|
color: #666;
|
|
margin-bottom: 10px;
|
|
line-height: 1.6;
|
|
}
|
|
.school-name {
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
font-size: 18px;
|
|
}
|
|
.sync-section {
|
|
margin-top: 30px;
|
|
padding-top: 30px;
|
|
border-top: 2px solid #f0f0f0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<?php if (isset($_GET['success']) && $_GET['success'] == '1'): ?>
|
|
<!-- Success Page -->
|
|
<div class="success-page">
|
|
<div class="success-icon">✓</div>
|
|
<h2>Welcome!</h2>
|
|
<p>You have successfully connected your PayPal account.</p>
|
|
<p class="school-name"><?php echo htmlspecialchars($_GET['school'] ?? 'Your school'); ?></p>
|
|
<p style="margin-top: 20px; font-size: 13px;">Your merchant ID has been saved to the database.</p>
|
|
<button onclick="window.location.href='onboard.php'" class="btn success" style="margin-top: 30px;">
|
|
← Back to Home
|
|
</button>
|
|
</div>
|
|
<?php else: ?>
|
|
<!-- Main Form -->
|
|
<h1>🏫 School Merchant Setup</h1>
|
|
<p class="subtitle">Connect your school's PayPal account</p>
|
|
|
|
<?php
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if (isset($_GET['status'])) {
|
|
if ($_GET['status'] === 'error') {
|
|
$message = $_GET['msg'] ?? 'An error occurred. Please try again.';
|
|
$messageType = 'error';
|
|
}
|
|
}
|
|
|
|
if ($message): ?>
|
|
<div class="alert alert-<?php echo htmlspecialchars($messageType); ?>">
|
|
<?php echo htmlspecialchars($message); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form id="connectForm">
|
|
<div class="form-group">
|
|
<label for="school_name"><?php displayLanguageText('TEXT-SCHOOL-NAME'); ?></label>
|
|
<input type="text" id="school_name" name="school_name" placeholder="Enter school name" value="<?php echo($school_record['school_name']) ?>" required>
|
|
</div>
|
|
<button type="submit" class="btn" id="connectBtn">Connect PayPal Account</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<script>
|
|
// Connect form handler (the form is absent on the success page)
|
|
var connectForm = document.getElementById('connectForm');
|
|
if (connectForm) connectForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const btn = document.getElementById('connectBtn');
|
|
const schoolName = document.getElementById('school_name').value.trim();
|
|
|
|
if (!schoolName) {
|
|
alert('Please enter a school name');
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Connecting...';
|
|
|
|
fetch('onboard.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: 'action=connect&school_name=' + encodeURIComponent(schoolName)
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success && data.redirect) {
|
|
window.location.href = data.redirect;
|
|
} else {
|
|
alert('Error: ' + (data.error || 'Connection failed'));
|
|
btn.disabled = false;
|
|
btn.textContent = 'Connect PayPal Account';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert('Error: ' + err.message);
|
|
btn.disabled = false;
|
|
btn.textContent = 'Connect PayPal Account';
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|