turniere-backend/doc/tournament_transaction_log_...

5.7 KiB

Tournament Transaction Log API

Purpose

Tournament transaction log is owner-readable tournament history.

It records ordered tournament events such as:

  • tournament creation
  • timer changes
  • match state changes
  • match score changes
  • decider match creation
  • playoff start attempts and failures
  • playoff start success
  • group match auto-starts

Frontend should show this as an append-only history feed for tournament owners.

Read Flow

Transaction log is not embedded in normal tournament payload. Read it from dedicated owner-only endpoint:

  • GET /tournaments/:id/transaction_log

Auth:

  • same Devise token auth headers as other owner APIs
  • endpoint requires authenticated tournament owner

Response:

  • 200 OK
  • JSON array
  • ordered by sequence ascending
  • no pagination yet

Example:

[
  {
    "id": 17,
    "sequence": 1,
    "action": "tournament.created",
    "message": "owner created tournament Spring Cup(123)",
    "metadata": {
      "tournament_id": 123,
      "tournament_name": "Spring Cup"
    },
    "created_at": "2026-04-30T10:12:42Z",
    "user": {
      "id": 4,
      "username": "owner"
    }
  },
  {
    "id": 18,
    "sequence": 2,
    "action": "match.state_changed",
    "message": "owner changed match 77 (Team A(10) vs Team B(11)) to running",
    "metadata": {
      "match_id": 77,
      "stage_type": "playoffs",
      "old_state": "not_started",
      "new_state": "in_progress",
      "team_ids": [10, 11],
      "teams": [
        { "id": 10, "name": "Team A" },
        { "id": 11, "name": "Team B" }
      ],
      "winner_team_id": null,
      "advanced_match_ids": [],
      "group_scores": []
    },
    "created_at": "2026-04-30T10:13:01Z",
    "user": {
      "id": 4,
      "username": "owner"
    }
  }
]

Field Contract

Each entry has:

  • id: DB id for entry
  • sequence: per-tournament monotonic order number, starts at 1
  • action: machine-readable action key
  • message: human-readable owner history text
  • metadata: structured action-specific data
  • created_at: ISO-8601 timestamp
  • user: actor object, or null for system actions

user shape:

{
  "id": 4,
  "username": "owner"
}

Frontend display guidance:

  • sort by sequence, not by timestamp
  • use message for first UI pass
  • use metadata for links/details/chips
  • tolerate unknown future action values
  • tolerate extra metadata fields

Current Action Keys

Current action values:

  • tournament.created
  • tournament.timer_changed
  • match.state_changed
  • match_score.updated
  • decider_match.created
  • playoffs.start_failed
  • playoffs.started
  • group_matches.started

Future backend work may add more action keys. Unknown actions should still render via message.

Metadata Notes

metadata is stable JSON, but shape depends on action.

Common match metadata includes:

  • match_id
  • stage_id
  • group_id
  • stage_type: groupstage or playoffs
  • team_ids
  • teams
  • winner_team_id
  • advanced_match_ids
  • group_scores

Team object shape:

{
  "id": 10,
  "name": "Team A"
}

Group score object shape:

{
  "team_id": 10,
  "team_name": "Team A",
  "points": 9,
  "matches_played": 3,
  "wins": 3,
  "draws": 0,
  "losses": 0,
  "score": 12,
  "score_against": 4,
  "score_difference": 8,
  "position": 1
}

Example Actions

Match state change

{
  "action": "match.state_changed",
  "message": "owner changed match 77 to finished (Team A(10) wins) - playoffs - Team A(10) advances to match 80",
  "metadata": {
    "match_id": 77,
    "stage_type": "playoffs",
    "old_state": "in_progress",
    "new_state": "finished",
    "winner_team_id": 10,
    "advanced_match_ids": [80]
  }
}

Match score update

{
  "action": "match_score.updated",
  "message": "owner changed matchscore 91 from match 77 to 3 (3 points for Team A(10) in match vs Team B(11))",
  "metadata": {
    "match_id": 77,
    "match_score_id": 91,
    "team_id": 10,
    "changes": {
      "points": {
        "from": 2,
        "to": 3
      }
    }
  }
}

Playoff start failure

{
  "action": "playoffs.start_failed",
  "message": "owner attempted playoff start, failed because decider missing",
  "metadata": {
    "stage_id": 12,
    "reason": "decider_missing",
    "blocking_ties": [
      {
        "group_id": 5,
        "team_ids": [10, 11],
        "teams": [
          { "id": 10, "name": "Team A" },
          { "id": 11, "name": "Team B" }
        ]
      }
    ]
  }
}

Playoffs started

{
  "action": "playoffs.started",
  "message": "owner started playoffs, advancing teams: Team A(10), Team B(11), Team C(12), Team D(13)",
  "metadata": {
    "stage_id": 12,
    "advancing_team_ids": [10, 11, 12, 13],
    "advancing_teams": [
      { "id": 10, "name": "Team A" },
      { "id": 11, "name": "Team B" },
      { "id": 12, "name": "Team C" },
      { "id": 13, "name": "Team D" }
    ]
  }
}

Error Behavior

Expected errors:

  • 401 Unauthorized: missing or invalid auth
  • 403 Forbidden: authenticated user is not tournament owner
  • 404 Not Found: tournament id does not exist

Read-only follower tournaments:

  • log read is allowed for owner
  • log write is internal only; frontend has no write endpoint

Frontend Implementation Guide

Recommended owner-history flow:

  1. Load tournament as usual.
  2. If current user is owner, request GET /tournaments/:id/transaction_log.
  3. Render entries sorted by sequence.
  4. Use message as main text.
  5. Show created_at as event time.
  6. Link matches/teams from metadata when ids are present.
  7. Refresh after owner mutations, or poll while history panel is open.

There is no websocket stream for transaction log yet. Existing tournament/match websocket updates still fire for live tournament state.