/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); ?> PayPal check<?= $school !== '' ? ' — ' . h($school) : '' ?>

School PayPal check

Mirrors every rule the "Pay with PayPal" button applies. Delete this file when done.

Couldn't work out which school this is — the link has no subdomain and no ?school= was given. Open this page from the school's own link, or add &school=Exact School Name to the URL.

Schools currently connected (school_merchants.json)

$m): ?>
SchoolMerchant IDReady
yes' : 'no' ?>

None yet.

School

Checking
Resolved from
In schools database unknown (DB unavailable)' : ($foundInDb ? 'yes' : 'NO — name doesn\'t match any school_name in the DB') ?>

1 · Onboarding status

Connected & ready. Merchant ID , connected . PayPal reports this school can receive payments.
Connected, but NOT ready. The school linked its PayPal account (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.
Onboarding started but never completed. The school opened the onboarding flow but PayPal hasn't confirmed the connection. Ask the school to finish the PayPal sign-up flow, or restart onboarding from the admin page.
Not onboarded. No record of this school in school_merchants.json and no pending attempt. This is why the button doesn't show — the school needs to complete PayPal onboarding first.
Name mismatch detected! A connected school exists under a slightly different spelling: ' . h($nm) . ' '; ?>
The button looks the school up by its exact name (), so this near-match is invisible to it. The stored name and the app's school_name must match character-for-character.
Pending attemptStored nameStatusStarted

2 · Checkout page deployment

This script's folder
checkout.php next to this script yes' : 'MISSING' ?>
Folder the button auto-detects (probes ' . h($c) . ' '; ?>) ' . h($detectedBase) . '' : 'NONE FOUND' ?>
The integration is deployed (this script is running), but not in any folder the button probes under this site's document root. Either move it to /pp-integration, or define PPP_WEB_BASE as before the button include, or add to the probe list in paypal_ppcp_button.php.
Note: this script runs from but the button will use — there appear to be two copies of the integration deployed. Make sure is the up-to-date one (it wins).

3 · Gateway setting

Database unavailable: — gateway can't be read. (The button would treat an unreadable/unset value as Express Checkout.)
PayPal/gateway columns on schools '' . h($c) . '', $gatewayCols)) : 'none found'; ?>
Stored gateway value ' . h($gwRaw) . '' : 'unset → treated as Express Checkout' ?>
Express Checkout? unknown' : ($isExpress ? 'yes' : 'no — this school keeps its existing REST API button') ?>

Raw values: $v) echo '' . h($c) . '=' . h($v === null ? 'NULL' : (string)$v) . ' '; ?>

Verdict

✔ 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 $0, which this page can't check.)