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>
115 lines
5.0 KiB
JavaScript
115 lines
5.0 KiB
JavaScript
// A1 Session 3 — BOOK IT deep-link builder. Organic by default; the
|
|
// affiliate layer only fires when a book is explicitly enabled with
|
|
// real params. Every anchor renders rel="sponsored noopener noreferrer".
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { buildBookLink, resolveBookId, SUPPORTED_BOOKS, BOOK_LINK_REL } = require('../../web/src/lib/bookLinks');
|
|
const { AFFILIATE_CONFIG } = require('../../web/src/lib/affiliateConfig');
|
|
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
|
|
|
describe('buildBookLink — organic by default', () => {
|
|
it('builds a clean organic link for every supported book (default config)', () => {
|
|
for (const b of SUPPORTED_BOOKS) {
|
|
const out = buildBookLink({ book: b.id, player: 'Aaron Judge', sport: 'mlb' });
|
|
expect(out).not.toBeNull();
|
|
expect(out.tracking).toBe(false);
|
|
expect(out.url).toContain(`https://${b.host}/`);
|
|
expect(out.url).toContain('search=Aaron+Judge');
|
|
// no affiliate params leak into an organic link
|
|
expect(out.url).not.toMatch(/irclickid|sharedid|btag|afid|siteid/);
|
|
}
|
|
});
|
|
|
|
it('ships EVERY book disabled in the checked-in config', () => {
|
|
for (const b of SUPPORTED_BOOKS) {
|
|
expect(AFFILIATE_CONFIG[b.id]).toBeDefined();
|
|
expect(AFFILIATE_CONFIG[b.id].enabled).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('resolves book aliases (DK / DraftKings / draftkings)', () => {
|
|
expect(resolveBookId('DK')).toBe('draftkings');
|
|
expect(resolveBookId('DraftKings')).toBe('draftkings');
|
|
expect(resolveBookId('fanduel')).toBe('fanduel');
|
|
expect(resolveBookId('MGM')).toBe('betmgm');
|
|
expect(resolveBookId('CZR')).toBe('caesars');
|
|
expect(resolveBookId('BR')).toBe('betrivers');
|
|
});
|
|
|
|
it('returns null for unknown books and missing player (absent beats wrong)', () => {
|
|
expect(buildBookLink({ book: 'bovada', player: 'Aaron Judge' })).toBeNull();
|
|
expect(buildBookLink({ book: 'prizepicks', player: 'Aaron Judge' })).toBeNull();
|
|
expect(buildBookLink({ book: 'draftkings', player: '' })).toBeNull();
|
|
expect(buildBookLink({})).toBeNull();
|
|
});
|
|
|
|
it('betrivers routes by two-letter state subdomain, else www', () => {
|
|
const mi = buildBookLink({ book: 'betrivers', player: 'Judge', state: 'MI' });
|
|
expect(mi.url).toContain('https://mi.betrivers.com/');
|
|
const bad = buildBookLink({ book: 'betrivers', player: 'Judge', state: 'Michigan' });
|
|
expect(bad.url).toContain('https://www.betrivers.com/');
|
|
const none = buildBookLink({ book: 'betrivers', player: 'Judge' });
|
|
expect(none.url).toContain('https://www.betrivers.com/');
|
|
});
|
|
});
|
|
|
|
describe('buildBookLink — affiliate layer (config-flip)', () => {
|
|
const enabled = {
|
|
draftkings: { enabled: true, params: { irclickid: 'IRC123', sharedid: 'vyndr', wpsrc: 'vyndr-slate' } },
|
|
betmgm: { enabled: true, params: { btag: 'BT-42', afid: '' } },
|
|
caesars: { enabled: false, params: { btag: 'SHOULD-NOT-APPEAR' } },
|
|
};
|
|
|
|
it('appends Impact-style params and reports tracking when enabled', () => {
|
|
const out = buildBookLink({ book: 'draftkings', player: 'Aaron Judge' }, enabled);
|
|
expect(out.tracking).toBe(true);
|
|
expect(out.url).toContain('irclickid=IRC123');
|
|
expect(out.url).toContain('sharedid=vyndr');
|
|
expect(out.url).toContain('wpsrc=vyndr-slate');
|
|
expect(out.url).toContain('search=Aaron+Judge');
|
|
});
|
|
|
|
it('skips empty param values (never emits a fabricated id)', () => {
|
|
const out = buildBookLink({ book: 'betmgm', player: 'Judge' }, enabled);
|
|
expect(out.tracking).toBe(true);
|
|
expect(out.url).toContain('btag=BT-42');
|
|
expect(out.url).not.toContain('afid=');
|
|
});
|
|
|
|
it('enabled with ONLY empty params stays organic (tracking false)', () => {
|
|
const out = buildBookLink({ book: 'fanduel', player: 'Judge' }, { fanduel: { enabled: true, params: { irclickid: ' ' } } });
|
|
expect(out.tracking).toBe(false);
|
|
expect(out.url).not.toContain('irclickid');
|
|
});
|
|
|
|
it('disabled book with params configured stays organic', () => {
|
|
const out = buildBookLink({ book: 'caesars', player: 'Judge' }, enabled);
|
|
expect(out.tracking).toBe(false);
|
|
expect(out.url).not.toContain('SHOULD-NOT-APPEAR');
|
|
});
|
|
});
|
|
|
|
describe('rel="sponsored noopener noreferrer" on every book anchor', () => {
|
|
it('exports the exact rel contract', () => {
|
|
expect(BOOK_LINK_REL).toBe('sponsored noopener noreferrer');
|
|
});
|
|
|
|
it('StatStrip BOOK IT anchor uses buildBookLink + BOOK_LINK_REL', () => {
|
|
const src = read('components/vyndr/StatStrip.tsx');
|
|
expect(src).toContain('buildBookLink');
|
|
expect(src).toContain('rel={BOOK_LINK_REL}');
|
|
expect(src).toContain('BOOK IT ⟶');
|
|
});
|
|
|
|
it('scan page hand-off anchors use buildBookLink + BOOK_LINK_REL', () => {
|
|
const src = read('app/scan/page.tsx');
|
|
expect(src).toContain('SUPPORTED_BOOKS');
|
|
expect(src).toContain('rel={BOOK_LINK_REL}');
|
|
// the old hardcoded organic builder is gone
|
|
expect(src).not.toContain('const deepLink =');
|
|
});
|
|
});
|