'use strict'; /** * TAKEABLE ANCHOR in the ledger write (2026-08-02). * * Self-inflicted regression: widening the books for DISPLAY leaked into the * ledger, which indexed the widened list and stamped book / locked_odds / and * the `takeable` flag itself from DFS, offshore and exchange books. Non-takeable * share went 0% -> 47.9% overnight. * * The invariant: the PRICE ANCHOR is takeable-only; GAME FACTS may come from any * book. Collapsing those two roles into one index is the bug. */ const { __internals } = require('../../src/services/ledgerService'); const { indexProps } = __internals; const snap = require('../../src/services/snapshotService').__internals; const p = (book, over, under, extra = {}) => ({ player: 'Kyle Schwarber', stat_type: 'hits', line: 1.5, book, over_odds: over, under_odds: under, game_time: '2026-08-02T23:10:00Z', ...extra, }); describe('indexProps — two roles, two indexes', () => { const props = [ p('dabble', -119, -119), // DFS pick'em — FIRST in the list, as in prod p('kalshi', 1100, -1400), // exchange p('draftkings', -140, 120), // takeable ]; it('the PRICE index admits takeable books ONLY, whatever indexed first', () => { const byTakeable = indexProps(props, true); expect(byTakeable['kyle schwarber|hits'].book).toBe('draftkings'); }); it('the GAME-FACTS index still accepts any book — first pitch is book-independent', () => { const byAny = indexProps(props); expect(byAny['kyle schwarber|hits']).toBeTruthy(); expect(byAny['kyle schwarber|hits'].game_time).toBe('2026-08-02T23:10:00Z'); }); it('gates on TAKEABLE, not MODEL — pinnacle is model-eligible but not takeable', () => { // A MODEL gate would let pinnacle anchor the price and re-break this the // moment its feed recovers. const withPinnacle = indexProps([p('pinnacle', -130, 110)], true); expect(withPinnacle['kyle schwarber|hits']).toBeUndefined(); }); it('NO takeable quote leaves the key ABSENT — an honest missing price', () => { const none = indexProps([p('dabble', -119, -119), p('bovada', -150, 130)], true); expect(none['kyle schwarber|hits']).toBeUndefined(); }); it('still prefers a two-sided takeable quote over a one-sided one', () => { const out = indexProps([p('draftkings', -140, null), p('betmgm', -135, 115)], true); expect(out['kyle schwarber|hits'].book).toBe('betmgm'); }); }); describe('snapshotService.indexOdds — the locked PRICE is takeable-gated', () => { it('skips DFS/exchange rows entirely', () => { const map = snap.indexOdds([ { player: 'Kyle Schwarber', stat_type: 'hits', book: 'dabble', over_odds: -119, under_odds: -119 }, { player: 'Kyle Schwarber', stat_type: 'hits', book: 'draftkings', over_odds: -140, under_odds: 120 }, ]); const k = Object.keys(map)[0]; expect(map[k].book).toBe('draftkings'); }); it('yields NO entry when only non-takeable books quote the prop', () => { const map = snap.indexOdds([ { player: 'X', stat_type: 'hits', book: 'kalshi', over_odds: 1100, under_odds: -1400 }, ]); expect(Object.keys(map)).toHaveLength(0); }); });