This commit is contained in:
+60
-59
@@ -5,10 +5,10 @@ use crate::{
|
||||
services::{club as club_svc, notification},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn load_achievement_definitions(data_dir: &str) -> anyhow::Result<Vec<AchievementDefinition>> {
|
||||
let dir = Path::new(data_dir).join("achievements");
|
||||
@@ -196,67 +196,68 @@ async fn metric_value_tx(
|
||||
club_id: &str,
|
||||
trigger: &str,
|
||||
) -> AppResult<i64> {
|
||||
let v: i64 = match trigger {
|
||||
"matches_played" => {
|
||||
sqlx::query_scalar("SELECT matches_played FROM statistics WHERE profile_id = ?")
|
||||
let v: i64 =
|
||||
match trigger {
|
||||
"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)
|
||||
.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 = ?")
|
||||
.unwrap_or(1),
|
||||
"objectives_completed" => sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM objective_progress WHERE profile_id = ? AND completed = 1",
|
||||
)
|
||||
.bind(profile_id)
|
||||
.fetch_optional(&mut **tx)
|
||||
.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(&mut **tx)
|
||||
.await?,
|
||||
"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,
|
||||
};
|
||||
.fetch_one(&mut **tx)
|
||||
.await?,
|
||||
"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)
|
||||
}
|
||||
|
||||
|
||||
+9
-13
@@ -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
|
||||
/// 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>> {
|
||||
Ok(
|
||||
sqlx::query_scalar::<_, String>(
|
||||
"SELECT id FROM squads WHERE club_id = ? ORDER BY updated_at DESC LIMIT 1",
|
||||
)
|
||||
.bind(club_id)
|
||||
.fetch_optional(pool)
|
||||
.await?,
|
||||
Ok(sqlx::query_scalar::<_, String>(
|
||||
"SELECT id FROM squads WHERE club_id = ? ORDER BY updated_at DESC LIMIT 1",
|
||||
)
|
||||
.bind(club_id)
|
||||
.fetch_optional(pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// assign a card it does not own. One manager per squad (the PK REPLACE), so a
|
||||
/// re-assignment never accumulates duplicate rows.
|
||||
pub async fn set_squad_manager(
|
||||
pool: &Pool,
|
||||
club_id: &str,
|
||||
owned_card_id: &str,
|
||||
) -> AppResult<()> {
|
||||
pub async fn set_squad_manager(pool: &Pool, club_id: &str, owned_card_id: &str) -> AppResult<()> {
|
||||
let squad_id = active_squad_id(pool, club_id)
|
||||
.await?
|
||||
.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;
|
||||
|
||||
// SAVE.
|
||||
set_squad_manager(&pool, "club-a", "mgr").await.expect("assign");
|
||||
set_squad_manager(&pool, "club-a", "mgr")
|
||||
.await
|
||||
.expect("assign");
|
||||
// RELOAD (same pool).
|
||||
let got = get_squad_manager(&pool, "club-a").await.unwrap();
|
||||
assert_eq!(got.as_ref().map(|c| c.id.as_str()), Some("mgr"));
|
||||
|
||||
@@ -438,7 +438,9 @@ async fn complete_match_inner(
|
||||
));
|
||||
}
|
||||
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;
|
||||
@@ -905,7 +907,11 @@ mod match_completion_tests {
|
||||
assert_eq!(r.xp_awarded, XP_LOSS);
|
||||
assert_eq!(r.match_record.outcome, "dnf");
|
||||
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!(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!(coins(&fx.pool).await, START_COINS, "no coins for a void");
|
||||
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.
|
||||
assert_eq!(count(&fx.pool, "matches").await, 1);
|
||||
assert_eq!(count(&fx.pool, "match_completions").await, 1);
|
||||
@@ -1089,7 +1099,11 @@ mod match_completion_tests {
|
||||
.await
|
||||
.unwrap();
|
||||
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 first = complete_match(&fx.pool, PROFILE, CLUB, &req("m", MatchResultKind::Win, 2, 0), &objs, &achs)
|
||||
.await
|
||||
.unwrap();
|
||||
let first = complete_match(
|
||||
&fx.pool,
|
||||
PROFILE,
|
||||
CLUB,
|
||||
&req("m", MatchResultKind::Win, 2, 0),
|
||||
&objs,
|
||||
&achs,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(first.applied);
|
||||
assert_eq!(first.objectives_updated.len(), 2);
|
||||
assert_eq!(first.achievements_unlocked.len(), 1);
|
||||
@@ -1140,9 +1161,16 @@ mod match_completion_tests {
|
||||
assert_eq!(count(&fx.pool, "player_achievements").await, 1);
|
||||
|
||||
// Replay: no double objectives/achievements/coins.
|
||||
let replay = complete_match(&fx.pool, PROFILE, CLUB, &req("m", MatchResultKind::Win, 2, 0), &objs, &achs)
|
||||
.await
|
||||
.unwrap();
|
||||
let replay = complete_match(
|
||||
&fx.pool,
|
||||
PROFILE,
|
||||
CLUB,
|
||||
&req("m", MatchResultKind::Win, 2, 0),
|
||||
&objs,
|
||||
&achs,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!replay.applied);
|
||||
assert!(replay.objectives_updated.is_empty());
|
||||
assert!(replay.achievements_unlocked.is_empty());
|
||||
|
||||
@@ -6,9 +6,9 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
|
||||
pub fn load_objective_definitions(data_dir: &str) -> anyhow::Result<Vec<ObjectiveDefinition>> {
|
||||
let dir = Path::new(data_dir).join("objectives");
|
||||
|
||||
Reference in New Issue
Block a user