feat: Feature 1.1 — Odds API integration complete, 28 tests passing
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
const express = require('express');
|
||||
const { getOdds } = require('../services/oddsService');
|
||||
const { MARKET_MAP, ALLOWED_BOOKS } = require('../utils/oddsNormalizer');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const VALID_STAT_TYPES = new Set(Object.values(MARKET_MAP));
|
||||
const VALID_BOOKS = ALLOWED_BOOKS;
|
||||
|
||||
// NCAAB is in-season November through April
|
||||
function isNcaabSeason() {
|
||||
const month = new Date().getUTCMonth() + 1; // 1-indexed
|
||||
return month >= 11 || month <= 4;
|
||||
}
|
||||
|
||||
function validateQueryParams(query) {
|
||||
const errors = [];
|
||||
|
||||
if (query.stat_type && !VALID_STAT_TYPES.has(query.stat_type)) {
|
||||
errors.push(`Invalid stat_type: ${query.stat_type}`);
|
||||
}
|
||||
|
||||
if (query.book && !VALID_BOOKS.has(query.book)) {
|
||||
errors.push(`Invalid book: ${query.book}`);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function filterProps(props, query) {
|
||||
let filtered = props;
|
||||
|
||||
if (query.stat_type) {
|
||||
filtered = filtered.filter((p) => p.stat_type === query.stat_type);
|
||||
}
|
||||
|
||||
if (query.player) {
|
||||
const search = query.player.toLowerCase();
|
||||
filtered = filtered.filter((p) => p.player.toLowerCase().includes(search));
|
||||
}
|
||||
|
||||
if (query.book) {
|
||||
filtered = filtered.filter((p) => p.book === query.book);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Group flat props into the response format: grouped by player+stat with nested lines
|
||||
function groupProps(flatProps) {
|
||||
const grouped = {};
|
||||
|
||||
for (const prop of flatProps) {
|
||||
const key = `${prop.player}::${prop.stat_type}::${prop.game_time}`;
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = {
|
||||
player: prop.player,
|
||||
home_team: prop.home_team,
|
||||
away_team: prop.away_team,
|
||||
game_time: prop.game_time,
|
||||
stat_type: prop.stat_type,
|
||||
lines: [],
|
||||
};
|
||||
}
|
||||
grouped[key].lines.push({
|
||||
book: prop.book,
|
||||
line: prop.line,
|
||||
over_odds: prop.over_odds,
|
||||
under_odds: prop.under_odds,
|
||||
fetched_at: prop.fetched_at,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.values(grouped);
|
||||
}
|
||||
|
||||
router.get('/nba', async (req, res) => {
|
||||
const errors = validateQueryParams(req.query);
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({ error: errors.join('; ') });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getOdds('nba');
|
||||
const filtered = filterProps(result.props, req.query);
|
||||
const props = groupProps(filtered);
|
||||
|
||||
if (result.stale) {
|
||||
res.set('X-BetonBLK-Stale', 'true');
|
||||
}
|
||||
|
||||
return res.json({
|
||||
sport: 'nba',
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/ncaab', async (req, res) => {
|
||||
if (!isNcaabSeason()) {
|
||||
return res.json({
|
||||
sport: 'ncaab',
|
||||
updated_at: new Date().toISOString(),
|
||||
source: 'none',
|
||||
quota_remaining: null,
|
||||
props: [],
|
||||
message: 'NCAAB is off-season. Props return in November.',
|
||||
});
|
||||
}
|
||||
|
||||
const errors = validateQueryParams(req.query);
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({ error: errors.join('; ') });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getOdds('ncaab');
|
||||
const filtered = filterProps(result.props, req.query);
|
||||
const props = groupProps(filtered);
|
||||
|
||||
if (result.stale) {
|
||||
res.set('X-BetonBLK-Stale', 'true');
|
||||
}
|
||||
|
||||
return res.json({
|
||||
sport: 'ncaab',
|
||||
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;
|
||||
Reference in New Issue
Block a user