Skip to main content

hypercall_admin/
competition.rs

1//! Competition admin endpoints.
2//!
3//! These are the operator write surface for competitions: create/update/delete
4//! a competition. The validation rules (window/win-condition checks,
5//! post-competition immutability, pre-only deletion) live in
6//! [`hypercall_competition::CompetitionService`]; the public competition read
7//! routes stay in `hypercall-api`.
8
9use axum::{
10    extract::{Path, State},
11    http::StatusCode,
12    response::IntoResponse,
13    Json,
14};
15use chrono::Utc;
16
17use hypercall_competition::competition_to_api;
18use hypercall_db::{CompetitionUpdateInput, CompetitionUpsertInput};
19use hypercall_runtime_api::error::ApiError;
20use hypercall_types::api_models::{
21    CompetitionResponse, CompetitionUpdateRequest, CompetitionUpsertRequest,
22};
23
24use crate::state::AdminState;
25
26/// Create competition.
27#[utoipa::path(
28    post,
29    path = "/monitoring/competitions",
30    request_body = CompetitionUpsertRequest,
31    responses((status = 200, body = CompetitionResponse)),
32    tag = "Monitoring",
33    security(("admin_key" = []))
34)]
35pub async fn create_competition(
36    State(state): State<AdminState>,
37    Json(request): Json<CompetitionUpsertRequest>,
38) -> Result<Json<CompetitionResponse>, ApiError> {
39    let now_ts_ms = Utc::now().timestamp_millis();
40    let row = state
41        .competition
42        .create_competition(CompetitionUpsertInput {
43            name: request.name,
44            description: request.description,
45            rules_url: request.rules_url,
46            rules_content: request.rules_content,
47            win_conditions: request.win_conditions,
48            primary_win_condition: request.primary_win_condition,
49            start_ts_ms: request.start_ts_ms,
50            end_ts_ms: request.end_ts_ms,
51        })
52        .await
53        .map_err(ApiError::from)?;
54
55    Ok(Json(CompetitionResponse {
56        success: true,
57        data: competition_to_api(row, now_ts_ms),
58    }))
59}
60
61/// Update competition.
62#[utoipa::path(
63    put,
64    path = "/monitoring/competitions/{id}",
65    params(("id" = i64, Path, description = "Competition id")),
66    request_body = CompetitionUpdateRequest,
67    responses((status = 200, body = CompetitionResponse)),
68    tag = "Monitoring",
69    security(("admin_key" = []))
70)]
71pub async fn update_competition(
72    State(state): State<AdminState>,
73    Path(id): Path<i64>,
74    Json(request): Json<CompetitionUpdateRequest>,
75) -> Result<Json<CompetitionResponse>, ApiError> {
76    let now_ts_ms = Utc::now().timestamp_millis();
77    let row = state
78        .competition
79        .update_competition(
80            id,
81            CompetitionUpdateInput {
82                name: request.name,
83                description: request.description,
84                rules_url: request.rules_url,
85                rules_content: request.rules_content,
86                win_conditions: request.win_conditions,
87                primary_win_condition: request.primary_win_condition,
88                start_ts_ms: request.start_ts_ms,
89                end_ts_ms: request.end_ts_ms,
90            },
91            now_ts_ms,
92        )
93        .await
94        .map_err(ApiError::from)?;
95
96    Ok(Json(CompetitionResponse {
97        success: true,
98        data: competition_to_api(row, now_ts_ms),
99    }))
100}
101
102/// Delete competition.
103#[utoipa::path(
104    delete,
105    path = "/monitoring/competitions/{id}",
106    params(("id" = i64, Path, description = "Competition id")),
107    responses((status = 204, description = "Competition deleted")),
108    tag = "Monitoring",
109    security(("admin_key" = []))
110)]
111pub async fn delete_competition(
112    State(state): State<AdminState>,
113    Path(id): Path<i64>,
114) -> Result<impl IntoResponse, ApiError> {
115    let now_ts_ms = Utc::now().timestamp_millis();
116    state
117        .competition
118        .delete_competition(id, now_ts_ms)
119        .await
120        .map_err(ApiError::from)?;
121    Ok(StatusCode::NO_CONTENT)
122}