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:
@@ -801,6 +801,32 @@ phased plan in the Session-57 conversation / BUILD-STATE Next section).
|
||||
amber / VALUE green / red = STALE-or-miss only), mono data surfaces, and
|
||||
schedule-derived waiting copy. Keep them green.
|
||||
|
||||
## Affiliate + Partner Plumbing (A1 S3 — non-obvious)
|
||||
- **Every sportsbook link goes through `web/src/lib/bookLinks.js`**
|
||||
(`buildBookLink` → `{url, tracking}`; unknown book → null). The affiliate
|
||||
layer is `web/src/lib/affiliateConfig.js` — ALL books `enabled:false` (no
|
||||
program approved); flipping a book + filling params produces tracked URLs
|
||||
with NO component changes. Empty param values are SKIPPED (a half-filled
|
||||
config degrades to organic, never a fabricated id). Anchors MUST render
|
||||
`rel={BOOK_LINK_REL}` ("sponsored noopener noreferrer") — tests assert it.
|
||||
- **Best-price dot honesty rule:** `slateAdapter.detectBestBook(rows, side,
|
||||
refLine)` returns a book ONLY when ≥2 books post the SAME line for the side
|
||||
and prices differ — never compare odds across different lines, never mark a
|
||||
lone price "best". The per-book rows reach the browser as `lines[]` on each
|
||||
grouped prop; `Slate.groupByGame` now threads them as `books` on PropRowProp
|
||||
(pickLine still picks the single displayed line).
|
||||
- **Partner attribution:** `?ref=CODE` → `vyndr_ref` cookie (90d, FIRST-touch,
|
||||
`lib/partnerRef.js` + `PartnerRefCapture` in layout) → `signUp` metadata
|
||||
`partner_ref` → internal `GET /api/partners/report/:code`. The
|
||||
`user_profiles.partner_ref` column does NOT exist yet — the endpoint returns
|
||||
zeros + note until the TODO migration in `docs/PARTNERS.md` runs (metadata
|
||||
lands in auth.users.raw_user_meta_data, which PostgREST can't query). OAuth
|
||||
signups carry no metadata (known gap). Partner code == Stripe promotion code,
|
||||
verbatim (docs/PARTNERS.md §3).
|
||||
- **Worktree build gotcha:** `web/node_modules` isn't shared into git worktrees
|
||||
and Turbopack REJECTS a symlink pointing outside the project root — use
|
||||
`cp -al` (hardlink copy) from the main repo's web/node_modules.
|
||||
|
||||
## Active Skills
|
||||
- vyndr-voice (all user-facing output)
|
||||
- prop-analysis (grading methodology)
|
||||
|
||||
Reference in New Issue
Block a user