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,72 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user