Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)
This commit is contained in:
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user