/** * UnifiedOddsProvider — orchestrator. * * Fan-out to every adapter via Promise.allSettled, normalize, attach * cross-source signals, run processing engines, return a structured * `sources` array describing what contributed. * * Never throws. Adapter failures are surfaced per-source so the API caller * can see exactly which upstreams contributed to a given refresh. */ const espn = require('./adapters/ESPNAdapter'); const pinnacle = require('./adapters/PinnacleAdapter'); const draftkings = require('./adapters/DraftKingsAdapter'); const fanduel = require('./adapters/FanDuelAdapter'); const betmgm = require('./adapters/BetMGMAdapter'); const caesars = require('./adapters/CaesarsAdapter'); const prizepicks = require('./adapters/PrizePicksAdapter'); const covers = require('./adapters/CoversAdapter'); const rotowire = require('./adapters/RotowireAdapter'); const lineShopping = require('./processing/LineShoppingEngine'); const middles = require('./processing/MiddlesDetector'); const ev = require('./processing/EVCalculator'); const { shouldCollect, isActiveSport } = require('../config/sports'); const rateLimiter = require('./rateLimiter'); const breaker = require('./circuitBreaker'); const ADAPTERS = [espn, pinnacle, draftkings, fanduel, betmgm, caesars, prizepicks, covers, rotowire]; function settleToResult(name, settled) { if (settled.status === 'fulfilled') { const value = settled.value; const count = Array.isArray(value) ? value.length : (value?.projections?.length ?? 0); return { source: name, ok: true, count }; } const err = settled.reason; return { source: name, ok: false, error: err?.code === 'NOT_IMPLEMENTED' ? 'not_implemented' : err?.code === 'BREAKER_OPEN' ? 'breaker_open' : err?.code === 'RATE_LIMIT_TIMEOUT' ? 'rate_limited' : (err?.message || 'unknown'), }; } async function fullRefresh(sport, { gradedProps = [] } = {}) { if (!shouldCollect(sport)) { return { sport, collected: false, reason: 'sport not in collection set', sources: [], data: { games: [], props: [], shopped: [], middles: [] }, refreshed_at: new Date().toISOString(), }; } const results = await Promise.allSettled([ espn.getGames(sport), pinnacle.getGames(sport), draftkings.getPlayerProps(sport), fanduel.getPlayerProps(sport), betmgm.getPlayerProps(sport), caesars.getPlayerProps(sport), prizepicks.getPlayerProps(sport), covers.getConsensus?.(sport) ?? Promise.resolve([]), rotowire.getProjections(sport), ]); const sources = [ settleToResult('espn', results[0]), settleToResult('pinnacle', results[1]), settleToResult('draftkings', results[2]), settleToResult('fanduel', results[3]), settleToResult('betmgm', results[4]), settleToResult('caesars', results[5]), settleToResult('prizepicks', results[6]), settleToResult('covers', results[7]), settleToResult('rotowire', results[8]), ]; const games = results[0].status === 'fulfilled' ? results[0].value : []; // Merge every adapter's player-prop payload (those that returned arrays). const props = []; for (const idx of [2, 3, 4, 5, 6]) { if (results[idx].status === 'fulfilled' && Array.isArray(results[idx].value)) { props.push(...results[idx].value); } } const shopped = lineShopping.process(props); const middlesFound = middles.detect(shopped); // EV labels for any already-graded props the caller fed in. const evRows = (gradedProps || []).map((g) => ({ key: g.key, grade: g.grade, odds: g.odds, ev: ev.calculate({ grade: g.grade, odds: g.odds }), })); return { sport, active: isActiveSport(sport), sources, data: { games, props, shopped, middles: middlesFound, ev: evRows, }, refreshed_at: new Date().toISOString(), }; } function status() { return { rate_limiters: rateLimiter.snapshot(), breakers: breaker.snapshot(), adapters: ADAPTERS.map((a) => a.name), }; } module.exports = { fullRefresh, status };