import { NextRequest, NextResponse } from 'next/server'; import { getUserFromRequest, jsonError } from '@/lib/auth-helpers'; import { getServiceRoleSupabase } from '@/lib/supabase'; import { rateLimitCheck, rateLimitKey, rateLimitResponse } from '@/middleware/rateLimit'; const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000'; const FREE_LIMIT = 5; // reads per calendar month const monthKey = () => new Date().toISOString().slice(0, 7) + '-01'; const isSameMonth = (date: string | null | undefined) => !!date && date.slice(0, 7) === new Date().toISOString().slice(0, 7); const VALID_SPORTS = new Set(['NBA', 'MLB', 'WNBA', 'Soccer']); const VALID_DIRECTIONS = new Set(['over', 'under']); const VALID_NBA_STATS = new Set(['points', 'rebounds', 'assists', 'threes', 'blocks', 'steals', 'pra', 'turnovers']); const VALID_MLB_STATS = new Set([ 'strikeouts', 'hits_allowed', 'earned_runs', 'innings_pitched', 'walks_allowed', 'hits', 'total_bases', 'rbi', 'runs', 'stolen_bases', 'home_runs', 'walks', 'singles', 'doubles', ]); const VALID_SOCCER_STATS = new Set([ 'goals', 'assists', 'shots_on_target', 'shots', 'tackles', 'cards', 'corners', 'saves', 'goals_conceded', 'passes', 'clean_sheet', ]); export const dynamic = 'force-dynamic'; interface ScanBody { sport: 'NBA' | 'MLB' | 'WNBA' | 'Soccer'; player: string; stat: string; line: number; direction: 'over' | 'under'; book?: string; } export async function POST(req: NextRequest) { let body: ScanBody; try { body = (await req.json()) as ScanBody; } catch { return jsonError(400, 'Invalid JSON body.'); } if (!VALID_SPORTS.has(body.sport)) return jsonError(400, 'Unknown sport.'); if (!VALID_DIRECTIONS.has(body.direction)) return jsonError(400, 'Direction must be over or under.'); if (typeof body.player !== 'string' || body.player.length === 0 || body.player.length > 80) { return jsonError(400, 'Player name is required.'); } if (typeof body.line !== 'number' || !Number.isFinite(body.line) || body.line < 0 || body.line > 500) { return jsonError(400, 'Line must be a number between 0 and 500.'); } const validStats = body.sport === 'MLB' ? VALID_MLB_STATS : body.sport === 'Soccer' ? VALID_SOCCER_STATS : VALID_NBA_STATS; if (!validStats.has(body.stat)) { return jsonError(400, `Stat "${body.stat}" not supported for ${body.sport}.`); } const user = await getUserFromRequest(req); const sb = getServiceRoleSupabase(); // Per-minute rate limit (different limit per tier) const rl = rateLimitCheck(rateLimitKey(req), user?.tier ?? 'free'); if (!rl.ok) return rateLimitResponse(rl.retryAfter); // Throttle free tier (monthly cap) if (user && user.tier === 'free' && sb) { const { data: profile } = await sb .from('user_profiles') .select('scan_count, scan_reset_date') .eq('id', user.id) .maybeSingle(); const usedThisMonth = isSameMonth(profile?.scan_reset_date) ? (profile?.scan_count ?? 0) : 0; if (usedThisMonth >= FREE_LIMIT) { return NextResponse.json( { error: "You've used your 5 free reads this month. Unlock unlimited reads and full intelligence — Founder Access, $14.99/mo.", scans_remaining: 0, upgrade: { tier: 'analyst', price: 14.99 }, }, { status: 402 }, ); } } // Forward to backend grading engine try { const upstream = await fetch(`${BACKEND_URL}/api/analyze/prop`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(req.headers.get('authorization') ? { Authorization: req.headers.get('authorization')! } : {}), }, body: JSON.stringify({ sport: body.sport, player: body.player, stat_type: body.stat, line: body.line, direction: body.direction, book: body.book ?? 'draftkings', }), }); const data = await upstream.json().catch(() => ({})); if (!upstream.ok) { return NextResponse.json( { error: data?.error || 'The engine hit a wall. Try that read again.' }, { status: upstream.status }, ); } let scansRemaining: number | null = null; if (user && sb) { void sb.rpc('increment_parlay_leg_frequency', { p_player: body.player, p_stat: body.stat, p_line: body.line, p_dir: body.direction, p_sport: body.sport, p_scan_delta: 1, p_parlay_delta: 0, }); void sb.from('scan_history').insert({ user_id: user.id, sport: body.sport, player_name: body.player, stat: body.stat, line: body.line, direction: body.direction, grade: data.grade, projection: data.projection, confidence: data.confidence, factors: data.factors ?? null, }); if (user.tier === 'free') { const thisMonth = monthKey(); const { data: current } = await sb .from('user_profiles') .select('scan_count, scan_reset_date') .eq('id', user.id) .maybeSingle(); const next = (isSameMonth(current?.scan_reset_date) ? (current?.scan_count ?? 0) : 0) + 1; await sb .from('user_profiles') .update({ scan_count: next, scan_reset_date: thisMonth }) .eq('id', user.id); scansRemaining = Math.max(0, FREE_LIMIT - next); } } return NextResponse.json({ ...data, scans_remaining: scansRemaining, tier: user?.tier ?? 'free' }); } catch (err) { console.error('[scan] backend call failed', err); return jsonError(502, 'The engine hit a wall. Try that read again.'); } }