# A1 Session 3 — Affiliate + Partnership Plumbing

## Operating constraints
- ZERO out-of-pocket. No affiliate program is approved yet — everything ships
  config-flip-ready but DISABLED / organic by default.
- Data semantics: never fabricate market values. A "best price" claim renders
  only when genuinely multiple books post the same line — absent beats wrong.
- No exclamation points in any copy (VOICE v1.1).

## 1. BOOK IT deep-link builder
- `web/src/lib/bookLinks.js` (CommonJS, unit-testable)
  - `SUPPORTED_BOOKS`: draftkings, fanduel, betmgm, caesars, betrivers
    (hosts sourced from the existing SPORTSBOOKS list in scan/page.tsx +
    lib/books.js brand map).
  - `buildBookLink({ book, player, sport, state? }, config?) → { url, tracking } | null`
    - Unknown book → null (absent beats wrong).
    - Organic base: `https://{host}/?search={player}` (existing deep-link shape).
    - Affiliate layer: reads `web/src/lib/affiliateConfig.js` — per book
      `{ enabled: false, params: {} }` with Impact/Partnerize-style param
      shapes documented in comments (irclickid / sharedid / afid / btag).
    - `tracking: true` only when the book is enabled AND at least one
      non-empty param was appended. Disabled → clean organic link.
  - `BOOK_LINK_REL = 'sponsored noopener noreferrer'` — every book anchor
    renders this rel.
- Wiring:
  - `StatStrip.tsx` BookItTeaser → real `<a>` deep link (best-price book when
    known, else the prop's book, else DraftKings). Organic until config flips.
  - `scan/page.tsx` sportsbook hand-off anchors → `buildBookLink` +
    `BOOK_LINK_REL` (replaces the local SPORTSBOOKS/deepLink pair).

## 2. Best-line highlight (cheap version)
- Finding (documented in the session report): the grouped odds proxy already
  ships per-book rows to the browser as `lines[]` per player+stat;
  `Slate.groupByGame`/`pickLine` collapse to ONE line and discard the rest.
- Ship: thread `books` (the raw `lines[]`) onto each PropRowProp;
  `slateAdapter.detectBestBook(rows, side, refLine)` returns
  `{ book, odds }` ONLY when ≥2 books post the SAME line for the side and
  prices differ; `buildPlayerStripsFromProps` attaches `bestBook` + `book`
  to each strip prop; StatStrip renders a subtle signal-green dot
  (one meaning: best available price).

## 3. Partner ref system
- (a) `web/src/lib/partnerRef.js` (CommonJS) — parse `?ref=CODE`, sanitize
  (A–Z 0–9 - _, ≤32, uppercased), first-party cookie `vyndr_ref` (90d,
  SameSite=Lax, Path=/), first-touch (never overwrites an existing cookie).
  `components/vyndr/PartnerRefCapture.tsx` mounted in the layout
  (GlobalHosts pattern).
- (b) `src/routes/partners.js` — `GET /api/partners/report/:code` behind
  `requireInternalAuth` → `{ signups, conversions, mrr_attributed }` read
  from `user_profiles` filtered by `partner_ref`. The column does NOT exist
  yet: the endpoint degrades to zeros + a note, and the TODO migration is
  documented in docs/PARTNERS.md (NOT run).
- (c) Signup: `AuthContext.signUp` forwards the `vyndr_ref` cookie into
  Supabase signup metadata as `partner_ref`.
- (d) `docs/PARTNERS.md` — Stripe promo-code mapping convention + the
  TODO migration.

## Acceptance criteria
- All book links organic by default; flipping `enabled` + params in
  affiliateConfig produces a tracked URL with the params appended.
- Best-price dot never renders on single-book or unequal-line data.
- `?ref=CODE` first visit sets `vyndr_ref` for 90 days; signup metadata
  carries `partner_ref`; report endpoint 401s without the internal key.
- Full `npx jest` green; `cd web && npx next build` exit 0.

## Test plan
- tests/unit/bookLinks.test.js — organic default, enabled param shapes,
  aliases, betrivers state, unknown book, rel constant + source assertions.
- tests/unit/slateAdapterBestBook.test.js — detectBestBook honesty rules +
  strip attachment.
- tests/unit/partnerRef.test.js — sanitize/parse/cookie/first-touch.
- tests/unit/partnersRoute.test.js — report math + internal-auth mount.
