Files
HotLunches/square/refresh_tokens.php
T
2026-10-19 15:22:52 -07:00

111 lines
4.8 KiB
PHP

<?php
/**
* Square token-refresh cron job (Phase 2)
* ───────────────────────────────────────
* Square access tokens expire after 30 days; refresh tokens (code flow) do not
* expire. This sweep refreshes every connected school's access token while it
* still has plenty of runway, so a token is never allowed to lapse and break
* payments.
*
* It is also self-healing for the web side: helper.php refreshes lazily on use,
* but a school that hasn't transacted in a month would otherwise drift toward
* expiry — this job covers them.
*
* USAGE (CLI only — refuses to run over HTTP):
* php refresh_tokens.php # refresh tokens expiring within 5 days
* php refresh_tokens.php --days=10 # widen the window
* php refresh_tokens.php --force # refresh ALL connected schools now
* php refresh_tokens.php --dry-run # report what WOULD happen, change nothing
* php refresh_tokens.php --school="Name"# limit to one school
*
* CRON — Square recommends renewing access tokens every 7 days or less, so the
* RECOMMENDED schedule is a weekly --force sweep (rotates every token weekly):
* 30 3 * * 0 /usr/bin/php /path/to/square/refresh_tokens.php --force >> /var/log/square_refresh.log 2>&1
* Optionally add a daily threshold sweep as a safety net against any lapse:
* 15 3 * * * /usr/bin/php /path/to/square/refresh_tokens.php --days=7 >> /var/log/square_refresh.log 2>&1
*
* Exit codes: 0 = all good (or dry-run), 1 = one or more refreshes failed.
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
header('Content-Type: text/plain');
echo "refresh_tokens.php is a CLI script and must not be run over HTTP.\n";
exit(1);
}
// Optional app logging, if these helpers are on the include path. Wrapped so the
// script also runs standalone (e.g. from a bare cron environment).
if (file_exists(__DIR__ . '/../include/logging-functions.php')) {
require_once __DIR__ . '/../include/logging-functions.php';
if (function_exists('startLog')) startLog('square-refresh-cron');
}
require_once __DIR__ . '/lib/SquareClient.php';
require_once __DIR__ . '/lib/MerchantStore.php';
$config = require __DIR__ . '/config.php';
// ── Parse flags ──────────────────────────────────────────────────────────────
$opts = getopt('', ['days::', 'force', 'dry-run', 'school::', 'help']);
if (isset($opts['help'])) {
fwrite(STDOUT, "Usage: php refresh_tokens.php [--days=N] [--force] [--dry-run] [--school=\"Name\"]\n");
exit(0);
}
$thresholdDays = isset($opts['days']) ? max(0, (int)$opts['days']) : 5;
$force = isset($opts['force']);
$dryRun = isset($opts['dry-run']);
$onlySchool = isset($opts['school']) ? (string)$opts['school'] : null;
$ts = date('c');
fwrite(STDOUT, "[$ts] Square token refresh starting"
. " (days=$thresholdDays" . ($force ? ", force" : "") . ($dryRun ? ", dry-run" : "") . ($onlySchool ? ", school=\"$onlySchool\"" : "") . ")\n");
try {
$client = new SquareClient($config);
$store = new MerchantStore($config);
} catch (Throwable $e) {
fwrite(STDERR, "FATAL: " . $e->getMessage() . "\n");
exit(1);
}
// record_num => name. Keyed by the unique record_num so same-named schools are
// each refreshed (a name-keyed sweep would refresh only the first of them).
$schools = $store->connectedSchoolNums();
if ($onlySchool !== null) {
$schools = array_filter($schools, fn($name) => $name === $onlySchool);
}
if (empty($schools)) {
fwrite(STDOUT, "No connected schools to process.\n");
exit(0);
}
$counts = ['refreshed' => 0, 'skipped' => 0, 'would_refresh' => 0, 'failed' => 0, 'no_refresh_token' => 0, 'not_connected' => 0];
foreach ($schools as $schoolNum => $school) {
$result = $store->ensureFreshToken($school, $client, [
'threshold_days' => $thresholdDays,
'force' => $force,
'dry_run' => $dryRun,
'school_num' => $schoolNum,
]);
$action = $result['action'] ?? 'failed';
$counts[$action] = ($counts[$action] ?? 0) + 1;
$line = sprintf(" %-16s %s (#%d)", $action, $school, $schoolNum);
if (!empty($result['expires_at'])) $line .= " (expires {$result['expires_at']})";
if (!empty($result['detail'])) $line .= " — {$result['detail']}";
fwrite(STDOUT, $line . "\n");
}
$ts = date('c');
fwrite(STDOUT, "[$ts] Done. "
. "refreshed={$counts['refreshed']} skipped={$counts['skipped']} "
. "would_refresh={$counts['would_refresh']} failed={$counts['failed']} "
. "no_refresh_token={$counts['no_refresh_token']} not_connected={$counts['not_connected']}\n");
// Non-zero exit if anything failed, so cron / monitoring can alert.
exit($counts['failed'] > 0 ? 1 : 0);