/* ============================================================ BOOK IT deep-link builder (A1 Session 3). Builds a per-book player-prop hand-off URL: buildBookLink({ book, player, sport, state? }) → { url, tracking } | null - Base hosts mirror the scan page's legacy SPORTSBOOKS list; the organic URL shape (`/?search=`) is the shape the product has always shipped. - The affiliate layer reads web/src/lib/affiliateConfig.js. Every book is `enabled: false` today (no program approved), so links are clean + organic. Flipping a book's config produces a tracked URL — no component changes needed. - Every rendered book anchor must carry BOOK_LINK_REL ("sponsored noopener noreferrer") — sponsored is required for affiliate compliance and harmless on organic links. - Unknown book → null. Absent beats wrong. CommonJS so the plain-JS Jest suite requires it AND .tsx imports it (allowJs) — same pattern as slateAdapter/books/playerName. ============================================================ */ const { bookKey } = require('./books'); const { AFFILIATE_CONFIG } = require('./affiliateConfig'); /** rel attribute every sportsbook anchor must render. */ const BOOK_LINK_REL = 'sponsored noopener noreferrer'; /** The books we build deep links for (order = display order). */ const SUPPORTED_BOOKS = [ { id: 'draftkings', label: 'DraftKings', host: 'sportsbook.draftkings.com' }, { id: 'fanduel', label: 'FanDuel', host: 'sportsbook.fanduel.com' }, { id: 'betmgm', label: 'BetMGM', host: 'sports.betmgm.com' }, { id: 'caesars', label: 'Caesars', host: 'sportsbook.caesars.com' }, // BetRivers routes by state subdomain (mi.betrivers.com); www works // as the state-agnostic landing. { id: 'betrivers', label: 'BetRivers', host: 'www.betrivers.com' }, ]; const HOSTS = SUPPORTED_BOOKS.reduce((m, b) => { m[b.id] = b.host; return m; }, {}); /** Canonicalize any book spelling ("DK", "DraftKings", "draftkings") → * a SUPPORTED_BOOKS id, or null when it isn't a book we link to. */ function resolveBookId(book) { const k = bookKey(book); return HOSTS[k] ? k : null; } /** Two-letter US state code or null — anything else is dropped, never guessed. */ function cleanState(state) { const s = String(state == null ? '' : state).trim().toLowerCase(); return /^[a-z]{2}$/.test(s) ? s : null; } /** * Build the deep link for one book + player. * * @param {{ book: string, player: string, sport?: string, state?: string }} input * @param {object} [config] — injectable for tests; defaults to AFFILIATE_CONFIG. * @returns {{ url: string, tracking: boolean } | null} */ function buildBookLink(input, config = AFFILIATE_CONFIG) { const { book, player, state } = input || {}; const id = resolveBookId(book); if (!id || !player) return null; let host = HOSTS[id]; if (id === 'betrivers') { const st = cleanState(state); if (st) host = `${st}.betrivers.com`; } const url = new URL(`https://${host}/`); url.searchParams.set('search', String(player)); // Affiliate layer — only when the operator flipped this book on AND // real (non-empty) params exist. Otherwise the link stays organic. let tracking = false; const cfg = config && config[id]; if (cfg && cfg.enabled === true && cfg.params && typeof cfg.params === 'object') { for (const [key, value] of Object.entries(cfg.params)) { const v = String(value == null ? '' : value).trim(); if (!key || !v) continue; // never emit an empty/fabricated id url.searchParams.set(key, v); tracking = true; } } return { url: url.toString(), tracking }; } module.exports = { buildBookLink, resolveBookId, SUPPORTED_BOOKS, BOOK_LINK_REL };