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:
@@ -0,0 +1,66 @@
|
||||
const express = require('express');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { scanParlay } = require('../services/parlayScanService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const VALID_STAT_TYPES = new Set([
|
||||
'points', 'rebounds', 'assists', 'threes', 'blocks',
|
||||
'steals', 'pra', 'turnovers',
|
||||
]);
|
||||
const VALID_DIRECTIONS = new Set(['over', 'under']);
|
||||
|
||||
function validateLegs(legs) {
|
||||
if (!Array.isArray(legs) || legs.length < 2) {
|
||||
return 'legs array is required with at least 2 props';
|
||||
}
|
||||
if (legs.length > 12) {
|
||||
return 'max_legs_exceeded';
|
||||
}
|
||||
for (let i = 0; i < legs.length; i++) {
|
||||
const leg = legs[i];
|
||||
if (!leg.player) return `leg ${i}: player is required`;
|
||||
if (!leg.stat_type) return `leg ${i}: stat_type is required`;
|
||||
if (leg.stat_type && !VALID_STAT_TYPES.has(leg.stat_type)) {
|
||||
return `leg ${i}: Invalid stat_type: ${leg.stat_type}`;
|
||||
}
|
||||
if (leg.line == null) return `leg ${i}: line is required`;
|
||||
if (!leg.direction) return `leg ${i}: direction is required`;
|
||||
if (leg.direction && !VALID_DIRECTIONS.has(leg.direction)) {
|
||||
return `leg ${i}: Invalid direction: ${leg.direction}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
router.post('/parlay', requireAuth, async (req, res) => {
|
||||
const { legs } = req.body;
|
||||
|
||||
const validationError = validateLegs(legs);
|
||||
if (validationError === 'max_legs_exceeded') {
|
||||
return res.status(422).json({ error: 'Maximum 12 legs per parlay' });
|
||||
}
|
||||
if (validationError) {
|
||||
return res.status(400).json({ error: validationError });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await scanParlay(req.user, legs);
|
||||
|
||||
if (result.blocked) {
|
||||
return res.status(403).json({
|
||||
error: 'scan_limit_reached',
|
||||
scan_count: result.scan_count,
|
||||
scans_remaining: result.scans_remaining,
|
||||
upgrade_pitch: result.upgrade_pitch,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
console.error('[BetonBLK] Scan error:', err.message);
|
||||
return res.status(503).json({ error: 'Scan service temporarily unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user