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 { 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> { 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)> { 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> { 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) }