From a034e74c16b37b925662d0277709b2372660c7ea Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 20 Aug 2026 16:05:41 +0000 Subject: [PATCH] style(core): apply cargo fmt across routes, services, models, tests Pure rustfmt reflow (import grouping, array/match-arm/call-arg wrapping, alphabetized module decls, comment realignment). No semantic change: full-diff and `git diff -w` both confirm logic byte-identical to 271c363; workspace builds and all 74 core tests pass. Retained pre-existing WIP brought forward after verification. --- src/models/mod.rs | 6 +- src/models/profile.rs | 22 +++--- src/routes/achievements.rs | 13 ++- src/routes/auth.rs | 5 +- src/routes/cards.rs | 33 ++++---- src/routes/club.rs | 60 +++++++------- src/routes/division.rs | 46 ++++++++--- src/routes/draft.rs | 6 +- src/routes/fut_champs.rs | 29 ++++--- src/routes/market.rs | 18 +++-- src/routes/matches.rs | 12 ++- src/routes/mod.rs | 2 +- src/routes/notifications.rs | 31 +++++--- src/routes/packs.rs | 13 ++- src/routes/upgrades.rs | 9 +-- src/services/achievement.rs | 145 ++++++++++++++++++---------------- src/services/checkin.rs | 19 +++-- src/services/draft.rs | 35 ++++---- src/services/fut_champs.rs | 6 +- src/services/market.rs | 15 ++-- src/services/match_service.rs | 96 +++++++++++++++++----- src/services/objective.rs | 5 +- src/services/pack.rs | 19 +++-- src/services/profile.rs | 6 +- src/services/season.rs | 22 +++--- src/services/upgrades.rs | 18 ++--- tests/integration_test.rs | 18 ++++- 27 files changed, 436 insertions(+), 273 deletions(-) diff --git a/src/models/mod.rs b/src/models/mod.rs index f3c6981..8524838 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,19 +1,19 @@ pub mod achievement; pub mod card; pub mod chemistry_style; -pub mod notification; pub mod club; pub mod draft; +pub mod event; pub mod fut_champs; pub mod game_ext; -pub mod event; -pub mod season; pub mod market; pub mod match_result; +pub mod notification; pub mod objective; pub mod pack; pub mod profile; pub mod reward; pub mod sbc; +pub mod season; pub mod squad; pub mod statistics; diff --git a/src/models/profile.rs b/src/models/profile.rs index 571353e..c4c4b0e 100644 --- a/src/models/profile.rs +++ b/src/models/profile.rs @@ -38,16 +38,16 @@ pub struct CreateProfileRequest { /// XP required to reach each level (cumulative total from level 1). /// Level 1 starts at 0 XP. Level 2 needs 500 total XP, etc. pub const XP_THRESHOLDS: &[i64] = &[ - 0, // level 1 - 500, // level 2 - 1200, // level 3 - 2000, // level 4 - 3000, // level 5 - 4200, // level 6 - 5600, // level 7 - 7200, // level 8 - 9000, // level 9 - 11000, // level 10 + 0, // level 1 + 500, // level 2 + 1200, // level 3 + 2000, // level 4 + 3000, // level 5 + 4200, // level 6 + 5600, // level 7 + 7200, // level 8 + 9000, // level 9 + 11000, // level 10 ]; /// Compute the level for a given cumulative XP total. @@ -75,7 +75,7 @@ pub fn coins_for_level(new_level: i64) -> i64 { /// Pack granted at milestone levels (5, 10, 15, 20, …). pub fn pack_for_level(new_level: i64) -> Option<&'static str> { match new_level { - 5 => Some("bronze_pack"), + 5 => Some("bronze_pack"), 10 => Some("silver_pack"), 15 => Some("gold_pack"), 20 => Some("rare_gold_pack"), diff --git a/src/routes/achievements.rs b/src/routes/achievements.rs index dd6e18b..a93f606 100644 --- a/src/routes/achievements.rs +++ b/src/routes/achievements.rs @@ -8,12 +8,19 @@ use crate::{ services::{achievement as ach_svc, club as club_svc, profile as profile_svc}, }; -pub async fn get_achievements(State(state): State, game: GameId) -> AppResult> { +pub async fn get_achievements( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; - let _ = ach_svc::check_and_unlock(&state.pool, &state.achievement_defs, &profile.id, &club.id).await; + let _ = ach_svc::check_and_unlock(&state.pool, &state.achievement_defs, &profile.id, &club.id) + .await; let achievements = ach_svc::list_with_status(&state.pool, &state.achievement_defs).await?; - let earned = achievements.iter().filter(|a| a["unlocked"].as_bool().unwrap_or(false)).count(); + let earned = achievements + .iter() + .filter(|a| a["unlocked"].as_bool().unwrap_or(false)) + .count(); Ok(Json(json!({ "achievements": achievements, "earned": earned, diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 83afbd2..b53bd69 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -34,7 +34,10 @@ pub async fn post_auth_local( /// GET /auth/status — lightweight check: does a profile exist? /// Returns 200 `{ "has_profile": true/false }` without erroring. -pub async fn get_auth_status(State(state): State, game: GameId) -> AppResult> { +pub async fn get_auth_status( + State(state): State, + game: GameId, +) -> AppResult> { let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM profiles WHERE game_id = ?") .bind(game.as_str()) .fetch_one(&state.pool) diff --git a/src/routes/cards.rs b/src/routes/cards.rs index d67b82b..a3bae52 100644 --- a/src/routes/cards.rs +++ b/src/routes/cards.rs @@ -11,8 +11,7 @@ use crate::{ error::{AppError, AppResult}, models::card::OwnedCard, services::{ - club as club_svc, - economy as economy_svc, + club as club_svc, economy as economy_svc, inventory::{self, OwnedItemQuery, OwnedItemView}, profile as profile_svc, }, @@ -20,11 +19,17 @@ use crate::{ /// Quick-sell value for a card based on overall rating. fn quick_sell_coins(overall: u8) -> i64 { - if overall >= 85 { 1500 } - else if overall >= 80 { 900 } - else if overall >= 75 { 600 } - else if overall >= 65 { 300 } - else { 150 } + if overall >= 85 { + 1500 + } else if overall >= 80 { + 900 + } else if overall >= 75 { + 600 + } else if overall >= 65 { + 300 + } else { + 150 + } } #[derive(Debug, Deserialize)] @@ -96,7 +101,9 @@ pub async fn get_cards( cards.truncate(limit); } - Ok(Json(json!({ "cards": cards, "total": total, "returned": cards.len() }))) + Ok(Json( + json!({ "cards": cards, "total": total, "returned": cards.len() }), + )) } pub async fn get_collection( @@ -119,8 +126,7 @@ pub async fn get_collection( .filter_map(|o| { state.card_db.get(&o.card_id).map(|def| { let effective_overall = def.overall as i64 + o.training_bonus; - let effective_position = - o.position_override.as_deref().unwrap_or(&def.position); + let effective_position = o.position_override.as_deref().unwrap_or(&def.position); let body = json!({ "owned_card_id": o.id, "is_loan": o.is_loan, @@ -178,10 +184,9 @@ pub async fn delete_owned_card( .await? .ok_or_else(|| AppError::NotFound(format!("owned card '{owned_card_id}' not found")))?; - let card = state - .card_db - .get(&owned.card_id) - .ok_or_else(|| AppError::NotFound(format!("card definition '{}' missing", owned.card_id)))?; + let card = state.card_db.get(&owned.card_id).ok_or_else(|| { + AppError::NotFound(format!("card definition '{}' missing", owned.card_id)) + })?; let coins = quick_sell_coins(card.overall); diff --git a/src/routes/club.rs b/src/routes/club.rs index 531726e..31683bd 100644 --- a/src/routes/club.rs +++ b/src/routes/club.rs @@ -3,7 +3,9 @@ use crate::{ app::AppState, error::AppResult, models::club::Club, - services::{checkin as checkin_svc, club as club_svc, profile as profile_svc, statistics as stats_svc}, + services::{ + checkin as checkin_svc, club as club_svc, profile as profile_svc, statistics as stats_svc, + }, }; use axum::{extract::State, Json}; use serde::Deserialize; @@ -38,7 +40,10 @@ pub async fn put_club( Ok(Json(json!({ "club": updated }))) } -pub async fn get_checkin_status(State(state): State, game: GameId) -> AppResult> { +pub async fn get_checkin_status( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let status = checkin_svc::get_status(&state.pool, &profile.id).await?; Ok(Json(json!({ @@ -67,13 +72,12 @@ pub async fn get_milestones(State(state): State, game: GameId) -> AppR let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let stats = stats_svc::get_or_create(&state.pool, &profile.id).await?; - let seasons_completed: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM season_history WHERE profile_id = ?", - ) - .bind(&profile.id) - .fetch_one(&state.pool) - .await - .unwrap_or(0); + let seasons_completed: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM season_history WHERE profile_id = ?") + .bind(&profile.id) + .fetch_one(&state.pool) + .await + .unwrap_or(0); let highest_division: i64 = sqlx::query_scalar( "SELECT COALESCE(MIN(new_division), 10) FROM season_history WHERE profile_id = ?", @@ -83,29 +87,25 @@ pub async fn get_milestones(State(state): State, game: GameId) -> AppR .await .unwrap_or(10); - let cards_owned: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM owned_cards WHERE club_id = ?", - ) - .bind(&club.id) - .fetch_one(&state.pool) - .await - .unwrap_or(0); + let cards_owned: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE club_id = ?") + .bind(&club.id) + .fetch_one(&state.pool) + .await + .unwrap_or(0); - let sbcs_completed: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM sbc_submissions WHERE club_id = ? AND passed = 1", - ) - .bind(&club.id) - .fetch_one(&state.pool) - .await - .unwrap_or(0); + let sbcs_completed: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM sbc_submissions WHERE club_id = ? AND passed = 1") + .bind(&club.id) + .fetch_one(&state.pool) + .await + .unwrap_or(0); - let total_checkins: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM daily_checkins WHERE profile_id = ?", - ) - .bind(&profile.id) - .fetch_one(&state.pool) - .await - .unwrap_or(0); + let total_checkins: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM daily_checkins WHERE profile_id = ?") + .bind(&profile.id) + .fetch_one(&state.pool) + .await + .unwrap_or(0); Ok(Json(json!({ "total_wins": stats.matches_won, diff --git a/src/routes/division.rs b/src/routes/division.rs index 9982009..048bd13 100644 --- a/src/routes/division.rs +++ b/src/routes/division.rs @@ -37,13 +37,19 @@ pub async fn get_division(State(state): State, game: GameId) -> AppRes }))) } -pub async fn get_division_history(State(state): State, game: GameId) -> AppResult> { +pub async fn get_division_history( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let history = season_svc::get_history(&state.pool, &profile.id).await?; Ok(Json(json!({ "history": history, "total": history.len() }))) } -pub async fn get_division_leaderboard(State(state): State, game: GameId) -> AppResult> { +pub async fn get_division_leaderboard( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let season = season_svc::get_or_create(&state.pool, &profile.id).await?; @@ -53,11 +59,26 @@ pub async fn get_division_leaderboard(State(state): State, game: GameI let mut rng = rand::rngs::SmallRng::seed_from_u64(seed); const NPC_NAMES: &[&str] = &[ - "Riverside FC", "City Athletic", "County United", "Valley Rangers", - "Harbor Town FC", "Mountside City", "Lakewood Athletic", "Eastbrook United", - "Westfield Rovers", "Northgate FC", "Southport Athletic", "Ironbridge City", - "Milldale United", "Hillcrest Rangers", "Bayside FC", "Thornfield Athletic", - "Greenhill United", "Coldwater City", "Redbury Rangers", "Ashdown FC", + "Riverside FC", + "City Athletic", + "County United", + "Valley Rangers", + "Harbor Town FC", + "Mountside City", + "Lakewood Athletic", + "Eastbrook United", + "Westfield Rovers", + "Northgate FC", + "Southport Athletic", + "Ironbridge City", + "Milldale United", + "Hillcrest Rangers", + "Bayside FC", + "Thornfield Athletic", + "Greenhill United", + "Coldwater City", + "Redbury Rangers", + "Ashdown FC", ]; // Pick 9 NPC names without repetition using the seeded RNG @@ -75,10 +96,13 @@ pub async fn get_division_leaderboard(State(state): State, game: GameI // Quality bias: index 0-2 = stronger, 6-8 = weaker let quality: f64 = 1.0 - (idx as f64 / 8.0); // 1.0 → 0.0 let expected_win_rate = 0.2 + quality * 0.6; // 0.2–0.8 - let wins = (npc_matches as f64 * expected_win_rate * (0.8 + rng.gen::() * 0.4)) as i64; - let losses = (npc_matches as f64 * (1.0 - expected_win_rate) * (0.8 + rng.gen::() * 0.4)) as i64; - let draws = (npc_matches - wins - losses).max(0); - let pts = wins * 3 + draws; + let wins = + (npc_matches as f64 * expected_win_rate * (0.8 + rng.gen::() * 0.4)) as i64; + let losses = (npc_matches as f64 + * (1.0 - expected_win_rate) + * (0.8 + rng.gen::() * 0.4)) as i64; + let draws = (npc_matches - wins - losses).max(0); + let pts = wins * 3 + draws; json!({ "club_name": name, "wins": wins, diff --git a/src/routes/draft.rs b/src/routes/draft.rs index 5fb9eed..feee005 100644 --- a/src/routes/draft.rs +++ b/src/routes/draft.rs @@ -45,7 +45,8 @@ pub async fn post_draft_start( ) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let difficulty = query.difficulty.as_deref().unwrap_or("professional"); - let session = draft_svc::start_draft(&state.pool, &state.card_db, &profile.id, difficulty).await?; + let session = + draft_svc::start_draft(&state.pool, &state.card_db, &profile.id, difficulty).await?; Ok(Json(session)) } @@ -56,7 +57,8 @@ pub async fn get_draft_session( Path(session_id): Path, ) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; - let session = draft_svc::get_draft(&state.pool, &state.card_db, &profile.id, &session_id).await?; + let session = + draft_svc::get_draft(&state.pool, &state.card_db, &profile.id, &session_id).await?; Ok(Json(session)) } diff --git a/src/routes/fut_champs.rs b/src/routes/fut_champs.rs index 774a58d..7d9972d 100644 --- a/src/routes/fut_champs.rs +++ b/src/routes/fut_champs.rs @@ -9,7 +9,9 @@ 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}, + 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. @@ -24,7 +26,10 @@ pub async fn get_fut_champs(State(state): State, game: GameId) -> AppR } /// POST /fut-champs/start — open a new FUT Champions week. -pub async fn post_start_fut_champs(State(state): State, game: GameId) -> AppResult> { +pub async fn post_start_fut_champs( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let session = champs_svc::start_session(&state.pool, &profile.id).await?; @@ -96,7 +101,10 @@ pub async fn post_claim_champs_rewards( } /// GET /fut-champs/history — past sessions, newest first. -pub async fn get_champs_history(State(state): State, game: GameId) -> AppResult> { +pub async fn get_champs_history( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let history = champs_svc::get_history(&state.pool, &profile.id).await?; @@ -107,20 +115,19 @@ pub async fn get_champs_history(State(state): State, game: GameId) -> } /// POST /rivals/claim-weekly — claim weekly Division Rivals reward. -pub async fn post_claim_rivals_reward(State(state): State, game: GameId) -> AppResult> { +pub async fn post_claim_rivals_reward( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).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?; + let result = + champs_svc::claim_rivals_reward(&state.pool, &profile.id, &club.id, &state.pack_defs) + .await?; Ok(Json(result)) } diff --git a/src/routes/market.rs b/src/routes/market.rs index a915fb1..4db7696 100644 --- a/src/routes/market.rs +++ b/src/routes/market.rs @@ -13,14 +13,16 @@ use crate::{ services::{club as club_svc, market as market_svc, profile as profile_svc}, }; -pub async fn get_trade_history(State(state): State, game: GameId) -> AppResult> { +pub async fn get_trade_history( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let trades = market_svc::get_trade_history(&state.pool, &club.id).await?; Ok(Json(json!({ "trades": trades, "total": trades.len() }))) } - #[derive(Deserialize)] pub struct MarketQuery { pub min_overall: Option, @@ -86,11 +88,17 @@ pub async fn post_market_refresh(State(state): State) -> AppResult, game: GameId) -> AppResult> { +pub async fn get_my_listings( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; - let listings = market_svc::get_listings_by_seller(&state.pool, &state.card_db, &club.id).await?; - Ok(Json(json!({ "listings": listings, "total": listings.len() }))) + let listings = + market_svc::get_listings_by_seller(&state.pool, &state.card_db, &club.id).await?; + Ok(Json( + json!({ "listings": listings, "total": listings.len() }), + )) } /// Cancel a player-posted listing and return the card to the collection. diff --git a/src/routes/matches.rs b/src/routes/matches.rs index 5a8b25f..50c0465 100644 --- a/src/routes/matches.rs +++ b/src/routes/matches.rs @@ -71,9 +71,15 @@ pub async fn post_match_result( let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; - let result = - match_service::process_match(&state.pool, &profile.id, &club.id, &req, &state.obj_defs, &state.achievement_defs) - .await?; + let result = match_service::process_match( + &state.pool, + &profile.id, + &club.id, + &req, + &state.obj_defs, + &state.achievement_defs, + ) + .await?; Ok(Json(result)) } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 3746b04..5741f12 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -5,8 +5,8 @@ pub mod club; pub mod division; pub mod draft; pub mod economy; -pub mod fut_champs; pub mod events; +pub mod fut_champs; pub mod health; pub mod market; pub mod matches; diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs index 8805ae2..0241f1c 100644 --- a/src/routes/notifications.rs +++ b/src/routes/notifications.rs @@ -8,7 +8,9 @@ use serde_json::{json, Value}; use crate::{ app::AppState, error::{AppError, AppResult}, - services::{club as club_svc, notification as notif_svc, objective as obj_svc, profile as profile_svc}, + services::{ + club as club_svc, notification as notif_svc, objective as obj_svc, profile as profile_svc, + }, }; /// GET /notifications @@ -17,7 +19,10 @@ use crate::{ /// notifications (unclaimed objectives, expiring loans, season ending soon). /// Persistent notifications carry an `id` and `is_read` flag; dynamic ones /// have `id: null` and are always considered unread. -pub async fn get_notifications(State(state): State, game: GameId) -> AppResult> { +pub async fn get_notifications( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; @@ -93,14 +98,16 @@ pub async fn get_notifications(State(state): State, game: GameId) -> A // Use "type" key for compatibility with dashboard and existing tests. let all: Vec = persistent .iter() - .map(|n| json!({ - "id": n.id, - "type": n.kind, - "title": n.title, - "body": n.body, - "is_read": n.is_read, - "created_at": n.created_at, - })) + .map(|n| { + json!({ + "id": n.id, + "type": n.kind, + "title": n.title, + "body": n.body, + "is_read": n.is_read, + "created_at": n.created_at, + }) + }) .chain(dynamic.iter().cloned()) .collect(); @@ -125,9 +132,7 @@ pub async fn mark_notification_read( } /// POST /notifications/read-all -pub async fn mark_all_notifications_read( - State(state): State, -) -> AppResult> { +pub async fn mark_all_notifications_read(State(state): State) -> AppResult> { let count = notif_svc::mark_all_read(&state.pool).await?; Ok(Json(json!({ "marked_read": count }))) } diff --git a/src/routes/packs.rs b/src/routes/packs.rs index 14e03b7..d376cd2 100644 --- a/src/routes/packs.rs +++ b/src/routes/packs.rs @@ -79,7 +79,10 @@ pub async fn get_packs(State(state): State, game: GameId) -> AppResult } /// Return recently opened packs with the card IDs they contained. -pub async fn get_pack_history(State(state): State, game: GameId) -> AppResult> { +pub async fn get_pack_history( + State(state): State, + game: GameId, +) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; @@ -143,8 +146,12 @@ pub async fn post_open_pack( objective::increment_metric(&state.pool, &profile.id, &state.obj_defs, "packsopened", 1) .await?; let _ = crate::services::achievement::check_and_unlock( - &state.pool, &state.achievement_defs, &profile.id, &club.id, - ).await; + &state.pool, + &state.achievement_defs, + &profile.id, + &club.id, + ) + .await; Ok(Json(result)) } diff --git a/src/routes/upgrades.rs b/src/routes/upgrades.rs index 9979fe0..0884045 100644 --- a/src/routes/upgrades.rs +++ b/src/routes/upgrades.rs @@ -72,13 +72,8 @@ pub async fn post_change_position( let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; - let updated = upgrade_svc::change_position( - &state.pool, - &club.id, - &owned_card_id, - &req.position, - ) - .await?; + let updated = + upgrade_svc::change_position(&state.pool, &club.id, &owned_card_id, &req.position).await?; let card_def = state.card_db.get(&updated.card_id); diff --git a/src/services/achievement.rs b/src/services/achievement.rs index 7690e4d..5a80af6 100644 --- a/src/services/achievement.rs +++ b/src/services/achievement.rs @@ -29,79 +29,83 @@ pub fn load_achievement_definitions(data_dir: &str) -> anyhow::Result AppResult { - let v: i64 = match trigger { - "matches_played" => sqlx::query_scalar( - "SELECT matches_played FROM statistics WHERE profile_id = ?", - ) - .bind(profile_id) - .fetch_optional(pool) - .await? - .unwrap_or(0), +async fn metric_value( + pool: &Pool, + profile_id: &str, + club_id: &str, + trigger: &str, +) -> AppResult { + let v: i64 = + match trigger { + "matches_played" => { + sqlx::query_scalar("SELECT matches_played FROM statistics WHERE profile_id = ?") + .bind(profile_id) + .fetch_optional(pool) + .await? + .unwrap_or(0) + } - "matches_won" => sqlx::query_scalar( - "SELECT matches_won FROM statistics WHERE profile_id = ?", - ) - .bind(profile_id) - .fetch_optional(pool) - .await? - .unwrap_or(0), + "matches_won" => { + sqlx::query_scalar("SELECT matches_won FROM statistics WHERE profile_id = ?") + .bind(profile_id) + .fetch_optional(pool) + .await? + .unwrap_or(0) + } - "goals_scored" => sqlx::query_scalar( - "SELECT goals_scored FROM statistics WHERE profile_id = ?", - ) - .bind(profile_id) - .fetch_optional(pool) - .await? - .unwrap_or(0), + "goals_scored" => { + sqlx::query_scalar("SELECT goals_scored FROM statistics WHERE profile_id = ?") + .bind(profile_id) + .fetch_optional(pool) + .await? + .unwrap_or(0) + } - "packs_opened" => sqlx::query_scalar( - "SELECT packs_opened FROM statistics WHERE profile_id = ?", - ) - .bind(profile_id) - .fetch_optional(pool) - .await? - .unwrap_or(0), + "packs_opened" => { + sqlx::query_scalar("SELECT packs_opened FROM statistics WHERE profile_id = ?") + .bind(profile_id) + .fetch_optional(pool) + .await? + .unwrap_or(0) + } - "sbcs_completed" => sqlx::query_scalar( - "SELECT sbcs_completed FROM statistics WHERE profile_id = ?", - ) - .bind(profile_id) - .fetch_optional(pool) - .await? - .unwrap_or(0), + "sbcs_completed" => { + sqlx::query_scalar("SELECT sbcs_completed FROM statistics WHERE profile_id = ?") + .bind(profile_id) + .fetch_optional(pool) + .await? + .unwrap_or(0) + } - "cards_owned" => sqlx::query_scalar( - "SELECT COUNT(*) FROM owned_cards WHERE club_id = ?", - ) - .bind(club_id) - .fetch_one(pool) - .await?, + "cards_owned" => { + sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE club_id = ?") + .bind(club_id) + .fetch_one(pool) + .await? + } - "level" => sqlx::query_scalar( - "SELECT level FROM profiles WHERE id = ?", - ) - .bind(profile_id) - .fetch_optional(pool) - .await? - .unwrap_or(1), + "level" => sqlx::query_scalar("SELECT level FROM profiles WHERE id = ?") + .bind(profile_id) + .fetch_optional(pool) + .await? + .unwrap_or(1), - "objectives_completed" => sqlx::query_scalar( - "SELECT COUNT(*) FROM objective_progress WHERE profile_id = ? AND completed = 1", - ) - .bind(profile_id) - .fetch_one(pool) - .await?, + "objectives_completed" => sqlx::query_scalar( + "SELECT COUNT(*) FROM objective_progress WHERE profile_id = ? AND completed = 1", + ) + .bind(profile_id) + .fetch_one(pool) + .await?, - "drafts_completed" => sqlx::query_scalar( - "SELECT COUNT(*) FROM draft_sessions WHERE profile_id = ? AND status = 'completed'", - ) - .bind(profile_id) - .fetch_one(pool) - .await?, + "drafts_completed" => sqlx::query_scalar( + "SELECT COUNT(*) FROM draft_sessions WHERE profile_id = ? AND status = 'completed'", + ) + .bind(profile_id) + .fetch_one(pool) + .await?, - _ => 0, - }; + _ => 0, + }; Ok(v) } @@ -165,11 +169,14 @@ pub async fn check_and_unlock( club_svc::add_coins(pool, club_id, def.reward_coins).await?; } - let body = format!( - "{} Reward: {} coins.", - def.description, def.reward_coins - ); - let _ = notification::create(pool, "achievement", &format!("Achievement: {}", def.title), &body).await; + let body = format!("{} Reward: {} coins.", def.description, def.reward_coins); + let _ = notification::create( + pool, + "achievement", + &format!("Achievement: {}", def.title), + &body, + ) + .await; newly_unlocked.push(def.clone()); } diff --git a/src/services/checkin.rs b/src/services/checkin.rs index eaf1fd5..49e7749 100644 --- a/src/services/checkin.rs +++ b/src/services/checkin.rs @@ -1,4 +1,8 @@ -use crate::{db::Pool, error::AppResult, services::{club, pack}}; +use crate::{ + db::Pool, + error::AppResult, + services::{club, pack}, +}; use uuid::Uuid; const STREAK_COINS: [i64; 7] = [500, 650, 800, 1000, 1200, 1500, 2000]; @@ -8,7 +12,7 @@ const STREAK_7_PACK: &str = "silver_pack"; #[derive(Debug, serde::Serialize)] pub struct CheckinStatus { pub available: bool, - pub streak_day: i64, // current streak (1–7 cycle, 0 if never checked in) + pub streak_day: i64, // current streak (1–7 cycle, 0 if never checked in) pub next_reward_coins: i64, pub next_reward_pack: Option<&'static str>, pub last_checked_in: Option, @@ -56,11 +60,7 @@ pub async fn get_status(pool: &Pool, profile_id: &str) -> AppResult AppResult { +pub async fn claim(pool: &Pool, profile_id: &str, club_id: &str) -> AppResult { let row: Option<(i64, String)> = sqlx::query_as( "SELECT streak_day, checked_in_at FROM daily_checkins \ WHERE profile_id = ? ORDER BY checked_in_at DESC LIMIT 1", @@ -82,7 +82,10 @@ pub async fn claim( } } - let last_streak = row.as_ref().map(|(s, last_at)| compute_next_streak(*s, last_at)).unwrap_or(1); + let last_streak = row + .as_ref() + .map(|(s, last_at)| compute_next_streak(*s, last_at)) + .unwrap_or(1); let idx = (last_streak - 1).rem_euclid(7) as usize; let coins = STREAK_COINS[idx]; let pack_def = if idx == 6 { Some(STREAK_7_PACK) } else { None }; diff --git a/src/services/draft.rs b/src/services/draft.rs index eef64e3..a11de7b 100644 --- a/src/services/draft.rs +++ b/src/services/draft.rs @@ -184,24 +184,32 @@ pub async fn pick_card( let (new_candidates, new_status, reward_coins, reward_pack_id, squad_rating, completed_at) = if all_filled { let (coins, pack, avg) = compute_reward(card_db, &picks); - (None, "completed".to_string(), coins, pack, avg, Some(chrono::Utc::now().to_rfc3339())) + ( + None, + "completed".to_string(), + coins, + pack, + avg, + Some(chrono::Utc::now().to_rfc3339()), + ) } else { let min_overall = difficulty_min_overall(&session.difficulty); let next_pos = &pick_order[next_index]; let next_candidates = pick_candidates(card_db, next_pos, min_overall, CANDIDATES_PER_SLOT); { - let candidates_json = serde_json::to_string(&next_candidates) - .map_err(|e| AppError::Internal(anyhow::anyhow!("serialization failed: {e}")))?; - ( - Some(candidates_json), - "active".to_string(), - 0, - None, - 0, - None, - ) - } + let candidates_json = serde_json::to_string(&next_candidates).map_err(|e| { + AppError::Internal(anyhow::anyhow!("serialization failed: {e}")) + })?; + ( + Some(candidates_json), + "active".to_string(), + 0, + None, + 0, + None, + ) + } }; let picks_json = serde_json::to_string(&picks) @@ -294,8 +302,7 @@ async fn fetch_session(pool: &Pool, profile_id: &str, session_id: &str) -> AppRe } fn render_session(session: &DraftSession, card_db: &CardDb) -> serde_json::Value { - let pick_order: Vec = - serde_json::from_str(&session.pick_order).unwrap_or_default(); + let pick_order: Vec = serde_json::from_str(&session.pick_order).unwrap_or_default(); let picks: Vec = serde_json::from_str(&session.picks).unwrap_or_default(); let candidates: Vec = session .current_candidates diff --git a/src/services/fut_champs.rs b/src/services/fut_champs.rs index 0c2d1e3..19e18db 100644 --- a/src/services/fut_champs.rs +++ b/src/services/fut_champs.rs @@ -26,7 +26,11 @@ pub async fn get_active_session( .map_err(Into::into) } -pub async fn get_session(pool: &Pool, session_id: &str, profile_id: &str) -> AppResult { +pub async fn get_session( + pool: &Pool, + session_id: &str, + profile_id: &str, +) -> AppResult { sqlx::query_as::<_, FutChampsSession>(&format!( "{SESSION_SELECT} WHERE id = ? AND profile_id = ?" )) diff --git a/src/services/market.rs b/src/services/market.rs index 0ccee01..fe95bdf 100644 --- a/src/services/market.rs +++ b/src/services/market.rs @@ -149,7 +149,9 @@ pub async fn buy_listing( .await? .rows_affected(); if claimed == 0 { - return Err(AppError::NotFound("listing not found or already sold".into())); + return Err(AppError::NotFound( + "listing not found or already sold".into(), + )); } if let Err(e) = club::spend_coins(pool, club_id, listing.price).await { let _ = sqlx::query("UPDATE market_listings SET sold = 0 WHERE id = ?") @@ -308,9 +310,10 @@ pub async fn get_listings_by_seller( let with_cards = listings .into_iter() .filter_map(|l| { - card_db - .get(&l.card_id) - .map(|card| MarketListingWithCard { listing: l, card: card.clone() }) + card_db.get(&l.card_id).map(|card| MarketListingWithCard { + listing: l, + card: card.clone(), + }) }) .collect(); Ok(with_cards) @@ -326,7 +329,9 @@ pub async fn cancel_listing(pool: &Pool, club_id: &str, listing_id: &str) -> App .bind(club_id) .fetch_optional(pool) .await? - .ok_or_else(|| AppError::NotFound(format!("listing '{listing_id}' not found or already sold")))?; + .ok_or_else(|| { + AppError::NotFound(format!("listing '{listing_id}' not found or already sold")) + })?; sqlx::query("DELETE FROM market_listings WHERE id = ?") .bind(listing_id) diff --git a/src/services/match_service.rs b/src/services/match_service.rs index 6ba4738..94159a0 100644 --- a/src/services/match_service.rs +++ b/src/services/match_service.rs @@ -7,13 +7,14 @@ use crate::{ match_result::{Match, MatchRewardResult, SubmitMatchRequest}, objective::ObjectiveDefinition, }, - services::{achievement, card_db::CardDb, club, notification, objective, profile, season as season_svc, statistics}, + services::{ + achievement, card_db::CardDb, club, notification, objective, profile, season as season_svc, + statistics, + }, }; use rand::{seq::SliceRandom, Rng}; -const FORMATIONS: &[&str] = &[ - "4-3-3", "4-4-2", "4-2-3-1", "4-1-2-1-2", "3-5-2", "5-3-2", -]; +const FORMATIONS: &[&str] = &["4-3-3", "4-4-2", "4-2-3-1", "4-1-2-1-2", "3-5-2", "5-3-2"]; /// Generate a random AI opponent squad for Squad Battles. /// @@ -27,23 +28,53 @@ pub fn generate_opponent(card_db: &CardDb, difficulty: &str) -> serde_json::Valu let (min_overall, names): (u8, &[&str]) = match difficulty { "professional" => ( 70, - &["Athletic CF", "City Wanderers", "The Rovers", "United Select", "Blue Stars FC"], + &[ + "Athletic CF", + "City Wanderers", + "The Rovers", + "United Select", + "Blue Stars FC", + ], ), "world_class" => ( 78, - &["Elite Stars FC", "Champions Select", "Premier XI", "Galaxy United", "Titan FC"], + &[ + "Elite Stars FC", + "Champions Select", + "Premier XI", + "Galaxy United", + "Titan FC", + ], ), "legendary" => ( 85, - &["Legends United", "Ultimate XI", "Gold Standard FC", "The Icons", "Heritage FC"], + &[ + "Legends United", + "Ultimate XI", + "Gold Standard FC", + "The Icons", + "Heritage FC", + ], ), "ultimate" => ( 90, - &["Apex XI", "Pantheon FC", "Gods of FUT", "Invincibles Select", "Eternal XI"], + &[ + "Apex XI", + "Pantheon FC", + "Gods of FUT", + "Invincibles Select", + "Eternal XI", + ], ), _ => ( 55, - &["Amateur Town FC", "Sunday League XI", "Park FC", "Village Stars", "Reserve XI"], + &[ + "Amateur Town FC", + "Sunday League XI", + "Park FC", + "Village Stars", + "Reserve XI", + ], ), }; @@ -59,7 +90,11 @@ pub fn generate_opponent(card_db: &CardDb, difficulty: &str) -> serde_json::Valu let mut indices: Vec = (0..pool.len()).collect(); indices.shuffle(&mut rng); - let cards: Vec<_> = indices.into_iter().take(11).map(|i| pool[i].clone()).collect(); + let cards: Vec<_> = indices + .into_iter() + .take(11) + .map(|i| pool[i].clone()) + .collect(); let squad_rating = if cards.is_empty() { 0 @@ -147,11 +182,18 @@ pub async fn process_match( for ev in &level_ups { let body = if let Some(ref pack) = ev.pack_granted { - format!("You reached level {}! Reward: {} coins + {pack}.", ev.new_level, ev.coins_granted) + format!( + "You reached level {}! Reward: {} coins + {pack}.", + ev.new_level, ev.coins_granted + ) } else { - format!("You reached level {}! Reward: {} coins.", ev.new_level, ev.coins_granted) + format!( + "You reached level {}! Reward: {} coins.", + ev.new_level, ev.coins_granted + ) }; - let _ = notification::create(pool, "level_up", &format!("Level {}!", ev.new_level), &body).await; + let _ = notification::create(pool, "level_up", &format!("Level {}!", ev.new_level), &body) + .await; } statistics::record_match( @@ -195,15 +237,20 @@ pub async fn process_match( .find(|d| &d.id == obj_id) .map(|d| d.title.as_str()) .unwrap_or(obj_id.as_str()); - let body = format!("\"{}\" is now complete. Claim your reward in Objectives.", display_name); - let _ = notification::create(pool, "objective_complete", "Objective complete!", &body).await; + let body = format!( + "\"{}\" is now complete. Claim your reward in Objectives.", + display_name + ); + let _ = + notification::create(pool, "objective_complete", "Objective complete!", &body).await; } // Decrement loan matches remaining for squad starters; collect expired loan IDs. let expired_loans = process_loan_expiry(pool, club_id, &req.squad_id).await?; for owned_id in &expired_loans { - let body = format!("Loan card (id: {owned_id}) has expired and been removed from your club."); + let body = + format!("Loan card (id: {owned_id}) has expired and been removed from your club."); let _ = notification::create(pool, "loan_expired", "Loan card expired", &body).await; } @@ -218,14 +265,21 @@ pub async fn process_match( SeasonResult::Relegated => "Relegated", SeasonResult::Maintained => "Maintained", }; - let body = format!("{direction} — now in Division {}. Rewards: {} coins{}.", se.new_division, se.coins_awarded, se.pack_awarded.as_deref().map(|p| format!(" + {p}")).unwrap_or_default()); + let body = format!( + "{direction} — now in Division {}. Rewards: {} coins{}.", + se.new_division, + se.coins_awarded, + se.pack_awarded + .as_deref() + .map(|p| format!(" + {p}")) + .unwrap_or_default() + ); let _ = notification::create(pool, "season_end", "Season complete!", &body).await; } - let achievements_unlocked = - achievement::check_and_unlock(pool, ach_defs, profile_id, club_id) - .await - .unwrap_or_default(); + let achievements_unlocked = achievement::check_and_unlock(pool, ach_defs, profile_id, club_id) + .await + .unwrap_or_default(); Ok(MatchRewardResult { match_record, diff --git a/src/services/objective.rs b/src/services/objective.rs index ca310a0..315a7be 100644 --- a/src/services/objective.rs +++ b/src/services/objective.rs @@ -67,10 +67,7 @@ pub async fn increment_metric( ) -> AppResult> { let mut completed_ids = Vec::new(); - for def in defs - .iter() - .filter(|d| d.metric.as_str() == metric) - { + for def in defs.iter().filter(|d| d.metric.as_str() == metric) { let existing = sqlx::query_as::<_, ObjectiveProgress>( "SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ? AND objective_id = ?" ) diff --git a/src/services/pack.rs b/src/services/pack.rs index 3f95c23..b4e7d0c 100644 --- a/src/services/pack.rs +++ b/src/services/pack.rs @@ -73,14 +73,13 @@ pub async fn open_pack( // Atomically claim the pack before minting any cards: only one concurrent opener // flips opened 0->1, so a double-open cannot mint the reward twice (duplication). - let claimed = sqlx::query( - "UPDATE packs SET opened = 1 WHERE id = ? AND club_id = ? AND opened = 0", - ) - .bind(pack_id) - .bind(club_id) - .execute(pool) - .await? - .rows_affected(); + let claimed = + sqlx::query("UPDATE packs SET opened = 1 WHERE id = ? AND club_id = ? AND opened = 0") + .bind(pack_id) + .bind(club_id) + .execute(pool) + .await? + .rows_affected(); if claimed == 0 { return Err(AppError::BadRequest("pack already opened".into())); } @@ -134,8 +133,8 @@ pub async fn open_pack( } } - let card_ids_json = serde_json::to_string(&cards.iter().map(|c| &c.id).collect::>()) - .unwrap_or_default(); + let card_ids_json = + serde_json::to_string(&cards.iter().map(|c| &c.id).collect::>()).unwrap_or_default(); let now = chrono::Utc::now().to_rfc3339(); sqlx::query("UPDATE packs SET opened_cards = ?, opened_at = ? WHERE id = ?") diff --git a/src/services/profile.rs b/src/services/profile.rs index df01af6..c751f1d 100644 --- a/src/services/profile.rs +++ b/src/services/profile.rs @@ -100,7 +100,11 @@ pub async fn add_xp_with_levelup( } tracing::info!(profile_id, new_level = lvl, coins, "level up"); - events.push(LevelUpEvent { new_level: lvl, coins_granted: coins, pack_granted: pack }); + events.push(LevelUpEvent { + new_level: lvl, + coins_granted: coins, + pack_granted: pack, + }); } Ok(events) diff --git a/src/services/season.rs b/src/services/season.rs index 3909fe8..fc8859f 100644 --- a/src/services/season.rs +++ b/src/services/season.rs @@ -20,9 +20,11 @@ pub async fn get_or_create(pool: &Pool, profile_id: &str) -> AppResult { .bind(&now) .execute(pool) .await?; - fetch(pool, profile_id) - .await? - .ok_or_else(|| AppError::Internal(anyhow::anyhow!("season row missing immediately after insert"))) + fetch(pool, profile_id).await?.ok_or_else(|| { + AppError::Internal(anyhow::anyhow!( + "season row missing immediately after insert" + )) + }) } async fn fetch(pool: &Pool, profile_id: &str) -> AppResult> { @@ -68,9 +70,11 @@ pub async fn record_match( .execute(pool) .await?; - let season = fetch(pool, profile_id) - .await? - .ok_or_else(|| AppError::Internal(anyhow::anyhow!("season row missing after record_match update")))?; + let season = fetch(pool, profile_id).await?.ok_or_else(|| { + AppError::Internal(anyhow::anyhow!( + "season row missing after record_match update" + )) + })?; if !season.is_complete() { return Ok((season, None)); @@ -145,9 +149,9 @@ pub async fn record_match( pack_awarded: pack_id.map(String::from), }; - let updated = fetch(pool, profile_id) - .await? - .ok_or_else(|| AppError::Internal(anyhow::anyhow!("season row missing after season rollover")))?; + let updated = fetch(pool, profile_id).await?.ok_or_else(|| { + AppError::Internal(anyhow::anyhow!("season row missing after season rollover")) + })?; Ok((updated, Some(summary))) } diff --git a/src/services/upgrades.rs b/src/services/upgrades.rs index 761b04d..cccc15d 100644 --- a/src/services/upgrades.rs +++ b/src/services/upgrades.rs @@ -16,14 +16,12 @@ pub const MAX_TRAINING_BONUS: i64 = 3; pub const POSITION_CHANGE_COST: i64 = 500; async fn fetch_owned(pool: &Pool, owned_card_id: &str, club_id: &str) -> AppResult { - sqlx::query_as::<_, OwnedCard>(&format!( - "{OWNED_CARD_SELECT} WHERE id = ? AND club_id = ?" - )) - .bind(owned_card_id) - .bind(club_id) - .fetch_optional(pool) - .await? - .ok_or_else(|| AppError::NotFound("owned card not found".into())) + sqlx::query_as::<_, OwnedCard>(&format!("{OWNED_CARD_SELECT} WHERE id = ? AND club_id = ?")) + .bind(owned_card_id) + .bind(club_id) + .fetch_optional(pool) + .await? + .ok_or_else(|| AppError::NotFound("owned card not found".into())) } /// Apply a chemistry style to an owned card. @@ -64,8 +62,8 @@ pub async fn change_position( new_position: &str, ) -> AppResult { let valid_positions = [ - "GK", "RB", "LB", "CB", "RWB", "LWB", "CDM", "CM", "CAM", "RM", "LM", "RW", "LW", - "CF", "ST", + "GK", "RB", "LB", "CB", "RWB", "LWB", "CDM", "CM", "CAM", "RM", "LM", "RW", "LW", "CF", + "ST", ]; if !valid_positions.contains(&new_position) { return Err(AppError::BadRequest(format!( diff --git a/tests/integration_test.rs b/tests/integration_test.rs index ad8890f..0c55340 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -294,7 +294,12 @@ async fn test_sbc_rejects_duplicate_cards() { .as_str() .unwrap() .to_string(); - let (s, _) = json_post(&app, &format!("/packs/open/{pack_id}"), serde_json::json!({})).await; + let (s, _) = json_post( + &app, + &format!("/packs/open/{pack_id}"), + serde_json::json!({}), + ) + .await; assert_eq!(s, StatusCode::OK); let (_, coll) = json_get(&app, "/collection").await; @@ -312,9 +317,16 @@ async fn test_sbc_rejects_duplicate_cards() { }), ) .await; - assert_eq!(s, StatusCode::BAD_REQUEST, "duplicate submission must be rejected: {result}"); + assert_eq!( + s, + StatusCode::BAD_REQUEST, + "duplicate submission must be rejected: {result}" + ); assert!( - result["error"].as_str().unwrap_or_default().contains("duplicate"), + result["error"] + .as_str() + .unwrap_or_default() + .contains("duplicate"), "expected a duplicate-card error, got: {result}" ); }