331 lines
17 KiB
PHP
331 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* School PayPal diagnostic — TEMPORARY, delete after checking.
|
|
* ────────────────────────────────────────────────────────────
|
|
* Open it from the school's own link (same subdomain the school uses):
|
|
* https://<school-link>/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);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PayPal check<?= $school !== '' ? ' — ' . h($school) : '' ?></title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background:#f7f8fa; color:#1a202c; margin:0; padding:32px 20px; }
|
|
.wrap { max-width: 780px; margin: 0 auto; }
|
|
h1 { font-size:20px; margin:0 0 4px; }
|
|
.sub { font-size:13px; color:#718096; margin-bottom:22px; }
|
|
.card { background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:18px 20px; margin-bottom:16px; }
|
|
.card h2 { font-size:14px; margin:0 0 12px; }
|
|
table { border-collapse:collapse; width:100%; font-size:13.5px; }
|
|
th, td { text-align:left; padding:8px 10px; border-bottom:1px solid #f0f2f5; vertical-align:top; }
|
|
th { font-size:11px; text-transform:uppercase; letter-spacing:.5px; color:#718096; }
|
|
.yes { color:#276749; font-weight:600; }
|
|
.no { color:#c53030; font-weight:600; }
|
|
.mid { color:#b7791f; font-weight:600; }
|
|
.muted { color:#a0aec0; }
|
|
code { background:#f5f7fa; border:1px solid #e2e8f0; border-radius:4px; padding:1px 6px; font-size:12.5px; }
|
|
.warn { background:#fffaf0; border:1px solid #feebc8; color:#9c4221; border-radius:8px; padding:12px 14px; font-size:13px; margin:10px 0; line-height:1.55; }
|
|
.ok { background:#f0fff4; border:1px solid #c6f6d5; color:#276749; border-radius:8px; padding:12px 14px; font-size:13px; margin:10px 0; line-height:1.55; }
|
|
.bad { background:#fff5f5; border:1px solid #feb2b2; color:#c53030; border-radius:8px; padding:12px 14px; font-size:13px; margin:10px 0; line-height:1.55; }
|
|
.verdict { font-size:16px; font-weight:700; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<h1>School PayPal check</h1>
|
|
<p class="sub">Mirrors every rule the "Pay with PayPal" button applies. Delete this file when done.</p>
|
|
|
|
<?php if ($school === ''): ?>
|
|
|
|
<div class="bad">Couldn't work out which school this is — the link has no subdomain and no
|
|
<code>?school=</code> was given. Open this page from the school's own link, or add
|
|
<code>&school=Exact School Name</code> to the URL.</div>
|
|
|
|
<div class="card">
|
|
<h2>Schools currently connected (school_merchants.json)</h2>
|
|
<?php if ($merchants): ?>
|
|
<table>
|
|
<tr><th>School</th><th>Merchant ID</th><th>Ready</th></tr>
|
|
<?php foreach ($merchants as $n => $m): ?>
|
|
<tr><td><?= h($n) ?></td><td><code><?= h($m['merchant_id'] ?? '—') ?></code></td>
|
|
<td><?= (!isset($m['paypal_ready']) || $m['paypal_ready']) ? '<span class="yes">yes</span>' : '<span class="no">no</span>' ?></td></tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<?php else: ?><p class="muted">None yet.</p><?php endif; ?>
|
|
</div>
|
|
|
|
<?php else: ?>
|
|
|
|
<div class="card">
|
|
<h2>School</h2>
|
|
<table>
|
|
<tr><td>Checking</td><td><strong><?= h($school) ?></strong></td></tr>
|
|
<tr><td>Resolved from</td><td><?= h($resolvedBy) ?></td></tr>
|
|
<tr><td>In schools database</td>
|
|
<td><?= $dbError ? '<span class="mid">unknown (DB unavailable)</span>' : ($foundInDb ? '<span class="yes">yes</span>' : '<span class="no">NO — name doesn\'t match any school_name in the DB</span>') ?></td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>1 · Onboarding status</h2>
|
|
<?php if ($onb['status'] === 'connected_ready'): ?>
|
|
<div class="ok"><strong>Connected & ready.</strong> Merchant ID
|
|
<code><?= h($onb['exact']['merchant_id']) ?></code><?php if (!empty($onb['exact']['connected_at'])): ?>,
|
|
connected <?= h(date('M j, Y', (int)$onb['exact']['connected_at'])) ?><?php endif; ?>.
|
|
PayPal reports this school can receive payments.</div>
|
|
<?php elseif ($onb['status'] === 'connected_not_ready'): ?>
|
|
<div class="warn"><strong>Connected, but NOT ready.</strong> The school linked its PayPal account
|
|
(merchant ID <code><?= h($onb['exact']['merchant_id']) ?></code>) 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.</div>
|
|
<?php elseif ($onb['status'] === 'pending'): ?>
|
|
<div class="warn"><strong>Onboarding started but never completed.</strong> 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.</div>
|
|
<?php else: ?>
|
|
<div class="bad"><strong>Not onboarded.</strong> No record of this school in
|
|
<code>school_merchants.json</code> and no pending attempt. This is why the button doesn't show —
|
|
the school needs to complete PayPal onboarding first.</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($onb['nearMatches']): ?>
|
|
<div class="warn"><strong>Name mismatch detected!</strong> A connected school exists under a
|
|
slightly different spelling:
|
|
<?php foreach ($onb['nearMatches'] as $nm) echo '<code>' . h($nm) . '</code> '; ?><br>
|
|
The button looks the school up by its <strong>exact</strong> name
|
|
(<code><?= h($school) ?></code>), so this near-match is invisible to it. The stored name and the
|
|
app's <code>school_name</code> must match character-for-character.</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($onb['pendings']): ?>
|
|
<table>
|
|
<tr><th>Pending attempt</th><th>Stored name</th><th>Status</th><th>Started</th></tr>
|
|
<?php foreach ($onb['pendings'] as $p): ?>
|
|
<tr><td><code><?= h($p['tracking_id']) ?></code></td><td><?= h($p['school']) ?></td>
|
|
<td><?= h($p['status']) ?></td>
|
|
<td><?= $p['created_at'] ? h(date('M j, Y H:i', $p['created_at'])) : '—' ?></td></tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>2 · Checkout page deployment</h2>
|
|
<table>
|
|
<tr><td>This script's folder</td><td><code><?= h($actualFolder) ?></code></td></tr>
|
|
<tr><td>checkout.php next to this script</td>
|
|
<td><?= $checkoutHere ? '<span class="yes">yes</span>' : '<span class="no">MISSING</span>' ?></td></tr>
|
|
<tr><td>Folder the button auto-detects (probes <?php foreach ($candidates as $c) echo '<code>' . h($c) . '</code> '; ?>)</td>
|
|
<td><?= $pathOk ? '<span class="yes"><code>' . h($detectedBase) . '</code></span>' : '<span class="no">NONE FOUND</span>' ?></td></tr>
|
|
</table>
|
|
<?php if ($checkoutHere && !$pathOk): ?>
|
|
<div class="bad">The integration is deployed (this script is running), but <strong>not in any folder
|
|
the button probes</strong> under this site's document root. Either move it to
|
|
<code>/pp-integration</code>, or define <code>PPP_WEB_BASE</code> as
|
|
<code><?= h($actualFolder) ?></code> before the button include, or add
|
|
<code><?= h($actualFolder) ?></code> to the probe list in paypal_ppcp_button.php.</div>
|
|
<?php elseif ($pathOk && $checkoutHere && $detectedBase !== $actualFolder): ?>
|
|
<div class="warn">Note: this script runs from <code><?= h($actualFolder) ?></code> but the button will
|
|
use <code><?= h($detectedBase) ?></code> — there appear to be two copies of the integration
|
|
deployed. Make sure <code><?= h($detectedBase) ?></code> is the up-to-date one (it wins).</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>3 · Gateway setting</h2>
|
|
<?php if ($dbError): ?>
|
|
<div class="warn"><strong>Database unavailable:</strong> <?= h($dbError) ?> — gateway can't be read.
|
|
(The button would treat an unreadable/unset value as Express Checkout.)</div>
|
|
<?php else: ?>
|
|
<table>
|
|
<tr><td>PayPal/gateway columns on <code>schools</code></td>
|
|
<td><?php echo $gatewayCols ? implode(' ', array_map(fn($c) => '<code>' . h($c) . '</code>', $gatewayCols)) : '<span class="muted">none found</span>'; ?></td></tr>
|
|
<tr><td>Stored gateway value</td>
|
|
<td><?= $gwRaw !== '' ? '<code>' . h($gwRaw) . '</code>' : '<span class="muted">unset → treated as Express Checkout</span>' ?></td></tr>
|
|
<tr><td>Express Checkout?</td>
|
|
<td><?= $isExpress === null ? '<span class="mid">unknown</span>' : ($isExpress ? '<span class="yes">yes</span>' : '<span class="no">no — this school keeps its existing REST API button</span>') ?></td></tr>
|
|
</table>
|
|
<?php if ($rawRow): ?>
|
|
<p style="font-size:12px;color:#a0aec0;margin:10px 0 0;">Raw values:
|
|
<?php foreach ($rawRow as $c => $v) echo '<code>' . h($c) . '=' . h($v === null ? 'NULL' : (string)$v) . '</code> '; ?></p>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Verdict</h2>
|
|
<p class="verdict"><?= $buttonShows
|
|
? '<span class="yes">✔ The “Pay with PayPal” button WILL show for this school</span>'
|
|
: '<span class="no">✘ The button will NOT show for this school</span>' ?></p>
|
|
<p style="font-size:12.5px;color:#718096;margin-top:8px;">
|
|
(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.)</p>
|
|
</div>
|
|
|
|
<?php endif; ?>
|
|
</div>
|
|
</body>
|
|
</html>
|