S3 (a1): affiliate + partner plumbing
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>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user