0996320bd1
Zero out-of-pocket; everything config-flip-ready but DISABLED/organic. - BOOK IT deep links: web/src/lib/bookLinks.js + affiliateConfig.js (all books enabled:false, Impact/Partnerize param shapes documented, empty params skipped). Wired into StatStrip BookItTeaser (real anchor now) + scan hand-off links. Every book anchor renders rel="sponsored noopener noreferrer" (BOOK_LINK_REL). - Best-price marker: slateAdapter.detectBestBook (only when >=2 books post the SAME line and prices differ — absent beats wrong) + subtle signal-green dot in StatStrip. Slate.groupByGame threads the grouped per-book rows (books[]) onto PropRowProp instead of discarding them. - Partner refs: ?ref=CODE -> vyndr_ref cookie (90d, first-touch, PartnerRefCapture in layout) -> signup metadata partner_ref -> internal GET /api/partners/report/:code (requireInternalAuth; honest zeros + note until the TODO migration in docs/PARTNERS.md adds user_profiles.partner_ref — NOT run). Stripe promo-code convention: partner code == promotion code, verbatim. - Tests: +41 (2398 -> 2439, 209 suites); bookItTeaser + vyndrCoreScreens invariants updated to the new (stronger) rel contract. Web build exit 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
4.5 KiB
JavaScript
108 lines
4.5 KiB
JavaScript
// A1 Session 3 — best available price detection. Honesty rules: a best
|
|
// price only exists when ≥2 books post the SAME line for the side and
|
|
// their prices differ. Absent beats wrong.
|
|
|
|
const { detectBestBook, buildPlayerStripsFromProps } = require('../../web/src/lib/slateAdapter');
|
|
|
|
describe('detectBestBook', () => {
|
|
const rows = [
|
|
{ book: 'draftkings', line: 1.5, over_odds: -115, under_odds: -105 },
|
|
{ book: 'fanduel', line: 1.5, over_odds: -105, under_odds: -115 },
|
|
{ book: 'betmgm', line: 1.5, over_odds: -120, under_odds: 100 },
|
|
];
|
|
|
|
it('picks the best over price at the shared line', () => {
|
|
const out = detectBestBook(rows, 'over', 1.5);
|
|
expect(out).toEqual({ book: 'fanduel', odds: -105 });
|
|
});
|
|
|
|
it('picks the best under price independently', () => {
|
|
const out = detectBestBook(rows, 'under', 1.5);
|
|
expect(out).toEqual({ book: 'betmgm', odds: 100 });
|
|
});
|
|
|
|
it('returns null for a single book (a lone price is not "best")', () => {
|
|
expect(detectBestBook([rows[0]], 'over', 1.5)).toBeNull();
|
|
expect(detectBestBook([], 'over', 1.5)).toBeNull();
|
|
expect(detectBestBook(null, 'over', 1.5)).toBeNull();
|
|
});
|
|
|
|
it('returns null when all books post the identical price', () => {
|
|
const flat = [
|
|
{ book: 'draftkings', line: 1.5, over_odds: -110 },
|
|
{ book: 'fanduel', line: 1.5, over_odds: -110 },
|
|
];
|
|
expect(detectBestBook(flat, 'over', 1.5)).toBeNull();
|
|
});
|
|
|
|
it('returns null when books only post DIFFERENT lines (odds are not comparable)', () => {
|
|
const split = [
|
|
{ book: 'draftkings', line: 1.5, over_odds: -200 },
|
|
{ book: 'fanduel', line: 2.5, over_odds: 120 },
|
|
];
|
|
expect(detectBestBook(split, 'over', 1.5)).toBeNull();
|
|
expect(detectBestBook(split, 'over', 2.5)).toBeNull();
|
|
});
|
|
|
|
it('without a reference line, compares at the modal line', () => {
|
|
const mixed = [
|
|
{ book: 'draftkings', line: 1.5, over_odds: -115 },
|
|
{ book: 'fanduel', line: 1.5, over_odds: -105 },
|
|
{ book: 'betmgm', line: 2.5, over_odds: 200 },
|
|
];
|
|
expect(detectBestBook(mixed, 'over')).toEqual({ book: 'fanduel', odds: -105 });
|
|
});
|
|
|
|
it('ignores rows with missing book / line / side odds', () => {
|
|
const dirty = [
|
|
{ book: 'draftkings', line: 1.5, over_odds: -115 },
|
|
{ book: null, line: 1.5, over_odds: -100 },
|
|
{ book: 'fanduel', line: 1.5, over_odds: null },
|
|
{ book: 'betmgm', over_odds: -105 },
|
|
];
|
|
expect(detectBestBook(dirty, 'over', 1.5)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('buildPlayerStripsFromProps — bestBook attachment', () => {
|
|
const books = [
|
|
{ book: 'draftkings', line: 1.5, over_odds: -115, under_odds: -105 },
|
|
{ book: 'fanduel', line: 1.5, over_odds: -105, under_odds: -115 },
|
|
];
|
|
|
|
it('attaches bestBook + book to a graded prop (graded side)', () => {
|
|
const props = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', book: 'draftkings', books }];
|
|
const gradeIndex = {
|
|
'aaron judge|total_bases': {
|
|
player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5,
|
|
direction: 'over', grade: 'A', gradedAt: { line: 1.5, odds: -115, timestamp: new Date().toISOString() },
|
|
},
|
|
};
|
|
const strips = buildPlayerStripsFromProps(props, gradeIndex, {});
|
|
expect(strips).toHaveLength(1);
|
|
const p = strips[0].props[0];
|
|
expect(p.book).toBe('draftkings');
|
|
expect(p.bestBook).toEqual({ book: 'fanduel', odds: -105 });
|
|
});
|
|
|
|
it('attaches bestBook on awaiting props too (still real market data)', () => {
|
|
const props = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'under', book: 'draftkings', books }];
|
|
const strips = buildPlayerStripsFromProps(props, {}, {});
|
|
const p = strips[0].props[0];
|
|
expect(p.awaiting).toBe(true);
|
|
expect(p.bestBook).toEqual({ book: 'draftkings', odds: -105 });
|
|
});
|
|
|
|
it('leaves bestBook null when only one book posted the prop', () => {
|
|
const props = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', book: 'draftkings', books: [books[0]] }];
|
|
const strips = buildPlayerStripsFromProps(props, {}, {});
|
|
expect(strips[0].props[0].bestBook).toBeNull();
|
|
});
|
|
|
|
it('leaves bestBook null when props carry no books array (legacy shape)', () => {
|
|
const props = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', book: 'draftkings' }];
|
|
const strips = buildPlayerStripsFromProps(props, {}, {});
|
|
expect(strips[0].props[0].bestBook).toBeNull();
|
|
});
|
|
});
|