Sessions 29-30: Content templates + PropLine 3-key adapter + MLB Stats API + ESPN summary (1694 tests)

This commit is contained in:
Kev
2026-06-14 22:29:01 -04:00
parent 927c4a5c65
commit a3351e2135
14 changed files with 1091 additions and 27 deletions
+56 -19
View File
@@ -280,6 +280,27 @@ function parseQuota(headers) {
return val != null ? parseInt(val, 10) : null;
}
// Best-effort post-fetch processing shared by both providers (PropLine +
// odds-api): line movement, scratch cascade, and rolling line snapshots.
// Never throws — a failure here must not break the odds response.
async function recordDownstream(sport, props) {
let movements = [];
let scratchedPlayers = [];
try {
const lineMovement = require('./lineMovementService');
const cascade = require('./cascadeService');
const moveResult = await lineMovement.processNewOdds(sport, props);
movements = moveResult.movements || [];
const cascadeResult = await cascade.detectScratches(sport, props);
scratchedPlayers = cascadeResult.scratchedPlayers || [];
const lineSnapshots = require('./lineSnapshotService');
await lineSnapshots.recordSnapshots(sport, props);
} catch (e) {
console.warn('[VYNDR] Movement/cascade detection error:', e.message);
}
return { movements, scratchedPlayers };
}
async function getOdds(sport) {
const redis = getRedisClient();
const apiKey = process.env.ODDS_API_KEY;
@@ -294,12 +315,43 @@ async function getOdds(sport) {
sport,
updated_at: data.updated_at,
source: 'cache',
provider: data.provider || 'odds-api',
quota_remaining: quota,
props: data.props,
spreads: data.spreads || [],
};
}
// Session 30 — PropLine is the PRIMARY props provider when configured:
// 3 free keys, 3,000 req/day combined (vs odds-api's 500/month). Try it
// first; on empty/error fall through to the conserved odds-api path
// below. Gated on hasKeys() so environments without PropLine keys (incl.
// the test suite) keep the exact prior behavior.
const propline = require('./adapters/proplineAdapter');
if (propline.hasKeys()) {
try {
const pl = await propline.getProps(sport);
if (pl && Array.isArray(pl.props) && pl.props.length > 0) {
const now = new Date().toISOString();
const cacheData = { updated_at: now, props: pl.props, spreads: pl.spreads || [], provider: 'propline' };
await redis.set(cacheKey, JSON.stringify(cacheData), 'EX', CACHE_TTL);
const { movements, scratchedPlayers } = await recordDownstream(sport, pl.props);
return {
sport,
updated_at: now,
source: 'live',
provider: 'propline',
props: pl.props,
spreads: pl.spreads || [],
movements,
scratchedPlayers,
};
}
} catch (e) {
console.warn('[oddsService] PropLine failed, falling back to odds-api:', e.message);
}
}
// Session 22 — pre-flight quota check now reads from the
// Session 20 tracker (truth source: synced from upstream
// response headers on every call). The legacy
@@ -333,32 +385,17 @@ async function getOdds(sport) {
}
const now = new Date().toISOString();
const cacheData = { updated_at: now, props, spreads };
const cacheData = { updated_at: now, props, spreads, provider: 'odds-api' };
await redis.set(cacheKey, JSON.stringify(cacheData), 'EX', CACHE_TTL);
// Line movement + cascade detection (best-effort, don't block response)
let movements = [];
let scratchedPlayers = [];
try {
const lineMovement = require('./lineMovementService');
const cascade = require('./cascadeService');
const moveResult = await lineMovement.processNewOdds(sport, props);
movements = moveResult.movements || [];
const cascadeResult = await cascade.detectScratches(sport, props);
scratchedPlayers = cascadeResult.scratchedPlayers || [];
// Session 28 — append a rolling line-history snapshot per prop so the
// sparkline / biggest-movers views have data. Redis-only, free.
const lineSnapshots = require('./lineSnapshotService');
await lineSnapshots.recordSnapshots(sport, props);
} catch (e) {
// Non-fatal — log and continue
console.warn('[VYNDR] Movement/cascade detection error:', e.message);
}
// Line movement + cascade + snapshots (best-effort; shared helper).
const { movements, scratchedPlayers } = await recordDownstream(sport, props);
return {
sport,
updated_at: now,
source: 'live',
provider: 'odds-api',
quota_remaining: quotaRemaining,
props,
spreads,