/** * Session 64 Order 1.5 — bind a prop to its REAL game. * * The root bug: PropLine emits no commence_time, so ledgerService fell back to * dating rows by the GRADE timestamp. A 01:00/03:00 UTC snapshot is the * previous ET day, so tonight's props were filed under yesterday — which made * settlement impossible and then got 64 rows wrongly voided as DNP. */ const binder = require('../../src/services/gameBinder'); const GAME = (id, home, away, time) => ({ id, homeTeam: { abbreviation: home }, awayTeam: { abbreviation: away }, gameTime: time }); describe('candidateDates', () => { test('a 03:00 UTC grade (23:00 ET previous day) considers the NEXT ET day', () => { // 2026-07-19T03:00Z = 2026-07-18 23:00 ET const c = binder.candidateDates('2026-07-19T03:00:00Z'); expect(c[0]).toBe('2026-07-18'); expect(c).toContain('2026-07-19'); // the real game day }); test('also looks one day back for an in-progress game', () => { expect(binder.candidateDates('2026-07-19T18:00:00Z')).toContain('2026-07-18'); }); }); describe('bindGame', () => { const prop = { player: 'X', stat_type: 'hits', home_team: 'CLE', away_team: 'PIT' }; test('trusts a real game_time when the feed provides one', async () => { const r = await binder.bindGame({ sport: 'mlb', prop: { ...prop, game_time: '2026-07-19T23:10:00Z' }, gradedAt: '2026-07-19T03:00:00Z', getSchedule: async () => [], }); expect(r.game_date).toBe('2026-07-19'); }); test('binds by TEAMS to the real game when game_time is missing', async () => { const r = await binder.bindGame({ sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z', getSchedule: async (sp, d) => (d === '2026-07-19' ? [GAME('401', 'CLE', 'PIT', '2026-07-19T23:10:00Z')] : []), }); expect(r.game_date).toBe('2026-07-19'); // NOT the 07-18 grade date expect(r.game_id).toBe('mlb:2026-07-19:401'); }); test('unbindable prop returns NULL — never a guessed date', async () => { const r = await binder.bindGame({ sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z', getSchedule: async () => [], }); expect(r).toBeNull(); }); test('DOUBLEHEADER is flagged ambiguous, not silently picked', async () => { const r = await binder.bindGame({ sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z', getSchedule: async (sp, d) => (d === '2026-07-19' ? [ GAME('401', 'CLE', 'PIT', '2026-07-19T17:10:00Z'), GAME('402', 'CLE', 'PIT', '2026-07-19T23:10:00Z'), ] : []), }); expect(r.ambiguous).toBe(true); }); test('team orientation is tolerated (feeds disagree on home/away)', async () => { const r = await binder.bindGame({ sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z', getSchedule: async (sp, d) => (d === '2026-07-19' ? [GAME('401', 'PIT', 'CLE', '2026-07-19T23:10:00Z')] : []), }); expect(r).not.toBeNull(); }); }); describe('attachGameTimes', () => { test('binds a slate and reports counts (a silent collapse must be visible)', async () => { const props = [ { player: 'A', home_team: 'CLE', away_team: 'PIT' }, { player: 'B', home_team: 'ZZZ', away_team: 'YYY' }, // unbindable { player: 'C', home_team: 'CLE', away_team: 'PIT', game_time: '2026-07-19T23:10:00Z' }, ]; const out = await binder.attachGameTimes('mlb', props, { gradedAt: '2026-07-19T03:00:00Z', getSchedule: async (sp, d) => (d === '2026-07-19' ? [GAME('401', 'CLE', 'PIT', '2026-07-19T23:10:00Z')] : []), }); expect(out).toMatchObject({ total: 3, bound: 1, alreadyHad: 1, unresolved: 1 }); expect(props[0].game_time).toBe('2026-07-19T23:10:00Z'); expect(props[1].game_time).toBeUndefined(); // left unresolved, not invented }); });