style(core): cargo fmt match/club agent additions
CI / Build, lint & test (push) Successful in 4m1s

This commit is contained in:
funman300
2026-08-20 17:59:02 +00:00
parent 5f9f556af8
commit 2fb835200f
4 changed files with 108 additions and 83 deletions
+60 -59
View File
@@ -5,10 +5,10 @@ use crate::{
services::{club as club_svc, notification}, services::{club as club_svc, notification},
}; };
use anyhow::Context; use anyhow::Context;
use std::path::Path;
use uuid::Uuid;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path;
use uuid::Uuid;
pub fn load_achievement_definitions(data_dir: &str) -> anyhow::Result<Vec<AchievementDefinition>> { pub fn load_achievement_definitions(data_dir: &str) -> anyhow::Result<Vec<AchievementDefinition>> {
let dir = Path::new(data_dir).join("achievements"); let dir = Path::new(data_dir).join("achievements");
@@ -196,67 +196,68 @@ async fn metric_value_tx(
club_id: &str, club_id: &str,
trigger: &str, trigger: &str,
) -> AppResult<i64> { ) -> AppResult<i64> {
let v: i64 = match trigger { let v: i64 =
"matches_played" => { match trigger {
sqlx::query_scalar("SELECT matches_played FROM statistics WHERE profile_id = ?") "matches_played" => {
sqlx::query_scalar("SELECT matches_played FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"matches_won" => {
sqlx::query_scalar("SELECT matches_won FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"goals_scored" => {
sqlx::query_scalar("SELECT goals_scored FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"packs_opened" => {
sqlx::query_scalar("SELECT packs_opened FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"sbcs_completed" => {
sqlx::query_scalar("SELECT sbcs_completed FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"cards_owned" => {
sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE club_id = ?")
.bind(club_id)
.fetch_one(&mut **tx)
.await?
}
"level" => sqlx::query_scalar("SELECT level FROM profiles WHERE id = ?")
.bind(profile_id) .bind(profile_id)
.fetch_optional(&mut **tx) .fetch_optional(&mut **tx)
.await? .await?
.unwrap_or(0) .unwrap_or(1),
} "objectives_completed" => sqlx::query_scalar(
"matches_won" => { "SELECT COUNT(*) FROM objective_progress WHERE profile_id = ? AND completed = 1",
sqlx::query_scalar("SELECT matches_won FROM statistics WHERE profile_id = ?") )
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"goals_scored" => {
sqlx::query_scalar("SELECT goals_scored FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"packs_opened" => {
sqlx::query_scalar("SELECT packs_opened FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"sbcs_completed" => {
sqlx::query_scalar("SELECT sbcs_completed FROM statistics WHERE profile_id = ?")
.bind(profile_id)
.fetch_optional(&mut **tx)
.await?
.unwrap_or(0)
}
"cards_owned" => {
sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE club_id = ?")
.bind(club_id)
.fetch_one(&mut **tx)
.await?
}
"level" => sqlx::query_scalar("SELECT level FROM profiles WHERE id = ?")
.bind(profile_id) .bind(profile_id)
.fetch_optional(&mut **tx) .fetch_one(&mut **tx)
.await? .await?,
.unwrap_or(1), "drafts_completed" => sqlx::query_scalar(
"objectives_completed" => sqlx::query_scalar( "SELECT COUNT(*) FROM draft_sessions WHERE profile_id = ? AND status = 'completed'",
"SELECT COUNT(*) FROM objective_progress WHERE profile_id = ? AND completed = 1", )
) .bind(profile_id)
.bind(profile_id) .fetch_one(&mut **tx)
.fetch_one(&mut **tx) .await?,
.await?, _ => 0,
"drafts_completed" => sqlx::query_scalar( };
"SELECT COUNT(*) FROM draft_sessions WHERE profile_id = ? AND status = 'completed'",
)
.bind(profile_id)
.fetch_one(&mut **tx)
.await?,
_ => 0,
};
Ok(v) Ok(v)
} }
+9 -13
View File
@@ -140,14 +140,12 @@ const OWNED_SELECT: &str = "SELECT id, club_id, card_id, is_loan, loan_matches_r
/// The club's most-recently-updated squad id (its "active" squad), matching the /// The club's most-recently-updated squad id (its "active" squad), matching the
/// selection `squad::get_squad` uses, or `None` when the club has no squad yet. /// selection `squad::get_squad` uses, or `None` when the club has no squad yet.
pub async fn active_squad_id(pool: &Pool, club_id: &str) -> AppResult<Option<String>> { pub async fn active_squad_id(pool: &Pool, club_id: &str) -> AppResult<Option<String>> {
Ok( Ok(sqlx::query_scalar::<_, String>(
sqlx::query_scalar::<_, String>( "SELECT id FROM squads WHERE club_id = ? ORDER BY updated_at DESC LIMIT 1",
"SELECT id FROM squads WHERE club_id = ? ORDER BY updated_at DESC LIMIT 1",
)
.bind(club_id)
.fetch_optional(pool)
.await?,
) )
.bind(club_id)
.fetch_optional(pool)
.await?)
} }
/// The owned card assigned as the manager of `club_id`'s active squad, if any. /// The owned card assigned as the manager of `club_id`'s active squad, if any.
@@ -182,11 +180,7 @@ pub async fn get_squad_manager_for_squad(
/// belong to `club_id`, so a client can neither manage another club's squad nor /// belong to `club_id`, so a client can neither manage another club's squad nor
/// assign a card it does not own. One manager per squad (the PK REPLACE), so a /// assign a card it does not own. One manager per squad (the PK REPLACE), so a
/// re-assignment never accumulates duplicate rows. /// re-assignment never accumulates duplicate rows.
pub async fn set_squad_manager( pub async fn set_squad_manager(pool: &Pool, club_id: &str, owned_card_id: &str) -> AppResult<()> {
pool: &Pool,
club_id: &str,
owned_card_id: &str,
) -> AppResult<()> {
let squad_id = active_squad_id(pool, club_id) let squad_id = active_squad_id(pool, club_id)
.await? .await?
.ok_or_else(|| AppError::NotFound("club has no squad to assign a manager to".into()))?; .ok_or_else(|| AppError::NotFound("club has no squad to assign a manager to".into()))?;
@@ -304,7 +298,9 @@ mod tests {
let (dir, url, pool) = fixture().await; let (dir, url, pool) = fixture().await;
// SAVE. // SAVE.
set_squad_manager(&pool, "club-a", "mgr").await.expect("assign"); set_squad_manager(&pool, "club-a", "mgr")
.await
.expect("assign");
// RELOAD (same pool). // RELOAD (same pool).
let got = get_squad_manager(&pool, "club-a").await.unwrap(); let got = get_squad_manager(&pool, "club-a").await.unwrap();
assert_eq!(got.as_ref().map(|c| c.id.as_str()), Some("mgr")); assert_eq!(got.as_ref().map(|c| c.id.as_str()), Some("mgr"));
+38 -10
View File
@@ -438,7 +438,9 @@ async fn complete_match_inner(
)); ));
} }
if req.match_identity.trim().is_empty() { if req.match_identity.trim().is_empty() {
return Err(AppError::BadRequest("match_identity must not be empty".into())); return Err(AppError::BadRequest(
"match_identity must not be empty".into(),
));
} }
let result = req.result; let result = req.result;
@@ -905,7 +907,11 @@ mod match_completion_tests {
assert_eq!(r.xp_awarded, XP_LOSS); assert_eq!(r.xp_awarded, XP_LOSS);
assert_eq!(r.match_record.outcome, "dnf"); assert_eq!(r.match_record.outcome, "dnf");
assert_eq!(stat(&fx.pool, "matches_dnf").await, 1); assert_eq!(stat(&fx.pool, "matches_dnf").await, 1);
assert_eq!(stat(&fx.pool, "matches_lost").await, 0, "DNF is not a loss row"); assert_eq!(
stat(&fx.pool, "matches_lost").await,
0,
"DNF is not a loss row"
);
assert_eq!(stat(&fx.pool, "matches_played").await, 1); assert_eq!(stat(&fx.pool, "matches_played").await, 1);
assert_eq!(coins(&fx.pool).await, START_COINS + COINS_LOSS); assert_eq!(coins(&fx.pool).await, START_COINS + COINS_LOSS);
} }
@@ -922,7 +928,11 @@ mod match_completion_tests {
assert_eq!(r.match_record.outcome, "no_contest"); assert_eq!(r.match_record.outcome, "no_contest");
assert_eq!(coins(&fx.pool).await, START_COINS, "no coins for a void"); assert_eq!(coins(&fx.pool).await, START_COINS, "no coins for a void");
assert_eq!(xp(&fx.pool).await, 0); assert_eq!(xp(&fx.pool).await, 0);
assert_eq!(stat(&fx.pool, "matches_played").await, 0, "not counted as played"); assert_eq!(
stat(&fx.pool, "matches_played").await,
0,
"not counted as played"
);
// Still recorded for history + idempotency. // Still recorded for history + idempotency.
assert_eq!(count(&fx.pool, "matches").await, 1); assert_eq!(count(&fx.pool, "matches").await, 1);
assert_eq!(count(&fx.pool, "match_completions").await, 1); assert_eq!(count(&fx.pool, "match_completions").await, 1);
@@ -1089,7 +1099,11 @@ mod match_completion_tests {
.await .await
.unwrap(); .unwrap();
assert!(retry.applied, "{fp:?} retry-after-fault must apply"); assert!(retry.applied, "{fp:?} retry-after-fault must apply");
assert_eq!(coins(&fx.pool).await, START_COINS + COINS_WIN, "{fp:?} retry"); assert_eq!(
coins(&fx.pool).await,
START_COINS + COINS_WIN,
"{fp:?} retry"
);
} }
} }
@@ -1129,9 +1143,16 @@ mod match_completion_tests {
]; ];
let achs = vec![ach("first_win", "matches_won", 1, 50)]; let achs = vec![ach("first_win", "matches_won", 1, 50)];
let first = complete_match(&fx.pool, PROFILE, CLUB, &req("m", MatchResultKind::Win, 2, 0), &objs, &achs) let first = complete_match(
.await &fx.pool,
.unwrap(); PROFILE,
CLUB,
&req("m", MatchResultKind::Win, 2, 0),
&objs,
&achs,
)
.await
.unwrap();
assert!(first.applied); assert!(first.applied);
assert_eq!(first.objectives_updated.len(), 2); assert_eq!(first.objectives_updated.len(), 2);
assert_eq!(first.achievements_unlocked.len(), 1); assert_eq!(first.achievements_unlocked.len(), 1);
@@ -1140,9 +1161,16 @@ mod match_completion_tests {
assert_eq!(count(&fx.pool, "player_achievements").await, 1); assert_eq!(count(&fx.pool, "player_achievements").await, 1);
// Replay: no double objectives/achievements/coins. // Replay: no double objectives/achievements/coins.
let replay = complete_match(&fx.pool, PROFILE, CLUB, &req("m", MatchResultKind::Win, 2, 0), &objs, &achs) let replay = complete_match(
.await &fx.pool,
.unwrap(); PROFILE,
CLUB,
&req("m", MatchResultKind::Win, 2, 0),
&objs,
&achs,
)
.await
.unwrap();
assert!(!replay.applied); assert!(!replay.applied);
assert!(replay.objectives_updated.is_empty()); assert!(replay.objectives_updated.is_empty());
assert!(replay.achievements_unlocked.is_empty()); assert!(replay.achievements_unlocked.is_empty());
+1 -1
View File
@@ -6,9 +6,9 @@ use crate::{
}, },
}; };
use anyhow::Context; use anyhow::Context;
use sqlx::{Sqlite, Transaction};
use std::path::Path; use std::path::Path;
use uuid::Uuid; use uuid::Uuid;
use sqlx::{Sqlite, Transaction};
pub fn load_objective_definitions(data_dir: &str) -> anyhow::Result<Vec<ObjectiveDefinition>> { pub fn load_objective_definitions(data_dir: &str) -> anyhow::Result<Vec<ObjectiveDefinition>> {
let dir = Path::new(data_dir).join("objectives"); let dir = Path::new(data_dir).join("objectives");