File size: 6,287 Bytes
461e021
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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;