feat: Feature 2.1 — Parlay Scan with correlation detection + monetization

POST /api/scan/parlay — authenticated parlay analysis:
- Supabase JWT auth middleware (auth.getUser verification)
- 5 correlation types detected between legs (same_game, same_team,
  same_player_conflicting, positive_correlation, blowout_cascade)
- Overall parlay grading (A/B/C/D) with correlation penalty adjustments
- Free tier: 5 scans/month, atomic scan count increment
- Scan 5: full analysis + personalized upgrade pitch
- Scan 6+: 403 block with upgrade pitch
- Pitch personalization from scan history (top stats, grades, tier rec)
- DB writes: picks + scan_sessions per scan

30 new tests, 158 total (131 Node.js + 27 Python), all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-03-21 12:45:15 -04:00
parent c8c0962e56
commit 411cb6f196
14 changed files with 1539 additions and 48 deletions
+14
View File
@@ -94,3 +94,17 @@ Outcome level (nested under market.outcomes[]):
- Decision: Add `spreads` to the comma-separated markets list in the existing per-event API call. Zero additional API credits — the spreads data rides alongside the player prop data in the same request.
- Alternatives considered: Skip blowout_risk for now — rejected because it's a high-value kill condition that prevents bad bets on blowout games.
- Consequences: `oddsService.js` now returns a `spreads` array alongside `props`. The `extractSpreads()` function in `oddsNormalizer.js` parses game-level spread data separately from player-level props.
### DECISION-006: Auth via Supabase auth.getUser() (Feature 2.1)
- Date: 2026-03-21
- Context: Need to verify Supabase JWTs in the scan endpoint. Options were manual JWT verification with secret or using Supabase client.
- Decision: Use `supabase.auth.getUser(token)` from `@supabase/supabase-js`. Simpler, no JWT secret management, automatically validates token expiry and revocation.
- Alternatives considered: Manual JWT decode with `jsonwebtoken` library — rejected: more code, need to manage JWT secret, doesn't check revocation.
- Consequences: Auth middleware depends on Supabase API being reachable (same DNS blocker in WSL2). In production this is fine.
### DECISION-007: Atomic Scan Count Increment (Feature 2.1)
- Date: 2026-03-21
- Context: Race condition risk — two concurrent scans from same free-tier user could both read scan_count=4 and both proceed past the limit.
- Decision: Use atomic `UPDATE users SET scan_count = scan_count + 1 WHERE id = :id AND scan_count = :current_count RETURNING scan_count`. If no rows returned, another request incremented first.
- Alternatives considered: Postgres advisory locks — rejected: overkill for this use case. Optimistic concurrency with the WHERE clause is simpler and sufficient.
- Consequences: At worst, one of two concurrent requests will fail to increment and still proceed (the check happens before analysis). Acceptable for MVP — the 5-scan limit is soft, not a billing boundary.