const express = require('express'); const { requireAuth } = require('../middleware/auth'); const { checkGracePeriod } = require('../middleware/gracePeriod'); const { getSupabaseServiceClient } = require('../utils/supabase'); const { createRateLimit } = require('../middleware/rateLimit'); const { resolveTierFromRequest } = require('../utils/requestTier'); const { getTopGraded } = require('../services/topGradedService'); const router = express.Router(); /** * GET /top-graded — the dashboard TOP GRADES board (specs/top-graded-selector.md). * * PUBLIC + tier-aware, NOT `requireAuth`: this board must keep serving anonymous * callers (it is the free funnel), so entitlement is resolved best-effort and * FAILS CLOSED to 'free'. Ranking happens server-side WITH `p_win` for every * tier; `stripModelPrice` then removes the paid signal for unentitled callers — * so a free caller gets the CORRECT ORDER without the paid VALUES. * * A thin slate is `200 { props: [] }`, never a 404 and never filler: the board * renders its own honest empty state, and a 404 is what left it blank for months. * * Cache-Control follows the /api/snapshot precedent — `private` once a bearer * token is present, so a CDN can never hand a paid payload to an anonymous viewer. */ router.get('/top-graded', createRateLimit({ windowMs: 60_000, max: 60 }), async (req, res) => { const deps = router.__deps || {}; const sportParam = req.query.sport ? String(req.query.sport) : null; try { const tier = await (deps.resolveTier || resolveTierFromRequest)(req); const out = await (deps.getTopGraded || getTopGraded)({ sport: sportParam, limit: req.query.limit, tier, ...(deps.cacheGet ? { cacheGet: deps.cacheGet } : {}), }); res.set('Cache-Control', req.headers.authorization ? 'private, max-age=60' : 'public, max-age=60'); return res.json(out); } catch (err) { console.error('[props/top-graded]', err.message); // Honest empty — the board's empty state, never a 404. return res.status(200).json({ props: [], sport: sportParam ? sportParam.toUpperCase() : null, updated_at: null }); } }); // Test seam — lets route tests inject without Redis/Supabase (slips.js precedent). router.__internals = { setDeps: (d) => { router.__deps = d; } }; // GET /joint-history — joint outcome history and phi coefficient router.get('/joint-history', requireAuth, checkGracePeriod, async (req, res) => { const { player_a, stat_a, player_b, stat_b } = req.query; // Block free tier if (!req.user.tier || req.user.tier === 'free') { return res.status(403).json({ error: 'Joint history requires Analyst or Desk tier', upgrade_url: '/pricing', }); } if (!player_a || !stat_a || !player_b || !stat_b) { return res.status(400).json({ error: 'Required query params: player_a, stat_a, player_b, stat_b', }); } try { const supabase = getSupabaseServiceClient(); const { data, error } = await supabase .from('joint_outcomes') .select('*') .eq('player_a', player_a) .eq('stat_a', stat_a) .eq('player_b', player_b) .eq('stat_b', stat_b); if (error) throw error; if (!data || data.length === 0) { return res.json({ player_a, stat_a, player_b, stat_b, sample_size: 0, phi_coefficient: null, outcomes: [], }); } // Calculate phi coefficient from joint outcomes let both_hit = 0, a_only = 0, b_only = 0, neither = 0; for (const row of data) { if (row.a_hit && row.b_hit) both_hit++; else if (row.a_hit && !row.b_hit) a_only++; else if (!row.a_hit && row.b_hit) b_only++; else neither++; } const n = data.length; const num = (both_hit * neither) - (a_only * b_only); const denom = Math.sqrt( (both_hit + a_only) * (b_only + neither) * (both_hit + b_only) * (a_only + neither) ); const phi = denom === 0 ? 0 : num / denom; res.json({ player_a, stat_a, player_b, stat_b, sample_size: n, phi_coefficient: Math.round(phi * 1000) / 1000, outcomes: data, }); } catch (err) { console.error('[props/joint-history]', err.message); res.status(503).json({ error: 'Service temporarily unavailable' }); } }); module.exports = router;