# Vote Tracking API Design

## Overview

Extend the REST and WebSocket APIs so clients can track votes at both session and global levels. The primary consumer is a stream overlay (ticker-style display) that already has the admin JWT.

## Approach

**Approach B — Split by resource ownership.** Session-scoped vote data lives under the session resource. Global vote history lives under the vote resource. The WebSocket emits real-time events for live votes only.

## WebSocket: `vote.received` Event

**Trigger:** `POST /api/votes/live` — fires after the vote transaction succeeds, before the HTTP response. Only live votes emit this event; chat-import does not.

**Broadcast target:** Session subscribers via `broadcastEvent('vote.received', data, sessionId)`.

**Payload:**

```json
{
  "type": "vote.received",
  "timestamp": "2026-03-15T20:30:00.000Z",
  "data": {
    "sessionId": 5,
    "game": {
      "id": 42,
      "title": "Quiplash 3",
      "pack_name": "Party Pack 7"
    },
    "vote": {
      "username": "viewer123",
      "type": "up",
      "timestamp": "2026-03-15T20:29:55.000Z"
    },
    "totals": {
      "upvotes": 14,
      "downvotes": 3,
      "popularity_score": 11
    }
  }
}
```

**Implementation notes:**
- `votes.js` needs access to the WebSocket manager singleton via `getWebSocketManager()`.
- The existing session games JOIN needs to select `pack_name` from the `games` table.

## REST: `GET /api/sessions/:id/votes`

Per-game vote breakdown for a specific session.

**Location:** `backend/routes/sessions.js`

**Auth:** None (matches `GET /api/sessions/:id/games`).

**Response:**

```json
{
  "session_id": 5,
  "votes": [
    {
      "game_id": 42,
      "title": "Quiplash 3",
      "pack_name": "Party Pack 7",
      "upvotes": 14,
      "downvotes": 3,
      "net_score": 11,
      "total_votes": 17
    }
  ]
}
```

**Query:**

```sql
SELECT
  lv.game_id,
  g.title,
  g.pack_name,
  SUM(CASE WHEN lv.vote_type = 1 THEN 1 ELSE 0 END) AS upvotes,
  SUM(CASE WHEN lv.vote_type = -1 THEN 1 ELSE 0 END) AS downvotes,
  SUM(lv.vote_type) AS net_score,
  COUNT(*) AS total_votes
FROM live_votes lv
JOIN games g ON lv.game_id = g.id
WHERE lv.session_id = ?
GROUP BY lv.game_id
ORDER BY net_score DESC
```

**Error handling:**
- Session not found → 404
- Session exists, no votes → 200 with empty `votes` array

## REST: `GET /api/votes`

Paginated global vote history with flexible filtering.

**Location:** `backend/routes/votes.js`

**Auth:** None.

**Query parameters:**

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `session_id` | integer | — | Filter by session |
| `game_id` | integer | — | Filter by game |
| `username` | string | — | Filter by voter |
| `vote_type` | `up` or `down` | — | Filter by direction |
| `page` | integer | 1 | Page number |
| `limit` | integer | 50 | Results per page (max 100) |

**Response:**

```json
{
  "votes": [
    {
      "id": 891,
      "session_id": 5,
      "game_id": 42,
      "game_title": "Quiplash 3",
      "pack_name": "Party Pack 7",
      "username": "viewer123",
      "vote_type": "up",
      "timestamp": "2026-03-15T20:29:55.000Z",
      "created_at": "2026-03-15T20:29:56.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 237,
    "total_pages": 5
  }
}
```

**Design notes:**
- `vote_type` returned as `"up"` / `"down"`, not raw `1` / `-1`.
- `game_title` and `pack_name` included via JOIN.
- Ordered by `timestamp DESC`.
- `limit` capped at 100 server-side.

**Error handling:**
- Invalid filter values → 400
- No results → 200 with empty array and `total: 0`

## Testing Strategy

### Phase 1: Regression tests (pre-implementation)

Written and passing before any code changes to lock down existing behavior.

**`tests/api/regression-votes-live.test.js`** — existing `POST /api/votes/live`:
- Returns 200 with correct response shape (`success`, `session`, `game`, `vote`)
- `game` includes `id`, `title`, `upvotes`, `downvotes`, `popularity_score`
- Increments `upvotes`/`popularity_score` for upvote
- Increments `downvotes`/decrements `popularity_score` for downvote
- 400 for missing fields, invalid vote value, invalid timestamp
- 404 when no active session or timestamp doesn't match a game
- 409 for duplicate within 1-second window
- 401 without JWT

**`tests/api/regression-games.test.js`** — game aggregate fields:
- `GET /api/games` returns `upvotes`, `downvotes`, `popularity_score`
- `GET /api/games/:id` returns same fields
- Aggregates accurate after votes

**`tests/api/regression-sessions.test.js`** — session endpoints:
- `GET /api/sessions/:id` returns session object
- `GET /api/sessions/:id` returns 404 for nonexistent session
- `GET /api/sessions/:id/games` returns game list with expected shape

**`tests/api/regression-websocket.test.js`** — existing WebSocket events:
- Auth flow (auth → auth_success)
- Subscribe/unsubscribe flow
- `session.started` broadcast on session create
- `session.ended` broadcast on session close
- `game.added` broadcast on game add

### Phase 2: New feature tests (TDD — written before implementation)

- **`tests/api/votes-get.test.js`** — `GET /api/votes` history endpoint
- **`tests/api/sessions-votes.test.js`** — `GET /api/sessions/:id/votes` breakdown
- **`tests/api/votes-live-websocket.test.js`** — `vote.received` WebSocket event

### Workflow

1. Write Phase 1 regression tests → run → all green
2. Write Phase 2 feature tests → run → all red
3. Implement features
4. Run all tests → Phase 1 still green, Phase 2 now green
