feat: Feature 1.5 — Bet Submission with 3 methods + performance tracking
Three submission methods: - POST /api/bets/quickslip — structured bet entry - POST /api/bets/screenshot — stub OCR with confirm flow - POST /api/bets/sync — coming soon stub Full bet lifecycle: - PATCH /api/bets/:id/settle — settle with outcome, recalculates performance - GET /api/bets — list with status/book/pagination filters - GET /api/bets/performance — ROI, win rate, profit (weekly/monthly/all_time) Payout calculator handles straight bets (American odds) and parlays (multiplied leg payouts). Performance service recalculates on each settlement and upserts into performance table. 33 new tests, 221 total (194 Node.js + 27 Python), all passing. All backend features for Phase 1 + Phase 2 now complete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
const { computeStats, getWeekStart } = require('../../src/services/performanceService');
|
||||
|
||||
describe('performanceService', () => {
|
||||
describe('computeStats', () => {
|
||||
test('calculates ROI correctly from won/lost mix', () => {
|
||||
const bets = [
|
||||
{ amount: '20', potential_payout: '38.18', status: 'won', settled_at: '2026-03-21' },
|
||||
{ amount: '20', potential_payout: '38.18', status: 'lost', settled_at: '2026-03-21' },
|
||||
{ amount: '20', potential_payout: '38.18', status: 'won', settled_at: '2026-03-21' },
|
||||
];
|
||||
const stats = computeStats(bets);
|
||||
// Won: 18.18 + 18.18 = 36.36 profit, Lost: -20 = -20
|
||||
// Total profit: 16.36, Total wagered: 60
|
||||
// ROI: 16.36/60 * 100 = 27.3%
|
||||
expect(stats.roi).toBeCloseTo(27.3, 0);
|
||||
expect(stats.total_wagered).toBe(60);
|
||||
expect(stats.total_profit).toBeCloseTo(16.36, 1);
|
||||
});
|
||||
|
||||
test('win rate = won / (won + lost), excludes push', () => {
|
||||
const bets = [
|
||||
{ amount: '20', potential_payout: '38', status: 'won', settled_at: '2026-03-21' },
|
||||
{ amount: '20', potential_payout: '38', status: 'lost', settled_at: '2026-03-21' },
|
||||
{ amount: '20', potential_payout: '38', status: 'push', settled_at: '2026-03-21' },
|
||||
];
|
||||
const stats = computeStats(bets);
|
||||
expect(stats.win_rate).toBe(50);
|
||||
expect(stats.sample_size).toBe(3);
|
||||
});
|
||||
|
||||
test('empty bets returns zeroes', () => {
|
||||
const stats = computeStats([]);
|
||||
expect(stats.roi).toBe(0);
|
||||
expect(stats.win_rate).toBe(0);
|
||||
expect(stats.sample_size).toBe(0);
|
||||
expect(stats.total_wagered).toBe(0);
|
||||
expect(stats.total_profit).toBe(0);
|
||||
});
|
||||
|
||||
test('all losses gives negative ROI', () => {
|
||||
const bets = [
|
||||
{ amount: '20', potential_payout: '38', status: 'lost', settled_at: '2026-03-21' },
|
||||
{ amount: '30', potential_payout: '57', status: 'lost', settled_at: '2026-03-21' },
|
||||
];
|
||||
const stats = computeStats(bets);
|
||||
expect(stats.roi).toBe(-100);
|
||||
expect(stats.win_rate).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWeekStart', () => {
|
||||
test('returns Monday for a Wednesday', () => {
|
||||
// 2026-03-18 is a Wednesday
|
||||
const ws = getWeekStart(new Date('2026-03-18T12:00:00Z'));
|
||||
expect(ws.getUTCDay()).toBe(1); // Monday
|
||||
expect(ws.toISOString().split('T')[0]).toBe('2026-03-16');
|
||||
});
|
||||
|
||||
test('returns Monday for a Monday', () => {
|
||||
const ws = getWeekStart(new Date('2026-03-16T12:00:00Z'));
|
||||
expect(ws.toISOString().split('T')[0]).toBe('2026-03-16');
|
||||
});
|
||||
|
||||
test('returns previous Monday for a Sunday', () => {
|
||||
// 2026-03-22 is a Sunday
|
||||
const ws = getWeekStart(new Date('2026-03-22T12:00:00Z'));
|
||||
expect(ws.toISOString().split('T')[0]).toBe('2026-03-16');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user