Spaces:
Paused
Paused
Download src/oddsApi.js from Shaurya2020/paddock-api: direct link, hf CLI and curl.
- Browser
- Download file 4.48 kB
-
https://huggingface.co/spaces/Shaurya2020/paddock-api/resolve/main/src/oddsApi.js
- Command line
-
hf download hf://spaces/Shaurya2020/paddock-api/src/oddsApi.js
-
curl -L -o oddsApi.js https://huggingface.co/spaces/Shaurya2020/paddock-api/resolve/main/src/oddsApi.js
4.48 kB
| const { Router } = require('express'); | |
| const { db, newId, h, bad, conflict, loadEvent, currentOdds, participants } = require('./core.js'); | |
| const r = Router({ mergeParams: true }); | |
| /* 1 ββ GET /race-events/:id/odds β the live board βββββββββββββββββββββββββ */ | |
| r.get('/', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| const field = participants(e.id).filter(p => !p.is_scratched && p.decimal_odds); | |
| const book = field.reduce((s, p) => s + 1 / p.decimal_odds, 0) * 100; | |
| res.json({ data: { | |
| event_id: e.id, | |
| version: e.current_odds_version, | |
| book_percentage: +book.toFixed(2), | |
| margin: +(book - 100).toFixed(2), | |
| prices: participants(e.id).map(p => ({ | |
| participant_id: p.id, cloth_no: p.cloth_no, horse_name: p.horse_name, | |
| decimal_odds: p.decimal_odds, previous_odds: p.previous_odds, is_scratched: p.is_scratched | |
| })) | |
| }}); | |
| })); | |
| /* 2 ββ POST /race-events/:id/odds β write a new version βββββββββββββββββββ */ | |
| r.post('/', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| if (!['draft', 'open'].includes(e.status)) | |
| conflict(`Odds are locked once a race is ${e.status}.`); | |
| const incoming = (req.body || {}).odds; | |
| if (!Array.isArray(incoming) || !incoming.length) bad('odds must be a non-empty array.'); | |
| const valid = new Set(db.prepare('SELECT id FROM race_participants WHERE event_id = ?').all(e.id).map(p => p.id)); | |
| for (const o of incoming) { | |
| if (!valid.has(o.participant_id)) bad('Unknown participant_id.', { participant_id: o.participant_id }); | |
| if (!(o.decimal_odds > 1)) bad('decimal_odds must be greater than 1.00.', { participant_id: o.participant_id, decimal_odds: o.decimal_odds }); | |
| } | |
| const prev = Object.fromEntries(currentOdds(e.id).map(o => [o.participant_id, o.decimal_odds])); | |
| const nextNo = e.current_odds_version + 1; | |
| // One transaction: a half-written version would leave the board inconsistent. | |
| db.exec('BEGIN'); | |
| try { | |
| const vid = newId('ver'); | |
| db.prepare(`INSERT INTO odds_versions (id, event_id, version_no, created_by, note) VALUES (?,?,?,?,?)`) | |
| .run(vid, e.id, nextNo, req.admin.id, (req.body.note || '').trim() || null); | |
| // Carry forward every runner so each version is a complete board, not a delta. | |
| const merged = { ...prev }; | |
| for (const o of incoming) merged[o.participant_id] = o.decimal_odds; | |
| const changes = []; | |
| for (const [pid, price] of Object.entries(merged)) { | |
| db.prepare(`INSERT INTO odds_entries (id, version_id, participant_id, decimal_odds, previous_odds) | |
| VALUES (?,?,?,?,?)`).run(newId('oe'), vid, pid, price, prev[pid] ?? null); | |
| if (prev[pid] != null && Math.abs(prev[pid] - price) > 1e-9) | |
| changes.push({ participant_id: pid, from: prev[pid], to: price }); | |
| } | |
| db.prepare(`UPDATE race_events SET current_odds_version = ?, updated_at = datetime('now') WHERE id = ?`) | |
| .run(nextNo, e.id); | |
| db.exec('COMMIT'); | |
| res.status(201).json({ data: { event_id: e.id, version: nextNo, changes } }); | |
| } catch (err) { | |
| db.exec('ROLLBACK'); | |
| throw err; | |
| } | |
| })); | |
| /* 3 ββ GET /race-events/:id/odds/history β the audit trail ββββββββββββββββ */ | |
| r.get('/history', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| const versions = db.prepare(` | |
| SELECT ov.id, ov.version_no, ov.note, ov.created_at, a.name AS created_by | |
| FROM odds_versions ov JOIN admins a ON a.id = ov.created_by | |
| WHERE ov.event_id = ? ORDER BY ov.version_no DESC`).all(e.id); | |
| const entries = db.prepare(` | |
| SELECT oe.version_id, oe.decimal_odds, oe.previous_odds, p.horse_name, p.cloth_no | |
| FROM odds_entries oe JOIN race_participants p ON p.id = oe.participant_id | |
| WHERE oe.version_id IN (SELECT id FROM odds_versions WHERE event_id = ?)`).all(e.id); | |
| res.json({ data: versions.map(v => ({ | |
| version: v.version_no, note: v.note, created_by: v.created_by, created_at: v.created_at, | |
| is_current: v.version_no === e.current_odds_version, | |
| changes: entries | |
| .filter(x => x.version_id === v.id && x.previous_odds != null | |
| && Math.abs(x.previous_odds - x.decimal_odds) > 1e-9) | |
| .map(x => ({ cloth_no: x.cloth_no, horse_name: x.horse_name, from: x.previous_odds, to: x.decimal_odds })) | |
| .sort((a, b) => a.cloth_no - b.cloth_no) | |
| }))}); | |
| })); | |
| module.exports = r; | |