// Unit: book comparison service. Pure functions. // Book Comparison order (Phase 3) — the crown is now HONEST + threshold-gated: // - single-book / flat-market props render with NO crown // - the crown fires only among ≥2 books at the SAME line with DIFFERING prices // - and only when enabled (BOOK_CROWN_ENABLED / opts.crownEnabled), OFF until // Phase 2's spread measurement clears the bar. // The crown-logic cases below pass { crownEnabled: true } explicitly. const { compareProp, bestLines } = require('../../src/services/bookComparisonService'); const prop = { player: 'Wembanyama', stat_type: 'points', lines: [ { book: 'draftkings', line: 28.5, over_odds: -110, under_odds: -110 }, { book: 'fanduel', line: 28.5, over_odds: -105, under_odds: -115 }, // best over { book: 'betmgm', line: 28.5, over_odds: -120, under_odds: -102 }, // best under ], }; describe('compareProp — crown enabled (real spread)', () => { const on = { crownEnabled: true }; test('identifies the best OVER line (highest payout)', () => { const c = compareProp(prop, 'over', on); expect(c.crowned).toBe(true); expect(c.bestBook).toBe('fanduel'); // -105 pays more than -110/-120 expect(c.bestOdds).toBe(-105); expect(c.books.find((b) => b.book === 'fanduel').isBest).toBe(true); expect(c.bookCount).toBe(3); }); test('identifies the best UNDER line', () => { const c = compareProp(prop, 'under', on); expect(c.bestBook).toBe('betmgm'); // -102 pays more than -110/-115 expect(c.bestOdds).toBe(-102); }); test('savings is positive (best beats the field average)', () => { expect(compareProp(prop, 'over', on).savings).toBeGreaterThan(0); }); }); describe('compareProp — honest-absent', () => { test('single-book prop renders the one book, NO crown', () => { const c = compareProp({ player: 'X', stat_type: 'hits', lines: [{ book: 'dk', line: 0.5, over_odds: -110 }] }, 'over', { crownEnabled: true }); expect(c.bookCount).toBe(1); expect(c.bestBook).toBeNull(); expect(c.crowned).toBe(false); expect(c.books[0].isBest).toBe(false); expect(c.savings).toBe(0); }); test('crown DISABLED by default → multi-book prop shows all books, none crowned', () => { const c = compareProp(prop, 'over'); // no opts → env default (off in tests) expect(c.crowned).toBe(false); expect(c.books.every((b) => b.isBest === false)).toBe(true); expect(c.bestBook).toBeNull(); }); test('identical prices at the same line → no crown even when enabled', () => { const flat = { player: 'X', stat_type: 'hits', lines: [ { book: 'draftkings', line: 0.5, over_odds: -110, under_odds: -110 }, { book: 'fanduel', line: 0.5, over_odds: -110, under_odds: -110 }, ] }; expect(compareProp(flat, 'over', { crownEnabled: true }).crowned).toBe(false); }); test('no usable lines → null, not crash', () => { expect(compareProp({ player: 'X', stat_type: 'hits', lines: [] }, 'over')).toBeNull(); expect(compareProp({ player: 'X', stat_type: 'hits' }, 'over')).toBeNull(); }); test('reads a grouped `books` array (bookprices store shape)', () => { const c = compareProp({ player: 'Y', stat_type: 'hits', books: [ { book: 'draftkings', line: 1.5, over_odds: -115 }, { book: 'betmgm', line: 1.5, over_odds: -105 }, ] }, 'over', { crownEnabled: true }); expect(c.bestBook).toBe('betmgm'); }); }); describe('bestLines', () => { test('makes no crown claim when gated off (returns [])', () => { expect(bestLines([prop], { side: 'over', crownEnabled: false })).toEqual([]); }); test('enabled: drops single-book props and sorts by savings desc', () => { const props = [ prop, { player: 'Solo', stat_type: 'reb', lines: [{ book: 'dk', line: 5.5, over_odds: -110 }] }, // 1 book → dropped { player: 'BigEdge', stat_type: 'ast', lines: [ { book: 'dk', line: 5.5, over_odds: -200 }, { book: 'fd', line: 5.5, over_odds: +120 }, // huge spread → big savings ], }, ]; const lines = bestLines(props, { side: 'over', crownEnabled: true }); expect(lines.map((l) => l.player)).not.toContain('Solo'); expect(lines[0].player).toBe('BigEdge'); // biggest savings first }); test('non-array input → empty', () => { expect(bestLines(null)).toEqual([]); }); });