282 lines
5.7 KiB
Markdown
282 lines
5.7 KiB
Markdown
# 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:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"id": 10,
|
|
"name": "Team A"
|
|
}
|
|
```
|
|
|
|
Group score object shape:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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.
|