Phase 11: FUT Champions mode and Division Rivals weekly rewards
CI / Build, lint & test (push) Failing after 1m33s
CI / Build, lint & test (push) Failing after 1m33s
FUT Champions (Weekend League): - POST /fut-champs/start — open a new 30-match week (one active session at a time) - GET /fut-champs — current session status with matches_remaining - POST /fut-champs/:id/result — record a match; auto-completes and computes tier at 30 - POST /fut-champs/:id/claim — claim coins + pack reward (idempotent guard) - GET /fut-champs/history — past sessions newest-first - 11 reward tiers: Elite (27+ wins, 50k coins + icon pack) down to Bronze 1 (0 wins, 250 coins) Division Rivals: - POST /rivals/claim-weekly — claim weekly reward scaled by current division - Week counter is monotonic (offline-safe, no real-time calendar dependency) - Division 1-3 get a bonus pack alongside coins Migration 0008 adds fut_champs_sessions table and two columns to seasons. 12 new integration tests — all 67 pass, clippy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{
|
||||
app::AppState,
|
||||
error::AppResult,
|
||||
services::{club as club_svc, fut_champs as champs_svc, profile as profile_svc, season as season_svc},
|
||||
};
|
||||
|
||||
/// GET /fut-champs — current active session, or null if none.
|
||||
pub async fn get_fut_champs(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let session = champs_svc::get_active_session(&state.pool, &profile.id).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"session": session,
|
||||
"max_matches": crate::models::fut_champs::MAX_MATCHES,
|
||||
})))
|
||||
}
|
||||
|
||||
/// POST /fut-champs/start — open a new FUT Champions week.
|
||||
pub async fn post_start_fut_champs(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let session = champs_svc::start_session(&state.pool, &profile.id).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"session": session,
|
||||
"message": "FUT Champions week started — play up to 30 matches",
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChampsMatchRequest {
|
||||
pub goals_for: i64,
|
||||
pub goals_against: i64,
|
||||
}
|
||||
|
||||
/// POST /fut-champs/:session_id/result — record a match in this session.
|
||||
pub async fn post_champs_result(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(req): Json<ChampsMatchRequest>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
|
||||
let session = champs_svc::record_match(
|
||||
&state.pool,
|
||||
&profile.id,
|
||||
&session_id,
|
||||
req.goals_for,
|
||||
req.goals_against,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let outcome = if req.goals_for > req.goals_against {
|
||||
"win"
|
||||
} else if req.goals_for == req.goals_against {
|
||||
"draw"
|
||||
} else {
|
||||
"loss"
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"session": session,
|
||||
"outcome": outcome,
|
||||
"matches_remaining": session.matches_remaining(),
|
||||
"auto_completed": session.is_complete(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// POST /fut-champs/:session_id/claim — claim end-of-week rewards.
|
||||
pub async fn post_claim_champs_rewards(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
let result = champs_svc::claim_rewards(
|
||||
&state.pool,
|
||||
&profile.id,
|
||||
&session_id,
|
||||
&club.id,
|
||||
&state.pack_defs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /fut-champs/history — past sessions, newest first.
|
||||
pub async fn get_champs_history(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let history = champs_svc::get_history(&state.pool, &profile.id).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"history": history,
|
||||
"total": history.len(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// POST /rivals/claim-weekly — claim weekly Division Rivals reward.
|
||||
pub async fn post_claim_rivals_reward(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
// Ensure a season row exists
|
||||
season_svc::get_or_create(&state.pool, &profile.id).await?;
|
||||
|
||||
let result = champs_svc::claim_rivals_reward(
|
||||
&state.pool,
|
||||
&profile.id,
|
||||
&club.id,
|
||||
&state.pack_defs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
Reference in New Issue
Block a user