/pp-integration/check-gateway.php?key=hl-gw-2026 * The school is resolved from the link exactly like the onboarding system * does (data/tenants.json mapping, then humanized subdomain). You can also * force a school explicitly: * .../check-gateway.php?key=hl-gw-2026&school=Central+Valley+Elementary * * For the resolved school it reports, guard by guard, everything the * "Pay with PayPal" button checks: * 1. Is the checkout page deployed where the button looks for it? * 2. Onboarding status — not started / pending / connected / ready * (including near-miss name mismatches, since the merchant lookup * is keyed by the EXACT school name) * 3. Gateway setting in the database — Express Checkout or not * 4. Final verdict: will the button render? */ if (($_GET['key'] ?? '') !== 'hl-gw-2026') { http_response_code(403); exit('Forbidden'); } header('Content-Type: text/html; charset=utf-8'); if (!function_exists('h')) { function h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } } if (!function_exists('norm')) { function norm(string $s): string { return strtolower(trim(preg_replace('/\s+/', ' ', $s))); } } // ── Resolve the school: ?school= first, else from the link (subdomain) ──────── require_once __DIR__ . '/lib/Tenant.php'; $resolvedBy = null; $school = trim((string)($_GET['school'] ?? '')); if ($school !== '') { $resolvedBy = 'school parameter in the URL'; } else { $sub = TenantResolver::currentSubdomain(); if ($sub !== null) { // Same mapping the onboarding system uses: data/tenants.json, then // a humanized subdomain (school-one → School One) as fallback. $tenants = []; $tenantsFile = __DIR__ . '/data/tenants.json'; if (is_file($tenantsFile)) { $tenants = json_decode((string)file_get_contents($tenantsFile), true) ?: []; } if (!empty($tenants[$sub]) && is_string($tenants[$sub])) { $school = $tenants[$sub]; $resolvedBy = "link subdomain \"$sub\" via tenants.json"; } else { $school = ucwords(str_replace(['-', '_'], ' ', $sub)); $resolvedBy = "link subdomain \"$sub\" (humanized — no tenants.json entry)"; } } } // ── Load onboarding data ────────────────────────────────────────────────────── $merchants = []; $mf = __DIR__ . '/data/school_merchants.json'; if (is_file($mf)) $merchants = json_decode((string)file_get_contents($mf), true) ?: []; $pending = []; $pf = __DIR__ . '/data/pending_onboarding.json'; if (is_file($pf)) $pending = json_decode((string)file_get_contents($pf), true) ?: []; // ── Onboarding status for the resolved school ───────────────────────────────── $onb = [ 'status' => 'not_started', // not_started | pending | connected_not_ready | connected_ready 'exact' => null, // merchant record on exact name match 'nearMatches' => [], // similar names found under a DIFFERENT exact spelling 'pendings' => [], // pending onboarding attempts for this school ]; if ($school !== '') { $rec = $merchants[$school] ?? null; if ($rec && !empty($rec['merchant_id'])) { $onb['exact'] = $rec; $ready = !isset($rec['paypal_ready']) || (bool)$rec['paypal_ready']; $onb['status'] = $ready ? 'connected_ready' : 'connected_not_ready'; } // Near-miss detection: same name after normalisation, different exact key. foreach ($merchants as $name => $m) { if ($name !== $school && norm($name) === norm($school)) { $onb['nearMatches'][] = $name; } } // Pending attempts (exact or normalised match on the stored school name). foreach ($pending as $trackingId => $p) { $pName = (string)($p['school'] ?? ''); if ($pName === $school || norm($pName) === norm($school)) { $onb['pendings'][] = [ 'tracking_id' => $trackingId, 'school' => $pName, 'status' => (string)($p['status'] ?? '?'), 'created_at' => (int)($p['created_at'] ?? 0), ]; } } if ($onb['status'] === 'not_started' && $onb['pendings']) { $onb['status'] = 'pending'; } } // ── Checkout page deployment check (guard #1 of the button) ─────────────────── // Mirrors the button's auto-detection: it probes these folders under the // document root and uses the first one containing checkout.php. $docroot = rtrim((string)($_SERVER['DOCUMENT_ROOT'] ?? ''), '/'); $candidates = ['/pp-integration', '/sm']; $detectedBase = null; foreach ($candidates as $c) { if (is_file($docroot . $c . '/checkout.php')) { $detectedBase = $c; break; } } $checkoutHere = is_file(__DIR__ . '/checkout.php'); // where this script actually lives $pathOk = $detectedBase !== null; $actualFolder = str_replace('\\', '/', dirname((string)($_SERVER['SCRIPT_NAME'] ?? ''))); // ── Gateway setting from the database ───────────────────────────────────────── $dbError = null; $gatewayCols = []; $gwRaw = ''; $gwEffective = null; $foundInDb = false; $rawRow = []; try { $cfg = require __DIR__ . '/db_config.php'; mysqli_report(MYSQLI_REPORT_OFF); $db = @new mysqli($cfg['host'], $cfg['username'], $cfg['password'], $cfg['database']); if ($db->connect_error) { $dbError = 'Could not connect: ' . $db->connect_error; } else { $db->set_charset($cfg['charset'] ?? 'utf8mb4'); $res = $db->query('SHOW COLUMNS FROM schools'); $allCols = []; while ($c = $res->fetch_assoc()) { $allCols[] = $c['Field']; $f = strtolower($c['Field']); if (strpos($f, 'paypal') !== false || strpos($f, 'gateway') !== false || strpos($f, 'express') !== false) { $gatewayCols[] = $c['Field']; } } if ($school !== '' && $gatewayCols) { $colList = implode('`,`', array_map(fn($c) => str_replace('`', '', $c), $gatewayCols)); $stmt = $db->prepare("SELECT `$colList` FROM schools WHERE school_name = ? LIMIT 1"); $stmt->bind_param('s', $school); $stmt->execute(); $rawRow = $stmt->get_result()->fetch_assoc() ?: []; $stmt->close(); $foundInDb = (bool)$rawRow; // Mirror user/paypal_ppcp_button.php exactly. foreach (['paypal_checkout', 'paypal_gateway', 'paypal_checkout_type'] as $bc) { if (isset($rawRow[$bc]) && trim((string)$rawRow[$bc]) !== '') { $gwRaw = trim((string)$rawRow[$bc]); break; } } $gwEffective = $gwRaw === '' ? 'Express Checkout' : $gwRaw; } $db->close(); } } catch (Throwable $e) { $dbError = 'Error: ' . $e->getMessage(); } $isExpress = ($gwEffective === null) ? null : ($gwEffective === 'Express Checkout'); $connected = in_array($onb['status'], ['connected_ready', 'connected_not_ready'], true); $ready = $onb['status'] === 'connected_ready'; // Final verdict — every guard the button applies (balance > 0 can't be known here). $buttonShows = $pathOk && $connected && $ready && ($isExpress !== false); ?>
Mirrors every rule the "Pay with PayPal" button applies. Delete this file when done.
?school= was given. Open this page from the school's own link, or add
&school=Exact School Name to the URL.| School | Merchant ID | Ready |
|---|---|---|
| = h($n) ?> | = h($m['merchant_id'] ?? '—') ?> |
= (!isset($m['paypal_ready']) || $m['paypal_ready']) ? 'yes' : 'no' ?> |
None yet.
| Checking | = h($school) ?> |
| Resolved from | = h($resolvedBy) ?> |
| In schools database | = $dbError ? 'unknown (DB unavailable)' : ($foundInDb ? 'yes' : 'NO — name doesn\'t match any school_name in the DB') ?> |
= h($onb['exact']['merchant_id']) ?>,
connected = h(date('M j, Y', (int)$onb['exact']['connected_at'])) ?>.
PayPal reports this school can receive payments.= h($onb['exact']['merchant_id']) ?>) but PayPal says it can't receive
payments yet — usually an unconfirmed email or incomplete business info in the school's PayPal
account. The button stays hidden until PayPal marks it ready.school_merchants.json and no pending attempt. This is why the button doesn't show —
the school needs to complete PayPal onboarding first.= h($school) ?>), so this near-match is invisible to it. The stored name and the
app's school_name must match character-for-character.| Pending attempt | Stored name | Status | Started |
|---|---|---|---|
= h($p['tracking_id']) ?> | = h($p['school']) ?> | = h($p['status']) ?> | = $p['created_at'] ? h(date('M j, Y H:i', $p['created_at'])) : '—' ?> |
| This script's folder | = h($actualFolder) ?> |
| checkout.php next to this script | = $checkoutHere ? 'yes' : 'MISSING' ?> |
| Folder the button auto-detects (probes ' . h($c) . ' '; ?>) | = $pathOk ? '' . h($detectedBase) . '' : 'NONE FOUND' ?> |
/pp-integration, or define PPP_WEB_BASE as
= h($actualFolder) ?> before the button include, or add
= h($actualFolder) ?> to the probe list in paypal_ppcp_button.php.= h($actualFolder) ?> but the button will
use = h($detectedBase) ?> — there appear to be two copies of the integration
deployed. Make sure = h($detectedBase) ?> is the up-to-date one (it wins).PayPal/gateway columns on schools |
'' . h($c) . '', $gatewayCols)) : 'none found'; ?> |
| Stored gateway value | = $gwRaw !== '' ? '' . h($gwRaw) . '' : 'unset → treated as Express Checkout' ?> |
| Express Checkout? | = $isExpress === null ? 'unknown' : ($isExpress ? 'yes' : 'no — this school keeps its existing REST API button') ?> |
Raw values:
$v) echo '' . h($c) . '=' . h($v === null ? 'NULL' : (string)$v) . ' '; ?>
= $buttonShows ? '✔ The “Pay with PayPal” button WILL show for this school' : '✘ The button will NOT show for this school' ?>
(Assuming the parent has a balance to pay — the button also hides when the amount owing is = h('≤') ?> $0, which this page can't check.)