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,74 @@
|
||||
const { gradeParlayFromLegs } = require('../../src/services/parlayGrader');
|
||||
|
||||
function makeLeg(grade, confidence, composite) {
|
||||
return { grade, confidence, _composite: composite };
|
||||
}
|
||||
|
||||
describe('parlayGrader', () => {
|
||||
test('grade A: high composite, no D legs, no major_negative', () => {
|
||||
const legs = [makeLeg('A', 85, 3.5), makeLeg('A', 90, 3.0), makeLeg('B', 72, 2.0)];
|
||||
const result = gradeParlayFromLegs(legs, []);
|
||||
expect(result.grade).toBe('A');
|
||||
expect(result.confidence).toBeGreaterThanOrEqual(80);
|
||||
});
|
||||
|
||||
test('grade B: moderate composite, at most 1 D leg', () => {
|
||||
const legs = [makeLeg('B', 70, 2.5), makeLeg('B', 65, 2.0), makeLeg('D', 35, 0.3)];
|
||||
const result = gradeParlayFromLegs(legs, []);
|
||||
expect(result.grade).toBe('B');
|
||||
});
|
||||
|
||||
test('grade C: low positive composite', () => {
|
||||
const legs = [makeLeg('C', 55, 0.8), makeLeg('C', 50, 0.6)];
|
||||
const result = gradeParlayFromLegs(legs, []);
|
||||
expect(result.grade).toBe('C');
|
||||
});
|
||||
|
||||
test('grade D: negative composite', () => {
|
||||
const legs = [makeLeg('D', 35, 0.1), makeLeg('D', 30, -0.5)];
|
||||
const result = gradeParlayFromLegs(legs, []);
|
||||
expect(result.grade).toBe('D');
|
||||
});
|
||||
|
||||
test('2+ D legs forces grade D', () => {
|
||||
const legs = [makeLeg('A', 90, 4.0), makeLeg('D', 35, 0.2), makeLeg('D', 30, 0.1)];
|
||||
const result = gradeParlayFromLegs(legs, []);
|
||||
expect(result.grade).toBe('D');
|
||||
});
|
||||
|
||||
test('major_negative caps grade at B', () => {
|
||||
const legs = [makeLeg('A', 90, 4.0), makeLeg('A', 85, 3.5)];
|
||||
const flags = [{ type: 'same_player_conflicting', legs: [0, 1], impact: 'major_negative' }];
|
||||
const result = gradeParlayFromLegs(legs, flags);
|
||||
expect(['B', 'C', 'D']).toContain(result.grade);
|
||||
});
|
||||
|
||||
test('minor_negative reduces composite by 0.3 each', () => {
|
||||
const legs = [makeLeg('B', 70, 2.0), makeLeg('B', 68, 1.8)];
|
||||
const withoutFlags = gradeParlayFromLegs(legs, []);
|
||||
const withFlags = gradeParlayFromLegs(legs, [
|
||||
{ type: 'same_game_same_team', legs: [0, 1], impact: 'minor_negative' },
|
||||
]);
|
||||
expect(withFlags.composite).toBeCloseTo(withoutFlags.composite - 0.3, 1);
|
||||
});
|
||||
|
||||
test('confidence adjusted: -5 per minor, -15 per major', () => {
|
||||
const legs = [makeLeg('B', 80, 2.0), makeLeg('B', 80, 2.0)];
|
||||
const noFlags = gradeParlayFromLegs(legs, []);
|
||||
const minorFlag = gradeParlayFromLegs(legs, [
|
||||
{ type: 'test', impact: 'minor_negative' },
|
||||
]);
|
||||
const majorFlag = gradeParlayFromLegs(legs, [
|
||||
{ type: 'test', impact: 'major_negative' },
|
||||
]);
|
||||
expect(minorFlag.confidence).toBe(noFlags.confidence - 5);
|
||||
expect(majorFlag.confidence).toBe(noFlags.confidence - 15);
|
||||
});
|
||||
|
||||
test('confidence clamped to 30-95', () => {
|
||||
const legs = [makeLeg('D', 35, 0.1)];
|
||||
const manyFlags = Array(5).fill({ type: 'test', impact: 'major_negative' });
|
||||
const result = gradeParlayFromLegs(legs, manyFlags);
|
||||
expect(result.confidence).toBe(30);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user