Session 9: api-football + FootApi + Tank01 adapters, grace period middleware, cookie consent, /pricing page, OOM fix documented (1240 tests)

This commit is contained in:
Kev
2026-06-10 19:41:37 -04:00
parent 4db1c1c539
commit b55dcbd614
25 changed files with 2463 additions and 22 deletions
@@ -39,27 +39,66 @@ async function safeCacheGet(key) {
}
}
// Read per-player season aggregate. The prefetch writes a flat shape
// that already collapses played + minutes into per-90 rates so we don't
// recompute on every request.
// Source-priority cascade (Session 9). Each load function checks
// adapter-specific keys in priority order:
// 1. api-football.com (PRIMARY — 100 req/day, richest payload)
// 2. FootApi via RapidAPI (BACKUP — 50 req/day, Sofascore mirror)
// 3. football-data.org (TERTIARY — fixtures/standings only)
//
// The richer adapters write name-keyed aliases (`apifootball:player_by_name:…`,
// `footapi:player_by_name:…`) during the daily prefetch so the request
// path can resolve without an upstream call. When the alias key is
// missing — either because the prefetch hasn't run, the adapter has
// no key configured, or the player isn't covered — we fall through to
// the next source. Final fallback is the legacy `soccer:player:{name}`
// key the football-data prefetch already populates.
//
// Every value is tagged with `_source` so trap detection and reasoning
// can attribute the data origin (and so we can spot when a source is
// silently failing in production logs).
async function loadFromCascade(keys) {
for (const { key, source } of keys) {
const v = await safeCacheGet(key);
if (v && typeof v === 'object') {
return { ...v, _source: v._source || source };
}
}
return null;
}
async function loadPlayerProfile(playerName) {
if (!playerName) return null;
return safeCacheGet(`soccer:player:${normalizeName(playerName)}`);
const n = normalizeName(playerName);
return loadFromCascade([
{ key: `apifootball:player_by_name:${n}`, source: 'api-football' },
{ key: `footapi:player_by_name:${n}`, source: 'footapi' },
{ key: `soccer:player:${n}`, source: 'football-data' },
]);
}
async function loadNextMatch(teamName) {
if (!teamName) return null;
return safeCacheGet(`soccer:nextmatch:${teamName}`);
return loadFromCascade([
{ key: `apifootball:nextmatch:${teamName}`, source: 'api-football' },
{ key: `soccer:nextmatch:${teamName}`, source: 'football-data' },
]);
}
async function loadLastFixture(teamName) {
if (!teamName) return null;
return safeCacheGet(`soccer:lastfixture:${teamName}`);
return loadFromCascade([
{ key: `apifootball:lastfixture:${teamName}`, source: 'api-football' },
{ key: `soccer:lastfixture:${teamName}`, source: 'football-data' },
]);
}
async function loadRefereeProfile(refName) {
if (!refName) return null;
return safeCacheGet(`soccer:referee:${refName}`);
return loadFromCascade([
{ key: `apifootball:referee_by_name:${refName}`, source: 'api-football' },
{ key: `footapi:referee_by_name:${refName}`, source: 'footapi' },
{ key: `soccer:referee:${refName}`, source: 'football-data' },
]);
}
async function loadTeamDefense(league, teamName) {
@@ -221,6 +260,16 @@ async function extractSoccerFeatures(input = {}) {
isHome,
gameLogs: [],
errors,
// Session 9 — which source the cascade actually resolved for
// each load. Useful for debugging "why does Messi's profile
// look thin?" — if `player_source: 'football-data'` it means
// the api-football prefetch hasn't populated his row yet.
sources: {
player: profile?._source || null,
nextMatch: nextMatch?._source || null,
lastFixture: lastFixture?._source || null,
referee: refProfile?._source || null,
},
},
};
}