'use strict'; /** * lineupService — MLB lineup confirmation (Session 64 / A1-S5). * * FREE statsapi `schedule?hydrate=lineups`: once a team posts its lineup the * feed carries homePlayers/awayPlayers in batting order. From that, per * player (nameKey): * CONFIRMED (with batting slot) — the player is in a posted lineup. * NOT_IN — his team's lineup IS posted and he isn't in it. This visibly * kills the grade on the slate (struck through + chip). The * locked ledger read is untouched — honesty means SHOWING the * read is dead, not deleting it. * (absent) — his team hasn't posted yet → the UI renders PROJECTED. * * Cache 10 min (lineups post in waves pre-game). Pure parser + injectable * fetch → unit-tested on the real feed shape with zero network. */ const { nameKey } = require('../utils/playerName'); const TTL = 600; // 10 min const BASE = 'https://statsapi.mlb.com/api/v1'; const HTTP_TIMEOUT_MS = 10_000; /** * Pure: statsapi schedule JSON → { * byPlayer: { nameKey: { status:'confirmed', slot, team } }, * postedTeams: [team names whose lineup is up], * } * Players NOT in byPlayer whose team IS in postedTeams are NOT_IN — * resolved by statusFor(). */ function parseLineups(scheduleJson) { const byPlayer = {}; const postedTeams = []; const games = ((scheduleJson || {}).dates || [])[0]?.games || []; for (const g of games) { const lu = g.lineups || {}; for (const side of ['home', 'away']) { const players = lu[`${side}Players`]; if (!Array.isArray(players) || players.length === 0) continue; const team = g.teams?.[side]?.team?.name || null; if (team) postedTeams.push(team); players.forEach((p, i) => { if (!p || !p.fullName) return; byPlayer[nameKey(p.fullName)] = { status: 'confirmed', slot: i + 1, team }; }); } } return { byPlayer, postedTeams }; } /** Resolve one player's viability given parsed lineups + his team. */ function statusFor(player, team, parsed) { if (!parsed) return { status: 'projected' }; const hit = parsed.byPlayer[nameKey(player)]; if (hit) return hit; const token = (n) => String(n || '').toLowerCase().split(/\s+/).pop(); const posted = team && parsed.postedTeams.some((t) => t === team || token(t) === token(team)); return posted ? { status: 'not_in', team } : { status: 'projected' }; } async function fetchLineups(date, opts = {}) { const cacheGet = opts.cacheGet || require('../utils/redis').cacheGet; const cacheSet = opts.cacheSet || require('../utils/redis').cacheSet; const key = `lineups:mlb:${date}`; const cached = await cacheGet(key); if (cached) return cached; const axios = opts.axios || require('axios'); try { const res = await axios.get(`${BASE}/schedule?sportId=1&date=${date}&hydrate=lineups`, { timeout: HTTP_TIMEOUT_MS }); const parsed = parseLineups(res.data); await cacheSet(key, parsed, TTL); return parsed; } catch (e) { console.warn('[lineups] fetch failed:', e.message); return { byPlayer: {}, postedTeams: [] }; } } module.exports = { fetchLineups, parseLineups, statusFor, __internals: { TTL, BASE } };