160 lines
5.0 KiB
Rust
160 lines
5.0 KiB
Rust
use crate::{
|
|
db::Pool,
|
|
error::AppResult,
|
|
models::season::{Season, SeasonEndSummary, SeasonHistoryEntry, SeasonResult},
|
|
services::{club, pack},
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
/// Get the current season for a profile, creating it if it doesn't exist.
|
|
pub async fn get_or_create(pool: &Pool, profile_id: &str) -> AppResult<Season> {
|
|
if let Some(s) = fetch(pool, profile_id).await? {
|
|
return Ok(s);
|
|
}
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
sqlx::query(
|
|
"INSERT INTO seasons (profile_id, division, season_number, season_points, \
|
|
matches_played, wins, draws, losses, started_at) VALUES (?, 5, 1, 0, 0, 0, 0, 0, ?)",
|
|
)
|
|
.bind(profile_id)
|
|
.bind(&now)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(fetch(pool, profile_id).await?.expect("just inserted"))
|
|
}
|
|
|
|
async fn fetch(pool: &Pool, profile_id: &str) -> AppResult<Option<Season>> {
|
|
let s = sqlx::query_as::<_, Season>(
|
|
"SELECT profile_id, division, season_number, season_points, matches_played, \
|
|
wins, draws, losses, started_at FROM seasons WHERE profile_id = ?",
|
|
)
|
|
.bind(profile_id)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
Ok(s)
|
|
}
|
|
|
|
/// Record a match result in the season; end the season if the quota is met.
|
|
///
|
|
/// Returns the updated season and an optional end-of-season summary.
|
|
pub async fn record_match(
|
|
pool: &Pool,
|
|
club_id: &str,
|
|
profile_id: &str,
|
|
outcome: &str,
|
|
) -> AppResult<(Season, Option<SeasonEndSummary>)> {
|
|
let points = match outcome {
|
|
"win" => 3,
|
|
"draw" => 1,
|
|
_ => 0,
|
|
};
|
|
|
|
sqlx::query(
|
|
"UPDATE seasons SET \
|
|
season_points = season_points + ?, \
|
|
matches_played = matches_played + 1, \
|
|
wins = wins + CASE WHEN ? = 'win' THEN 1 ELSE 0 END, \
|
|
draws = draws + CASE WHEN ? = 'draw' THEN 1 ELSE 0 END, \
|
|
losses = losses + CASE WHEN ? = 'loss' THEN 1 ELSE 0 END \
|
|
WHERE profile_id = ?",
|
|
)
|
|
.bind(points)
|
|
.bind(outcome)
|
|
.bind(outcome)
|
|
.bind(outcome)
|
|
.bind(profile_id)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
let season = fetch(pool, profile_id).await?.expect("season must exist");
|
|
|
|
if !season.is_complete() {
|
|
return Ok((season, None));
|
|
}
|
|
|
|
// Season complete — calculate result and start next
|
|
let result = season.end_result();
|
|
let old_div = season.division;
|
|
let coins = season.season_reward_coins();
|
|
let pack_id = season.season_reward_pack();
|
|
|
|
let new_div = match result {
|
|
SeasonResult::Promoted => (old_div - 1).max(1),
|
|
SeasonResult::Relegated => (old_div + 1).min(10),
|
|
SeasonResult::Maintained => old_div,
|
|
};
|
|
let new_season = season.season_number + 1;
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
|
|
sqlx::query(
|
|
"UPDATE seasons SET division = ?, season_number = ?, season_points = 0, \
|
|
matches_played = 0, wins = 0, draws = 0, losses = 0, started_at = ? \
|
|
WHERE profile_id = ?",
|
|
)
|
|
.bind(new_div)
|
|
.bind(new_season)
|
|
.bind(&now)
|
|
.bind(profile_id)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
// Grant rewards
|
|
club::add_coins(pool, club_id, coins).await?;
|
|
if let Some(pack_def) = pack_id {
|
|
pack::grant_pack(pool, club_id, pack_def).await?;
|
|
}
|
|
|
|
// Persist history entry before rolling over
|
|
let result_str = match result {
|
|
SeasonResult::Promoted => "promoted",
|
|
SeasonResult::Maintained => "maintained",
|
|
SeasonResult::Relegated => "relegated",
|
|
};
|
|
let history_id = Uuid::new_v4().to_string();
|
|
let _ = sqlx::query(
|
|
"INSERT INTO season_history (id, profile_id, season_number, division, season_points, \
|
|
wins, draws, losses, result, new_division, coins_awarded, pack_awarded, ended_at) \
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
)
|
|
.bind(&history_id)
|
|
.bind(profile_id)
|
|
.bind(season.season_number)
|
|
.bind(old_div)
|
|
.bind(season.season_points)
|
|
.bind(season.wins)
|
|
.bind(season.draws)
|
|
.bind(season.losses)
|
|
.bind(result_str)
|
|
.bind(new_div)
|
|
.bind(coins)
|
|
.bind(pack_id)
|
|
.bind(&now)
|
|
.execute(pool)
|
|
.await;
|
|
|
|
let summary = SeasonEndSummary {
|
|
result,
|
|
old_division: old_div,
|
|
new_division: new_div,
|
|
new_season_number: new_season,
|
|
coins_awarded: coins,
|
|
pack_awarded: pack_id.map(String::from),
|
|
};
|
|
|
|
let updated = fetch(pool, profile_id).await?.expect("season must exist");
|
|
Ok((updated, Some(summary)))
|
|
}
|
|
|
|
/// Return past seasons for a profile, newest first (max 20).
|
|
pub async fn get_history(pool: &Pool, profile_id: &str) -> AppResult<Vec<SeasonHistoryEntry>> {
|
|
let rows = sqlx::query_as::<_, SeasonHistoryEntry>(
|
|
"SELECT id, profile_id, season_number, division, season_points, wins, draws, losses, \
|
|
result, new_division, coins_awarded, pack_awarded, ended_at \
|
|
FROM season_history WHERE profile_id = ? ORDER BY season_number DESC LIMIT 20",
|
|
)
|
|
.bind(profile_id)
|
|
.fetch_all(pool)
|
|
.await?;
|
|
Ok(rows)
|
|
}
|