paddock-api / src /raceEventsApi.js
Shaurya2020's picture
Race events: 6 endpoints
461e021 verified
Raw History Blame Contribute Delete
6.29 kB
const { Router } = require('express');
const { db, newId, h, bad, conflict, scope, ownerFor, loadEvent, participants } = require('./core.js');
const r = Router();
/** Legal lifecycle moves. Anything not listed here is rejected. */
const TRANSITIONS = {
draft: ['open', 'void'],
open: ['closed', 'void'],
closed: ['resulted', 'void', 'open'], // reopening is allowed while nothing is settled
resulted: ['settled', 'void', 'closed'], // result can be corrected before settlement
settled: [], // terminal β€” money has moved
void: [] // terminal
};
const shape = e => ({
id: e.id, owner_admin_id: e.owner_admin_id, name: e.name, venue: e.venue,
race_no: e.race_no, distance_m: e.distance_m, going: e.going, race_class: e.race_class,
post_time: e.post_time, status: e.status, odds_version: e.current_odds_version,
void_reason: e.void_reason, created_at: e.created_at
});
/* 1 ── GET /race-events ─────────────────────────────────────────────────── */
r.get('/', h((req, res) => {
const s = scope(req.admin, 'e');
const where = [s.sql], params = [...s.params];
if (req.query.status) { where.push('e.status = ?'); params.push(req.query.status); }
if (req.query.venue) { where.push('e.venue LIKE ?'); params.push(`%${req.query.venue}%`); }
const rows = db.prepare(`
SELECT e.*,
(SELECT count(*) FROM race_participants p WHERE p.event_id = e.id AND p.is_scratched = 0) AS runners,
(SELECT coalesce(sum(b.stake),0) FROM bets b WHERE b.event_id = e.id AND b.status <> 'cancelled') AS held
FROM race_events e
WHERE ${where.join(' AND ')}
ORDER BY e.post_time`).all(...params);
res.json({ data: rows.map(e => ({ ...shape(e), runners: e.runners, total_staked: e.held })) });
}));
/* 2 ── GET /race-events/:id ─────────────────────────────────────────────── */
r.get('/:id', h((req, res) => {
const e = loadEvent(req.admin, req.params.id);
res.json({ data: { ...shape(e), field: participants(e.id) } });
}));
/* 3 ── POST /race-events ────────────────────────────────────────────────── */
r.post('/', h((req, res) => {
const b = req.body || {};
if (!b.name) bad('name is required.');
if (!b.venue) bad('venue is required.');
if (!b.distance_m) bad('distance_m is required.');
if (!b.post_time) bad('post_time is required.');
const id = newId('evt');
db.prepare(`INSERT INTO race_events
(id, owner_admin_id, name, venue, race_no, distance_m, going, race_class, post_time, status)
VALUES (?,?,?,?,?,?,?,?,?, 'draft')`)
.run(id, ownerFor(req.admin, b.owner_admin_id), b.name, b.venue,
b.race_no ?? 1, b.distance_m, b.going ?? null, b.race_class ?? null, b.post_time);
// Runners can arrive with the race or be added later.
for (const p of b.field || []) {
db.prepare(`INSERT INTO race_participants
(id, event_id, cloth_no, horse_name, jockey_id, team_id, carried_weight, recent_form)
VALUES (?,?,?,?,?,?,?,?)`)
.run(`${id}_p${p.cloth_no}`, id, p.cloth_no, p.horse_name,
p.jockey_id ?? null, p.team_id ?? null, p.carried_weight ?? null, p.recent_form ?? null);
}
res.status(201).json({ data: { ...shape(loadEvent(req.admin, id)), field: participants(id) } });
}));
/* 4 ── PUT /race-events/:id ─────────────────────────────────────────────── */
r.put('/:id', h((req, res) => {
const e = loadEvent(req.admin, req.params.id);
if (['settled', 'void'].includes(e.status)) conflict(`A ${e.status} race cannot be edited.`);
const allowed = ['name', 'venue', 'race_no', 'distance_m', 'going', 'race_class', 'post_time'];
const sets = [], params = [];
for (const k of allowed) if (k in (req.body || {})) { sets.push(`${k} = ?`); params.push(req.body[k]); }
if (!sets.length) bad('No editable fields supplied.', { allowed });
sets.push(`updated_at = datetime('now')`);
db.prepare(`UPDATE race_events SET ${sets.join(', ')} WHERE id = ?`).run(...params, e.id);
res.json({ data: shape(loadEvent(req.admin, e.id)) });
}));
/* 5 ── PATCH /race-events/:id/status ────────────────────────────────────── */
r.patch('/:id/status', h((req, res) => {
const e = loadEvent(req.admin, req.params.id);
const next = (req.body || {}).status;
if (!next) bad('status is required.');
const legal = TRANSITIONS[e.status] || [];
if (!legal.includes(next))
conflict(`Cannot move a race from ${e.status} to ${next}.`);
// Opening betting on a race with no priced runners would take money on nothing.
if (next === 'open') {
const priced = db.prepare(`
SELECT count(*) n FROM race_participants p
JOIN odds_entries oe ON oe.participant_id = p.id
JOIN odds_versions ov ON ov.id = oe.version_id AND ov.version_no = ?
WHERE p.event_id = ? AND p.is_scratched = 0`).get(e.current_odds_version, e.id).n;
if (priced < 2) conflict('At least two priced runners are needed before betting can open.');
}
db.prepare(`UPDATE race_events SET status = ?, updated_at = datetime('now') WHERE id = ?`).run(next, e.id);
res.json({ data: shape(loadEvent(req.admin, e.id)) });
}));
/* 6 ── DELETE /race-events/:id ──────────────────────────────────────────── */
r.delete('/:id', h((req, res) => {
const e = loadEvent(req.admin, req.params.id);
const bets = db.prepare(`SELECT count(*) n FROM bets WHERE event_id = ?`).get(e.id).n;
if (bets) conflict(`This race has ${bets} bets against it. Void it instead of deleting.`);
if (e.status !== 'draft') conflict('Only a draft race can be deleted.');
db.prepare('DELETE FROM race_events WHERE id = ?').run(e.id);
res.json({ data: { id: e.id, deleted: true } });
}));
module.exports = r;