Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)

This commit is contained in:
Kev
2026-06-10 14:50:13 -04:00
parent b9084408bf
commit ad5ea8d5a8
28 changed files with 3175 additions and 49 deletions
+5
View File
@@ -48,8 +48,13 @@ async function cachedAnalyze(prop) {
}
const VALID_STAT_TYPES = new Set([
// NBA / WNBA
'points', 'rebounds', 'assists', 'threes', 'blocks',
'steals', 'pra', 'turnovers',
// Soccer (Session 7j — assists already covered above; sport field
// discriminates downstream).
'goals', 'shots_on_target', 'shots', 'tackles', 'cards',
'corners', 'saves', 'goals_conceded', 'passes', 'clean_sheet',
]);
const VALID_DIRECTIONS = new Set(['over', 'under']);
+38
View File
@@ -147,4 +147,42 @@ router.get('/ncaab', async (req, res) => {
}
});
// Session 7j — soccer odds route. League is a path segment so each
// league has its own cache key (`odds:soccer_wc:2026-06-15` etc.) and
// queries don't cross-pollute. Falls through to getOdds → odds-api on
// demand; cached 15min like every other sport.
const { SOCCER_SPORT_KEYS } = require('../services/oddsService');
const SOCCER_KEY_SET = new Set(SOCCER_SPORT_KEYS);
router.get('/soccer/:league', async (req, res) => {
const leagueKey = `soccer_${String(req.params.league || '').toLowerCase()}`;
if (!SOCCER_KEY_SET.has(leagueKey)) {
return res.status(400).json({
error: `Unknown soccer league. Valid: ${SOCCER_SPORT_KEYS.map((k) => k.replace('soccer_', '')).join(', ')}.`,
});
}
const errors = validateQueryParams(req.query);
if (errors.length > 0) {
return res.status(400).json({ error: errors.join('; ') });
}
try {
const result = await getOdds(leagueKey);
const filtered = filterProps(result.props || [], req.query);
const props = groupProps(filtered);
if (result.stale) res.set('X-VYNDR-Stale', 'true');
return res.json({
sport: leagueKey,
updated_at: result.updated_at,
source: result.source,
quota_remaining: result.quota_remaining,
props,
});
} catch (err) {
const status = err.statusCode || 500;
return res.status(status).json({ error: err.message });
}
});
module.exports = router;
+4
View File
@@ -6,8 +6,12 @@ const { scanParlay } = require('../services/parlayScanService');
const router = express.Router();
const VALID_STAT_TYPES = new Set([
// NBA / WNBA
'points', 'rebounds', 'assists', 'threes', 'blocks',
'steals', 'pra', 'turnovers',
// Soccer (Session 7j)
'goals', 'shots_on_target', 'shots', 'tackles', 'cards',
'corners', 'saves', 'goals_conceded', 'passes', 'clean_sheet',
]);
const VALID_DIRECTIONS = new Set(['over', 'under']);