Session 50: Complete Parlay Lab (2215 tests)

Correlation-aware combined parlay grading — the Desk-tier differentiator.

- Correlation model (parlayService.js, added to S28 funcs): correlationScore
  (game-aware 0.7/0.4/0.2/0.0), combinedGrade (avg penalized by avgCorr*0.5),
  estimatedPayout (fair-odds product * (1-avgCorr) discount), correlationWarning,
  gradeParlay.
- POST /api/parlay/grade (public, 2-6 legs) -> {combined,correlation,payout,legs}.
  Fixed the Next proxy (was forwarding to /api/scan/parlay).
- ParlayContext: legs gained team/game/archetype; tier-aware maxLegs; auto-grades
  the slip (debounced) when legs>=2 -> live combined/correlation/payout; hasLeg/
  legKey/atCap.
- "+" button on every graded prop: StatStrip onAddLeg/isLegActive, wired by
  vyndr/GameCard via useParlay (builds leg w/ team + game). GradeResultCard feeds
  the same context from the scan page.
- ParlayPanel (replaces legacy ParlayTray): bottom slide-up w/ legs, combined
  grade, correlation warning, est payout, CLEAR ALL + floating leg-count badge.
  Tier-gated: free 2 legs (payout blurred -> Desk upsell), Analyst 4, Desk 6.

Backend 2185 -> 2215 tests (+30), 187 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-19 11:25:14 -04:00
parent 3b47b783dc
commit f1956dc953
14 changed files with 706 additions and 48 deletions
+10 -38
View File
@@ -1,56 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { getUserFromRequest, jsonError } from '@/lib/auth-helpers';
export const dynamic = 'force-dynamic';
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
interface Leg {
sport?: 'NBA' | 'MLB' | 'WNBA';
player: string;
stat_type: string;
line: number;
direction: 'over' | 'under';
}
interface Body {
legs: Leg[];
}
/**
* Parlay Lab grade proxy (Session 50) — forwards POST /api/parlay/grade to the
* Express correlation model. Public + stateless (the Lab UI is tier-gated on the
* frontend); body carries { legs, betAmount }.
*/
export async function POST(req: NextRequest) {
const user = await getUserFromRequest(req);
if (!user) return jsonError(401, 'Log in to grade parlays.');
let body: Body;
const body = await req.text();
try {
body = (await req.json()) as Body;
} catch {
return jsonError(400, 'Invalid JSON.');
}
if (!Array.isArray(body.legs) || body.legs.length < 2 || body.legs.length > 12) {
return jsonError(400, 'Send 212 legs.');
}
try {
const upstream = await fetch(`${BACKEND_URL}/api/scan/parlay`, {
const upstream = await fetch(`${BACKEND_URL}/api/parlay/grade`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(req.headers.get('authorization') ? { Authorization: req.headers.get('authorization')! } : {}),
},
body: JSON.stringify({ legs: body.legs }),
body,
});
const data = await upstream.json().catch(() => ({}));
if (!upstream.ok) {
return NextResponse.json(
{ error: (data as { error?: string }).error || 'The engine hit a wall on this parlay.' },
{ status: upstream.status },
);
}
return NextResponse.json(data);
return NextResponse.json(data, { status: upstream.status });
} catch {
return jsonError(502, 'The engine hit a wall on this parlay.');
return NextResponse.json({ error: 'The engine hit a wall on this parlay.' }, { status: 502 });
}
}