const { analyzeProp } = require('./propAnalyzer'); const { getOdds } = require('./oddsService'); const { detectCorrelations } = require('./correlationEngine'); const { gradeParlayFromLegs } = require('./parlayGrader'); const { generateUpgradePitch } = require('./upgradePitch'); const { getSupabaseServiceClient } = require('../utils/supabase'); async function scanParlay(user, legs) { const supabase = getSupabaseServiceClient(); const isFree = user.tier === 'free'; // Scan count check (atomic for free tier) if (isFree) { if (user.scan_count >= 5) { // Already exhausted — return 403 with pitch const pitch = await generateUpgradePitch(supabase, user.id, null); return { blocked: true, scan_count: user.scan_count, scans_remaining: 0, upgrade_pitch: pitch, }; } } // Analyze all legs const legResults = []; for (const leg of legs) { const result = await analyzeProp(leg); legResults.push(result); } // Fetch odds data for correlation detection (spreads, game context) let spreads = []; try { const oddsData = await getOdds('nba'); spreads = oddsData.spreads || []; // Attach game context to leg results for correlation detection for (const leg of legResults) { const matchingProps = (oddsData.props || []).filter( (p) => p.player.toLowerCase().includes(leg.player.toLowerCase()) ); if (matchingProps.length > 0) { const prop = matchingProps[0]; leg._gameTime = prop.game_time; // Resolve team from season avg const seasonStep = leg.reasoning?.steps?.season_avg; const team = leg._resolvedTeam || null; // Use the team from the analysis context if (leg.reasoning?.steps?.situational?.home_away?.context === 'home') { leg._team = prop.home_team; } else if (leg.reasoning?.steps?.situational?.home_away?.context === 'away') { leg._team = prop.away_team; } } } } catch (_) { // Correlation detection is best-effort } // Detect correlations const correlationFlags = detectCorrelations(legResults, spreads); // Grade the parlay // Attach composite scores from individual analyses for parlay grading for (const leg of legResults) { // Reconstruct composite from the reasoning steps const steps = leg.reasoning?.steps; if (steps) { const seasonDelta = steps.season_avg?.vs_line || 0; const recentDelta = steps.recent_form?.vs_line || 0; leg._composite = (Math.abs(seasonDelta) + Math.abs(recentDelta)) / 2; } else { leg._composite = 0; } } const { grade: parlayGrade, confidence: parlayConfidence } = gradeParlayFromLegs( legResults, correlationFlags ); // Write to database const pickIds = []; for (const leg of legResults) { const { data: pick, error } = await supabase .from('picks') .insert({ user_id: user.id, player: leg.player, stat_type: leg.stat_type, line: leg.line, book: leg.book || 'unknown', direction: leg.direction, grade: leg.grade, edge_pct: leg.edge_pct, reasoning: leg.reasoning?.summary || '', kill_conditions: (leg.kill_conditions_triggered || []).map((k) => k.code), confidence: leg.confidence, }) .select('id') .single(); if (pick) pickIds.push(pick.id); } // Write scan session const { data: session } = await supabase .from('scan_sessions') .insert({ user_id: user.id, legs: pickIds, final_grade: parlayGrade, kill_conditions: correlationFlags .filter((f) => f.impact !== 'positive') .map((f) => f.type), correlation_notes: JSON.stringify(correlationFlags), }) .select('id') .single(); // Atomic scan count increment for free tier let newScanCount = user.scan_count; if (isFree) { const { data: updated } = await supabase .from('users') .update({ scan_count: user.scan_count + 1 }) .eq('id', user.id) .eq('scan_count', user.scan_count) .select('scan_count') .single(); newScanCount = updated?.scan_count ?? user.scan_count + 1; } // Build response legs (stripped of internal fields) const responseLegs = legResults.map((leg, i) => ({ index: i, player: leg.player, stat_type: leg.stat_type, line: leg.line, direction: leg.direction, grade: leg.grade, confidence: leg.confidence, edge_pct: leg.edge_pct, kill_conditions: leg.kill_conditions_triggered || [], reasoning_summary: leg.reasoning?.summary || '', })); // Generate upgrade pitch at scan 5 let upgradePitch = null; if (isFree && newScanCount >= 5) { upgradePitch = await generateUpgradePitch(supabase, user.id, { grade: parlayGrade, legs: responseLegs, }); } return { blocked: false, scan_id: session?.id || null, parlay_grade: parlayGrade, parlay_confidence: parlayConfidence, correlation_flags: correlationFlags, legs: responseLegs, scan_count: newScanCount, scans_remaining: isFree ? Math.max(0, 5 - newScanCount) : null, upgrade_pitch: upgradePitch, }; } module.exports = { scanParlay };