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,131 @@
|
||||
const express = require('express');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { createBet, createBetFromScreenshot, settleBet, listBets } = require('../services/betService');
|
||||
const { extractFromScreenshot } = require('../services/ocrStub');
|
||||
const { recalculatePerformance } = require('../services/performanceService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const VALID_BET_TYPES = new Set(['straight', 'parlay', 'teaser', 'round_robin']);
|
||||
const VALID_SETTLE_STATUSES = new Set(['won', 'lost', 'push', 'void']);
|
||||
|
||||
function validateQuickslip(body) {
|
||||
if (!Array.isArray(body.legs) || body.legs.length === 0) return 'legs array is required';
|
||||
if (body.amount == null || body.amount <= 0) return 'amount is required and must be positive';
|
||||
if (!body.book) return 'book is required';
|
||||
if (!body.bet_type) return 'bet_type is required';
|
||||
if (!VALID_BET_TYPES.has(body.bet_type)) return `Invalid bet_type: ${body.bet_type}`;
|
||||
for (let i = 0; i < body.legs.length; i++) {
|
||||
const leg = body.legs[i];
|
||||
if (!leg.player) return `leg ${i}: player is required`;
|
||||
if (!leg.stat_type) return `leg ${i}: stat_type is required`;
|
||||
if (leg.line == null) return `leg ${i}: line is required`;
|
||||
if (!leg.direction) return `leg ${i}: direction is required`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// POST /api/bets/quickslip
|
||||
router.post('/quickslip', requireAuth, async (req, res) => {
|
||||
const error = validateQuickslip(req.body);
|
||||
if (error) return res.status(400).json({ error });
|
||||
|
||||
try {
|
||||
const result = await createBet(req.user.id, req.body);
|
||||
return res.status(201).json(result);
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404) return res.status(404).json({ error: err.message });
|
||||
console.error('[BetonBLK] Quickslip error:', err.message);
|
||||
return res.status(503).json({ error: 'Bet submission failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/bets/screenshot
|
||||
router.post('/screenshot', requireAuth, async (req, res) => {
|
||||
// For MVP: accept the request but return stub extraction
|
||||
const book = req.body?.book || req.query?.book || 'unknown';
|
||||
|
||||
// In a real implementation, we'd parse multipart form data and process the image
|
||||
// For MVP, just return the stub
|
||||
const extracted = extractFromScreenshot(book);
|
||||
|
||||
return res.status(201).json({
|
||||
bet_id: null,
|
||||
status: 'pending_confirmation',
|
||||
extracted,
|
||||
needs_confirmation: true,
|
||||
message: extracted.message,
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/bets/screenshot/confirm
|
||||
router.post('/screenshot/confirm', requireAuth, async (req, res) => {
|
||||
const error = validateQuickslip(req.body);
|
||||
if (error) return res.status(400).json({ error });
|
||||
|
||||
try {
|
||||
const result = await createBetFromScreenshot(req.user.id, req.body);
|
||||
return res.status(201).json(result);
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404) return res.status(404).json({ error: err.message });
|
||||
console.error('[BetonBLK] Screenshot confirm error:', err.message);
|
||||
return res.status(503).json({ error: 'Bet submission failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/bets/sync
|
||||
router.post('/sync', requireAuth, async (req, res) => {
|
||||
return res.json({
|
||||
status: 'coming_soon',
|
||||
message: 'Sportsbook sync is coming soon. Use quick slip or screenshot for now.',
|
||||
supported_books: ['draftkings', 'fanduel', 'betmgm'],
|
||||
});
|
||||
});
|
||||
|
||||
// PATCH /api/bets/:id/settle
|
||||
router.patch('/:id/settle', requireAuth, async (req, res) => {
|
||||
const { status, leg_outcomes } = req.body;
|
||||
|
||||
if (!status || !VALID_SETTLE_STATUSES.has(status)) {
|
||||
return res.status(400).json({ error: `Invalid status. Must be one of: ${[...VALID_SETTLE_STATUSES].join(', ')}` });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await settleBet(req.user.id, req.params.id, { status, leg_outcomes });
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404) return res.status(404).json({ error: err.message });
|
||||
if (err.statusCode === 422) return res.status(422).json({ error: err.message });
|
||||
console.error('[BetonBLK] Settle error:', err.message);
|
||||
return res.status(503).json({ error: 'Settlement failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/bets
|
||||
router.get('/', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await listBets(req.user.id, {
|
||||
status: req.query.status || null,
|
||||
book: req.query.book || null,
|
||||
limit: Math.min(parseInt(req.query.limit) || 20, 100),
|
||||
offset: parseInt(req.query.offset) || 0,
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
console.error('[BetonBLK] List bets error:', err.message);
|
||||
return res.status(503).json({ error: 'Failed to fetch bets' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/bets/performance
|
||||
router.get('/performance', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await recalculatePerformance(req.user.id);
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
console.error('[BetonBLK] Performance error:', err.message);
|
||||
return res.status(503).json({ error: 'Failed to calculate performance' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user