File size: 4,478 Bytes
6a44b15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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;