style(core): apply cargo fmt across routes, services, models, tests
CI / Build, lint & test (push) Successful in 2m19s
CI / Build, lint & test (push) Successful in 2m19s
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.
This commit is contained in:
+3
-3
@@ -1,19 +1,19 @@
|
|||||||
pub mod achievement;
|
pub mod achievement;
|
||||||
pub mod card;
|
pub mod card;
|
||||||
pub mod chemistry_style;
|
pub mod chemistry_style;
|
||||||
pub mod notification;
|
|
||||||
pub mod club;
|
pub mod club;
|
||||||
pub mod draft;
|
pub mod draft;
|
||||||
|
pub mod event;
|
||||||
pub mod fut_champs;
|
pub mod fut_champs;
|
||||||
pub mod game_ext;
|
pub mod game_ext;
|
||||||
pub mod event;
|
|
||||||
pub mod season;
|
|
||||||
pub mod market;
|
pub mod market;
|
||||||
pub mod match_result;
|
pub mod match_result;
|
||||||
|
pub mod notification;
|
||||||
pub mod objective;
|
pub mod objective;
|
||||||
pub mod pack;
|
pub mod pack;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod reward;
|
pub mod reward;
|
||||||
pub mod sbc;
|
pub mod sbc;
|
||||||
|
pub mod season;
|
||||||
pub mod squad;
|
pub mod squad;
|
||||||
pub mod statistics;
|
pub mod statistics;
|
||||||
|
|||||||
@@ -8,12 +8,19 @@ use crate::{
|
|||||||
services::{achievement as ach_svc, club as club_svc, profile as profile_svc},
|
services::{achievement as ach_svc, club as club_svc, profile as profile_svc},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_achievements(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_achievements(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 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 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!({
|
Ok(Json(json!({
|
||||||
"achievements": achievements,
|
"achievements": achievements,
|
||||||
"earned": earned,
|
"earned": earned,
|
||||||
|
|||||||
+4
-1
@@ -34,7 +34,10 @@ pub async fn post_auth_local(
|
|||||||
|
|
||||||
/// GET /auth/status — lightweight check: does a profile exist?
|
/// GET /auth/status — lightweight check: does a profile exist?
|
||||||
/// Returns 200 `{ "has_profile": true/false }` without erroring.
|
/// Returns 200 `{ "has_profile": true/false }` without erroring.
|
||||||
pub async fn get_auth_status(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_auth_status(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM profiles WHERE game_id = ?")
|
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM profiles WHERE game_id = ?")
|
||||||
.bind(game.as_str())
|
.bind(game.as_str())
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
|
|||||||
+19
-14
@@ -11,8 +11,7 @@ use crate::{
|
|||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
models::card::OwnedCard,
|
models::card::OwnedCard,
|
||||||
services::{
|
services::{
|
||||||
club as club_svc,
|
club as club_svc, economy as economy_svc,
|
||||||
economy as economy_svc,
|
|
||||||
inventory::{self, OwnedItemQuery, OwnedItemView},
|
inventory::{self, OwnedItemQuery, OwnedItemView},
|
||||||
profile as profile_svc,
|
profile as profile_svc,
|
||||||
},
|
},
|
||||||
@@ -20,11 +19,17 @@ use crate::{
|
|||||||
|
|
||||||
/// Quick-sell value for a card based on overall rating.
|
/// Quick-sell value for a card based on overall rating.
|
||||||
fn quick_sell_coins(overall: u8) -> i64 {
|
fn quick_sell_coins(overall: u8) -> i64 {
|
||||||
if overall >= 85 { 1500 }
|
if overall >= 85 {
|
||||||
else if overall >= 80 { 900 }
|
1500
|
||||||
else if overall >= 75 { 600 }
|
} else if overall >= 80 {
|
||||||
else if overall >= 65 { 300 }
|
900
|
||||||
else { 150 }
|
} else if overall >= 75 {
|
||||||
|
600
|
||||||
|
} else if overall >= 65 {
|
||||||
|
300
|
||||||
|
} else {
|
||||||
|
150
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -96,7 +101,9 @@ pub async fn get_cards(
|
|||||||
cards.truncate(limit);
|
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(
|
pub async fn get_collection(
|
||||||
@@ -119,8 +126,7 @@ pub async fn get_collection(
|
|||||||
.filter_map(|o| {
|
.filter_map(|o| {
|
||||||
state.card_db.get(&o.card_id).map(|def| {
|
state.card_db.get(&o.card_id).map(|def| {
|
||||||
let effective_overall = def.overall as i64 + o.training_bonus;
|
let effective_overall = def.overall as i64 + o.training_bonus;
|
||||||
let effective_position =
|
let effective_position = o.position_override.as_deref().unwrap_or(&def.position);
|
||||||
o.position_override.as_deref().unwrap_or(&def.position);
|
|
||||||
let body = json!({
|
let body = json!({
|
||||||
"owned_card_id": o.id,
|
"owned_card_id": o.id,
|
||||||
"is_loan": o.is_loan,
|
"is_loan": o.is_loan,
|
||||||
@@ -178,10 +184,9 @@ pub async fn delete_owned_card(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| AppError::NotFound(format!("owned card '{owned_card_id}' not found")))?;
|
.ok_or_else(|| AppError::NotFound(format!("owned card '{owned_card_id}' not found")))?;
|
||||||
|
|
||||||
let card = state
|
let card = state.card_db.get(&owned.card_id).ok_or_else(|| {
|
||||||
.card_db
|
AppError::NotFound(format!("card definition '{}' missing", owned.card_id))
|
||||||
.get(&owned.card_id)
|
})?;
|
||||||
.ok_or_else(|| AppError::NotFound(format!("card definition '{}' missing", owned.card_id)))?;
|
|
||||||
|
|
||||||
let coins = quick_sell_coins(card.overall);
|
let coins = quick_sell_coins(card.overall);
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -3,7 +3,9 @@ use crate::{
|
|||||||
app::AppState,
|
app::AppState,
|
||||||
error::AppResult,
|
error::AppResult,
|
||||||
models::club::Club,
|
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 axum::{extract::State, Json};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -38,7 +40,10 @@ pub async fn put_club(
|
|||||||
Ok(Json(json!({ "club": updated })))
|
Ok(Json(json!({ "club": updated })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_checkin_status(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_checkin_status(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
let status = checkin_svc::get_status(&state.pool, &profile.id).await?;
|
let status = checkin_svc::get_status(&state.pool, &profile.id).await?;
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -67,9 +72,8 @@ pub async fn get_milestones(State(state): State<AppState>, game: GameId) -> AppR
|
|||||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
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 stats = stats_svc::get_or_create(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
let seasons_completed: i64 = sqlx::query_scalar(
|
let seasons_completed: i64 =
|
||||||
"SELECT COUNT(*) FROM season_history WHERE profile_id = ?",
|
sqlx::query_scalar("SELECT COUNT(*) FROM season_history WHERE profile_id = ?")
|
||||||
)
|
|
||||||
.bind(&profile.id)
|
.bind(&profile.id)
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
.await
|
.await
|
||||||
@@ -83,25 +87,21 @@ pub async fn get_milestones(State(state): State<AppState>, game: GameId) -> AppR
|
|||||||
.await
|
.await
|
||||||
.unwrap_or(10);
|
.unwrap_or(10);
|
||||||
|
|
||||||
let cards_owned: i64 = sqlx::query_scalar(
|
let cards_owned: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE club_id = ?")
|
||||||
"SELECT COUNT(*) FROM owned_cards WHERE club_id = ?",
|
|
||||||
)
|
|
||||||
.bind(&club.id)
|
.bind(&club.id)
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let sbcs_completed: i64 = sqlx::query_scalar(
|
let sbcs_completed: i64 =
|
||||||
"SELECT COUNT(*) FROM sbc_submissions WHERE club_id = ? AND passed = 1",
|
sqlx::query_scalar("SELECT COUNT(*) FROM sbc_submissions WHERE club_id = ? AND passed = 1")
|
||||||
)
|
|
||||||
.bind(&club.id)
|
.bind(&club.id)
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let total_checkins: i64 = sqlx::query_scalar(
|
let total_checkins: i64 =
|
||||||
"SELECT COUNT(*) FROM daily_checkins WHERE profile_id = ?",
|
sqlx::query_scalar("SELECT COUNT(*) FROM daily_checkins WHERE profile_id = ?")
|
||||||
)
|
|
||||||
.bind(&profile.id)
|
.bind(&profile.id)
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
+33
-9
@@ -37,13 +37,19 @@ pub async fn get_division(State(state): State<AppState>, game: GameId) -> AppRes
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_division_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_division_history(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
let history = season_svc::get_history(&state.pool, &profile.id).await?;
|
let history = season_svc::get_history(&state.pool, &profile.id).await?;
|
||||||
Ok(Json(json!({ "history": history, "total": history.len() })))
|
Ok(Json(json!({ "history": history, "total": history.len() })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_division_leaderboard(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_division_leaderboard(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
let season = season_svc::get_or_create(&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<AppState>, game: GameI
|
|||||||
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
||||||
|
|
||||||
const NPC_NAMES: &[&str] = &[
|
const NPC_NAMES: &[&str] = &[
|
||||||
"Riverside FC", "City Athletic", "County United", "Valley Rangers",
|
"Riverside FC",
|
||||||
"Harbor Town FC", "Mountside City", "Lakewood Athletic", "Eastbrook United",
|
"City Athletic",
|
||||||
"Westfield Rovers", "Northgate FC", "Southport Athletic", "Ironbridge City",
|
"County United",
|
||||||
"Milldale United", "Hillcrest Rangers", "Bayside FC", "Thornfield Athletic",
|
"Valley Rangers",
|
||||||
"Greenhill United", "Coldwater City", "Redbury Rangers", "Ashdown FC",
|
"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
|
// Pick 9 NPC names without repetition using the seeded RNG
|
||||||
@@ -75,8 +96,11 @@ pub async fn get_division_leaderboard(State(state): State<AppState>, game: GameI
|
|||||||
// Quality bias: index 0-2 = stronger, 6-8 = weaker
|
// Quality bias: index 0-2 = stronger, 6-8 = weaker
|
||||||
let quality: f64 = 1.0 - (idx as f64 / 8.0); // 1.0 → 0.0
|
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 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::<f64>() * 0.4)) as i64;
|
let wins =
|
||||||
let losses = (npc_matches as f64 * (1.0 - expected_win_rate) * (0.8 + rng.gen::<f64>() * 0.4)) as i64;
|
(npc_matches as f64 * expected_win_rate * (0.8 + rng.gen::<f64>() * 0.4)) as i64;
|
||||||
|
let losses = (npc_matches as f64
|
||||||
|
* (1.0 - expected_win_rate)
|
||||||
|
* (0.8 + rng.gen::<f64>() * 0.4)) as i64;
|
||||||
let draws = (npc_matches - wins - losses).max(0);
|
let draws = (npc_matches - wins - losses).max(0);
|
||||||
let pts = wins * 3 + draws;
|
let pts = wins * 3 + draws;
|
||||||
json!({
|
json!({
|
||||||
|
|||||||
+4
-2
@@ -45,7 +45,8 @@ pub async fn post_draft_start(
|
|||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
let difficulty = query.difficulty.as_deref().unwrap_or("professional");
|
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))
|
Ok(Json(session))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,8 @@ pub async fn get_draft_session(
|
|||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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))
|
Ok(Json(session))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-10
@@ -9,7 +9,9 @@ use serde_json::{json, Value};
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::AppState,
|
app::AppState,
|
||||||
error::AppResult,
|
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.
|
/// GET /fut-champs — current active session, or null if none.
|
||||||
@@ -24,7 +26,10 @@ pub async fn get_fut_champs(State(state): State<AppState>, game: GameId) -> AppR
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST /fut-champs/start — open a new FUT Champions week.
|
/// POST /fut-champs/start — open a new FUT Champions week.
|
||||||
pub async fn post_start_fut_champs(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn post_start_fut_champs(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
let session = champs_svc::start_session(&state.pool, &profile.id).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.
|
/// GET /fut-champs/history — past sessions, newest first.
|
||||||
pub async fn get_champs_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_champs_history(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
let history = champs_svc::get_history(&state.pool, &profile.id).await?;
|
let history = champs_svc::get_history(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
@@ -107,19 +115,18 @@ pub async fn get_champs_history(State(state): State<AppState>, game: GameId) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST /rivals/claim-weekly — claim weekly Division Rivals reward.
|
/// POST /rivals/claim-weekly — claim weekly Division Rivals reward.
|
||||||
pub async fn post_claim_rivals_reward(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn post_claim_rivals_reward(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
// Ensure a season row exists
|
// Ensure a season row exists
|
||||||
season_svc::get_or_create(&state.pool, &profile.id).await?;
|
season_svc::get_or_create(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
let result = champs_svc::claim_rivals_reward(
|
let result =
|
||||||
&state.pool,
|
champs_svc::claim_rivals_reward(&state.pool, &profile.id, &club.id, &state.pack_defs)
|
||||||
&profile.id,
|
|
||||||
&club.id,
|
|
||||||
&state.pack_defs,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
|
|||||||
+13
-5
@@ -13,14 +13,16 @@ use crate::{
|
|||||||
services::{club as club_svc, market as market_svc, profile as profile_svc},
|
services::{club as club_svc, market as market_svc, profile as profile_svc},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_trade_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_trade_history(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
let trades = market_svc::get_trade_history(&state.pool, &club.id).await?;
|
let trades = market_svc::get_trade_history(&state.pool, &club.id).await?;
|
||||||
Ok(Json(json!({ "trades": trades, "total": trades.len() })))
|
Ok(Json(json!({ "trades": trades, "total": trades.len() })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct MarketQuery {
|
pub struct MarketQuery {
|
||||||
pub min_overall: Option<u8>,
|
pub min_overall: Option<u8>,
|
||||||
@@ -86,11 +88,17 @@ pub async fn post_market_refresh(State(state): State<AppState>) -> AppResult<Jso
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return all active market listings posted by the current player's club.
|
/// Return all active market listings posted by the current player's club.
|
||||||
pub async fn get_my_listings(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_my_listings(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 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?;
|
let listings =
|
||||||
Ok(Json(json!({ "listings": listings, "total": listings.len() })))
|
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.
|
/// Cancel a player-posted listing and return the card to the collection.
|
||||||
|
|||||||
@@ -71,8 +71,14 @@ pub async fn post_match_result(
|
|||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
let result =
|
let result = match_service::process_match(
|
||||||
match_service::process_match(&state.pool, &profile.id, &club.id, &req, &state.obj_defs, &state.achievement_defs)
|
&state.pool,
|
||||||
|
&profile.id,
|
||||||
|
&club.id,
|
||||||
|
&req,
|
||||||
|
&state.obj_defs,
|
||||||
|
&state.achievement_defs,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
|
|||||||
+1
-1
@@ -5,8 +5,8 @@ pub mod club;
|
|||||||
pub mod division;
|
pub mod division;
|
||||||
pub mod draft;
|
pub mod draft;
|
||||||
pub mod economy;
|
pub mod economy;
|
||||||
pub mod fut_champs;
|
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod fut_champs;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod market;
|
pub mod market;
|
||||||
pub mod matches;
|
pub mod matches;
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use serde_json::{json, Value};
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::AppState,
|
app::AppState,
|
||||||
error::{AppError, AppResult},
|
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
|
/// GET /notifications
|
||||||
@@ -17,7 +19,10 @@ use crate::{
|
|||||||
/// notifications (unclaimed objectives, expiring loans, season ending soon).
|
/// notifications (unclaimed objectives, expiring loans, season ending soon).
|
||||||
/// Persistent notifications carry an `id` and `is_read` flag; dynamic ones
|
/// Persistent notifications carry an `id` and `is_read` flag; dynamic ones
|
||||||
/// have `id: null` and are always considered unread.
|
/// have `id: null` and are always considered unread.
|
||||||
pub async fn get_notifications(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_notifications(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
@@ -93,14 +98,16 @@ pub async fn get_notifications(State(state): State<AppState>, game: GameId) -> A
|
|||||||
// Use "type" key for compatibility with dashboard and existing tests.
|
// Use "type" key for compatibility with dashboard and existing tests.
|
||||||
let all: Vec<Value> = persistent
|
let all: Vec<Value> = persistent
|
||||||
.iter()
|
.iter()
|
||||||
.map(|n| json!({
|
.map(|n| {
|
||||||
|
json!({
|
||||||
"id": n.id,
|
"id": n.id,
|
||||||
"type": n.kind,
|
"type": n.kind,
|
||||||
"title": n.title,
|
"title": n.title,
|
||||||
"body": n.body,
|
"body": n.body,
|
||||||
"is_read": n.is_read,
|
"is_read": n.is_read,
|
||||||
"created_at": n.created_at,
|
"created_at": n.created_at,
|
||||||
}))
|
})
|
||||||
|
})
|
||||||
.chain(dynamic.iter().cloned())
|
.chain(dynamic.iter().cloned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -125,9 +132,7 @@ pub async fn mark_notification_read(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST /notifications/read-all
|
/// POST /notifications/read-all
|
||||||
pub async fn mark_all_notifications_read(
|
pub async fn mark_all_notifications_read(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> AppResult<Json<Value>> {
|
|
||||||
let count = notif_svc::mark_all_read(&state.pool).await?;
|
let count = notif_svc::mark_all_read(&state.pool).await?;
|
||||||
Ok(Json(json!({ "marked_read": count })))
|
Ok(Json(json!({ "marked_read": count })))
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -79,7 +79,10 @@ pub async fn get_packs(State(state): State<AppState>, game: GameId) -> AppResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return recently opened packs with the card IDs they contained.
|
/// Return recently opened packs with the card IDs they contained.
|
||||||
pub async fn get_pack_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
pub async fn get_pack_history(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 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)
|
objective::increment_metric(&state.pool, &profile.id, &state.obj_defs, "packsopened", 1)
|
||||||
.await?;
|
.await?;
|
||||||
let _ = crate::services::achievement::check_and_unlock(
|
let _ = crate::services::achievement::check_and_unlock(
|
||||||
&state.pool, &state.achievement_defs, &profile.id, &club.id,
|
&state.pool,
|
||||||
).await;
|
&state.achievement_defs,
|
||||||
|
&profile.id,
|
||||||
|
&club.id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,13 +72,8 @@ pub async fn post_change_position(
|
|||||||
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
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 club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
|
|
||||||
let updated = upgrade_svc::change_position(
|
let updated =
|
||||||
&state.pool,
|
upgrade_svc::change_position(&state.pool, &club.id, &owned_card_id, &req.position).await?;
|
||||||
&club.id,
|
|
||||||
&owned_card_id,
|
|
||||||
&req.position,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let card_def = state.card_db.get(&updated.card_id);
|
let card_def = state.card_db.get(&updated.card_id);
|
||||||
|
|
||||||
|
|||||||
+41
-34
@@ -29,58 +29,62 @@ pub fn load_achievement_definitions(data_dir: &str) -> anyhow::Result<Vec<Achiev
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Query the current value for the given trigger metric.
|
/// Query the current value for the given trigger metric.
|
||||||
async fn metric_value(pool: &Pool, profile_id: &str, club_id: &str, trigger: &str) -> AppResult<i64> {
|
async fn metric_value(
|
||||||
let v: i64 = match trigger {
|
pool: &Pool,
|
||||||
"matches_played" => sqlx::query_scalar(
|
profile_id: &str,
|
||||||
"SELECT matches_played FROM statistics WHERE profile_id = ?",
|
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 = ?")
|
||||||
.bind(profile_id)
|
.bind(profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0),
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
"matches_won" => sqlx::query_scalar(
|
"matches_won" => {
|
||||||
"SELECT matches_won FROM statistics WHERE profile_id = ?",
|
sqlx::query_scalar("SELECT matches_won FROM statistics WHERE profile_id = ?")
|
||||||
)
|
|
||||||
.bind(profile_id)
|
.bind(profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0),
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
"goals_scored" => sqlx::query_scalar(
|
"goals_scored" => {
|
||||||
"SELECT goals_scored FROM statistics WHERE profile_id = ?",
|
sqlx::query_scalar("SELECT goals_scored FROM statistics WHERE profile_id = ?")
|
||||||
)
|
|
||||||
.bind(profile_id)
|
.bind(profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0),
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
"packs_opened" => sqlx::query_scalar(
|
"packs_opened" => {
|
||||||
"SELECT packs_opened FROM statistics WHERE profile_id = ?",
|
sqlx::query_scalar("SELECT packs_opened FROM statistics WHERE profile_id = ?")
|
||||||
)
|
|
||||||
.bind(profile_id)
|
.bind(profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0),
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
"sbcs_completed" => sqlx::query_scalar(
|
"sbcs_completed" => {
|
||||||
"SELECT sbcs_completed FROM statistics WHERE profile_id = ?",
|
sqlx::query_scalar("SELECT sbcs_completed FROM statistics WHERE profile_id = ?")
|
||||||
)
|
|
||||||
.bind(profile_id)
|
.bind(profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0),
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
"cards_owned" => sqlx::query_scalar(
|
"cards_owned" => {
|
||||||
"SELECT COUNT(*) FROM owned_cards WHERE club_id = ?",
|
sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE club_id = ?")
|
||||||
)
|
|
||||||
.bind(club_id)
|
.bind(club_id)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?,
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
"level" => sqlx::query_scalar(
|
"level" => sqlx::query_scalar("SELECT level FROM profiles WHERE id = ?")
|
||||||
"SELECT level FROM profiles WHERE id = ?",
|
|
||||||
)
|
|
||||||
.bind(profile_id)
|
.bind(profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
@@ -165,11 +169,14 @@ pub async fn check_and_unlock(
|
|||||||
club_svc::add_coins(pool, club_id, def.reward_coins).await?;
|
club_svc::add_coins(pool, club_id, def.reward_coins).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = format!(
|
let body = format!("{} Reward: {} coins.", def.description, def.reward_coins);
|
||||||
"{} Reward: {} coins.",
|
let _ = notification::create(
|
||||||
def.description, def.reward_coins
|
pool,
|
||||||
);
|
"achievement",
|
||||||
let _ = notification::create(pool, "achievement", &format!("Achievement: {}", def.title), &body).await;
|
&format!("Achievement: {}", def.title),
|
||||||
|
&body,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
newly_unlocked.push(def.clone());
|
newly_unlocked.push(def.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -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;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const STREAK_COINS: [i64; 7] = [500, 650, 800, 1000, 1200, 1500, 2000];
|
const STREAK_COINS: [i64; 7] = [500, 650, 800, 1000, 1200, 1500, 2000];
|
||||||
@@ -56,11 +60,7 @@ pub async fn get_status(pool: &Pool, profile_id: &str) -> AppResult<CheckinStatu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn claim(
|
pub async fn claim(pool: &Pool, profile_id: &str, club_id: &str) -> AppResult<CheckinResult> {
|
||||||
pool: &Pool,
|
|
||||||
profile_id: &str,
|
|
||||||
club_id: &str,
|
|
||||||
) -> AppResult<CheckinResult> {
|
|
||||||
let row: Option<(i64, String)> = sqlx::query_as(
|
let row: Option<(i64, String)> = sqlx::query_as(
|
||||||
"SELECT streak_day, checked_in_at FROM daily_checkins \
|
"SELECT streak_day, checked_in_at FROM daily_checkins \
|
||||||
WHERE profile_id = ? ORDER BY checked_in_at DESC LIMIT 1",
|
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 idx = (last_streak - 1).rem_euclid(7) as usize;
|
||||||
let coins = STREAK_COINS[idx];
|
let coins = STREAK_COINS[idx];
|
||||||
let pack_def = if idx == 6 { Some(STREAK_7_PACK) } else { None };
|
let pack_def = if idx == 6 { Some(STREAK_7_PACK) } else { None };
|
||||||
|
|||||||
+12
-5
@@ -184,15 +184,23 @@ pub async fn pick_card(
|
|||||||
let (new_candidates, new_status, reward_coins, reward_pack_id, squad_rating, completed_at) =
|
let (new_candidates, new_status, reward_coins, reward_pack_id, squad_rating, completed_at) =
|
||||||
if all_filled {
|
if all_filled {
|
||||||
let (coins, pack, avg) = compute_reward(card_db, &picks);
|
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 {
|
} else {
|
||||||
let min_overall = difficulty_min_overall(&session.difficulty);
|
let min_overall = difficulty_min_overall(&session.difficulty);
|
||||||
let next_pos = &pick_order[next_index];
|
let next_pos = &pick_order[next_index];
|
||||||
let next_candidates =
|
let next_candidates =
|
||||||
pick_candidates(card_db, next_pos, min_overall, CANDIDATES_PER_SLOT);
|
pick_candidates(card_db, next_pos, min_overall, CANDIDATES_PER_SLOT);
|
||||||
{
|
{
|
||||||
let candidates_json = serde_json::to_string(&next_candidates)
|
let candidates_json = serde_json::to_string(&next_candidates).map_err(|e| {
|
||||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("serialization failed: {e}")))?;
|
AppError::Internal(anyhow::anyhow!("serialization failed: {e}"))
|
||||||
|
})?;
|
||||||
(
|
(
|
||||||
Some(candidates_json),
|
Some(candidates_json),
|
||||||
"active".to_string(),
|
"active".to_string(),
|
||||||
@@ -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 {
|
fn render_session(session: &DraftSession, card_db: &CardDb) -> serde_json::Value {
|
||||||
let pick_order: Vec<String> =
|
let pick_order: Vec<String> = serde_json::from_str(&session.pick_order).unwrap_or_default();
|
||||||
serde_json::from_str(&session.pick_order).unwrap_or_default();
|
|
||||||
let picks: Vec<String> = serde_json::from_str(&session.picks).unwrap_or_default();
|
let picks: Vec<String> = serde_json::from_str(&session.picks).unwrap_or_default();
|
||||||
let candidates: Vec<String> = session
|
let candidates: Vec<String> = session
|
||||||
.current_candidates
|
.current_candidates
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ pub async fn get_active_session(
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_session(pool: &Pool, session_id: &str, profile_id: &str) -> AppResult<FutChampsSession> {
|
pub async fn get_session(
|
||||||
|
pool: &Pool,
|
||||||
|
session_id: &str,
|
||||||
|
profile_id: &str,
|
||||||
|
) -> AppResult<FutChampsSession> {
|
||||||
sqlx::query_as::<_, FutChampsSession>(&format!(
|
sqlx::query_as::<_, FutChampsSession>(&format!(
|
||||||
"{SESSION_SELECT} WHERE id = ? AND profile_id = ?"
|
"{SESSION_SELECT} WHERE id = ? AND profile_id = ?"
|
||||||
))
|
))
|
||||||
|
|||||||
+10
-5
@@ -149,7 +149,9 @@ pub async fn buy_listing(
|
|||||||
.await?
|
.await?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
if claimed == 0 {
|
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 {
|
if let Err(e) = club::spend_coins(pool, club_id, listing.price).await {
|
||||||
let _ = sqlx::query("UPDATE market_listings SET sold = 0 WHERE id = ?")
|
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
|
let with_cards = listings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|l| {
|
.filter_map(|l| {
|
||||||
card_db
|
card_db.get(&l.card_id).map(|card| MarketListingWithCard {
|
||||||
.get(&l.card_id)
|
listing: l,
|
||||||
.map(|card| MarketListingWithCard { listing: l, card: card.clone() })
|
card: card.clone(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(with_cards)
|
Ok(with_cards)
|
||||||
@@ -326,7 +329,9 @@ pub async fn cancel_listing(pool: &Pool, club_id: &str, listing_id: &str) -> App
|
|||||||
.bind(club_id)
|
.bind(club_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.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 = ?")
|
sqlx::query("DELETE FROM market_listings WHERE id = ?")
|
||||||
.bind(listing_id)
|
.bind(listing_id)
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ use crate::{
|
|||||||
match_result::{Match, MatchRewardResult, SubmitMatchRequest},
|
match_result::{Match, MatchRewardResult, SubmitMatchRequest},
|
||||||
objective::ObjectiveDefinition,
|
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};
|
use rand::{seq::SliceRandom, Rng};
|
||||||
|
|
||||||
const FORMATIONS: &[&str] = &[
|
const FORMATIONS: &[&str] = &["4-3-3", "4-4-2", "4-2-3-1", "4-1-2-1-2", "3-5-2", "5-3-2"];
|
||||||
"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.
|
/// 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 {
|
let (min_overall, names): (u8, &[&str]) = match difficulty {
|
||||||
"professional" => (
|
"professional" => (
|
||||||
70,
|
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" => (
|
"world_class" => (
|
||||||
78,
|
78,
|
||||||
&["Elite Stars FC", "Champions Select", "Premier XI", "Galaxy United", "Titan FC"],
|
&[
|
||||||
|
"Elite Stars FC",
|
||||||
|
"Champions Select",
|
||||||
|
"Premier XI",
|
||||||
|
"Galaxy United",
|
||||||
|
"Titan FC",
|
||||||
|
],
|
||||||
),
|
),
|
||||||
"legendary" => (
|
"legendary" => (
|
||||||
85,
|
85,
|
||||||
&["Legends United", "Ultimate XI", "Gold Standard FC", "The Icons", "Heritage FC"],
|
&[
|
||||||
|
"Legends United",
|
||||||
|
"Ultimate XI",
|
||||||
|
"Gold Standard FC",
|
||||||
|
"The Icons",
|
||||||
|
"Heritage FC",
|
||||||
|
],
|
||||||
),
|
),
|
||||||
"ultimate" => (
|
"ultimate" => (
|
||||||
90,
|
90,
|
||||||
&["Apex XI", "Pantheon FC", "Gods of FUT", "Invincibles Select", "Eternal XI"],
|
&[
|
||||||
|
"Apex XI",
|
||||||
|
"Pantheon FC",
|
||||||
|
"Gods of FUT",
|
||||||
|
"Invincibles Select",
|
||||||
|
"Eternal XI",
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_ => (
|
_ => (
|
||||||
55,
|
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<usize> = (0..pool.len()).collect();
|
let mut indices: Vec<usize> = (0..pool.len()).collect();
|
||||||
indices.shuffle(&mut rng);
|
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() {
|
let squad_rating = if cards.is_empty() {
|
||||||
0
|
0
|
||||||
@@ -147,11 +182,18 @@ pub async fn process_match(
|
|||||||
|
|
||||||
for ev in &level_ups {
|
for ev in &level_ups {
|
||||||
let body = if let Some(ref pack) = ev.pack_granted {
|
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 {
|
} 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(
|
statistics::record_match(
|
||||||
@@ -195,15 +237,20 @@ pub async fn process_match(
|
|||||||
.find(|d| &d.id == obj_id)
|
.find(|d| &d.id == obj_id)
|
||||||
.map(|d| d.title.as_str())
|
.map(|d| d.title.as_str())
|
||||||
.unwrap_or(obj_id.as_str());
|
.unwrap_or(obj_id.as_str());
|
||||||
let body = format!("\"{}\" is now complete. Claim your reward in Objectives.", display_name);
|
let body = format!(
|
||||||
let _ = notification::create(pool, "objective_complete", "Objective complete!", &body).await;
|
"\"{}\" 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.
|
// Decrement loan matches remaining for squad starters; collect expired loan IDs.
|
||||||
let expired_loans = process_loan_expiry(pool, club_id, &req.squad_id).await?;
|
let expired_loans = process_loan_expiry(pool, club_id, &req.squad_id).await?;
|
||||||
|
|
||||||
for owned_id in &expired_loans {
|
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;
|
let _ = notification::create(pool, "loan_expired", "Loan card expired", &body).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,12 +265,19 @@ pub async fn process_match(
|
|||||||
SeasonResult::Relegated => "Relegated",
|
SeasonResult::Relegated => "Relegated",
|
||||||
SeasonResult::Maintained => "Maintained",
|
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 _ = notification::create(pool, "season_end", "Season complete!", &body).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let achievements_unlocked =
|
let achievements_unlocked = achievement::check_and_unlock(pool, ach_defs, profile_id, club_id)
|
||||||
achievement::check_and_unlock(pool, ach_defs, profile_id, club_id)
|
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,7 @@ pub async fn increment_metric(
|
|||||||
) -> AppResult<Vec<String>> {
|
) -> AppResult<Vec<String>> {
|
||||||
let mut completed_ids = Vec::new();
|
let mut completed_ids = Vec::new();
|
||||||
|
|
||||||
for def in defs
|
for def in defs.iter().filter(|d| d.metric.as_str() == metric) {
|
||||||
.iter()
|
|
||||||
.filter(|d| d.metric.as_str() == metric)
|
|
||||||
{
|
|
||||||
let existing = sqlx::query_as::<_, ObjectiveProgress>(
|
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 = ?"
|
"SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ? AND objective_id = ?"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,9 +73,8 @@ pub async fn open_pack(
|
|||||||
|
|
||||||
// Atomically claim the pack before minting any cards: only one concurrent opener
|
// 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).
|
// flips opened 0->1, so a double-open cannot mint the reward twice (duplication).
|
||||||
let claimed = sqlx::query(
|
let claimed =
|
||||||
"UPDATE packs SET opened = 1 WHERE id = ? AND club_id = ? AND opened = 0",
|
sqlx::query("UPDATE packs SET opened = 1 WHERE id = ? AND club_id = ? AND opened = 0")
|
||||||
)
|
|
||||||
.bind(pack_id)
|
.bind(pack_id)
|
||||||
.bind(club_id)
|
.bind(club_id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
@@ -134,8 +133,8 @@ pub async fn open_pack(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let card_ids_json = serde_json::to_string(&cards.iter().map(|c| &c.id).collect::<Vec<_>>())
|
let card_ids_json =
|
||||||
.unwrap_or_default();
|
serde_json::to_string(&cards.iter().map(|c| &c.id).collect::<Vec<_>>()).unwrap_or_default();
|
||||||
let now = chrono::Utc::now().to_rfc3339();
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
|
|
||||||
sqlx::query("UPDATE packs SET opened_cards = ?, opened_at = ? WHERE id = ?")
|
sqlx::query("UPDATE packs SET opened_cards = ?, opened_at = ? WHERE id = ?")
|
||||||
|
|||||||
@@ -100,7 +100,11 @@ pub async fn add_xp_with_levelup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(profile_id, new_level = lvl, coins, "level up");
|
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)
|
Ok(events)
|
||||||
|
|||||||
+13
-9
@@ -20,9 +20,11 @@ pub async fn get_or_create(pool: &Pool, profile_id: &str) -> AppResult<Season> {
|
|||||||
.bind(&now)
|
.bind(&now)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
fetch(pool, profile_id)
|
fetch(pool, profile_id).await?.ok_or_else(|| {
|
||||||
.await?
|
AppError::Internal(anyhow::anyhow!(
|
||||||
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("season row missing immediately after insert")))
|
"season row missing immediately after insert"
|
||||||
|
))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch(pool: &Pool, profile_id: &str) -> AppResult<Option<Season>> {
|
async fn fetch(pool: &Pool, profile_id: &str) -> AppResult<Option<Season>> {
|
||||||
@@ -68,9 +70,11 @@ pub async fn record_match(
|
|||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let season = fetch(pool, profile_id)
|
let season = fetch(pool, profile_id).await?.ok_or_else(|| {
|
||||||
.await?
|
AppError::Internal(anyhow::anyhow!(
|
||||||
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("season row missing after record_match update")))?;
|
"season row missing after record_match update"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
if !season.is_complete() {
|
if !season.is_complete() {
|
||||||
return Ok((season, None));
|
return Ok((season, None));
|
||||||
@@ -145,9 +149,9 @@ pub async fn record_match(
|
|||||||
pack_awarded: pack_id.map(String::from),
|
pack_awarded: pack_id.map(String::from),
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = fetch(pool, profile_id)
|
let updated = fetch(pool, profile_id).await?.ok_or_else(|| {
|
||||||
.await?
|
AppError::Internal(anyhow::anyhow!("season row missing after season rollover"))
|
||||||
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("season row missing after season rollover")))?;
|
})?;
|
||||||
Ok((updated, Some(summary)))
|
Ok((updated, Some(summary)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ pub const MAX_TRAINING_BONUS: i64 = 3;
|
|||||||
pub const POSITION_CHANGE_COST: i64 = 500;
|
pub const POSITION_CHANGE_COST: i64 = 500;
|
||||||
|
|
||||||
async fn fetch_owned(pool: &Pool, owned_card_id: &str, club_id: &str) -> AppResult<OwnedCard> {
|
async fn fetch_owned(pool: &Pool, owned_card_id: &str, club_id: &str) -> AppResult<OwnedCard> {
|
||||||
sqlx::query_as::<_, OwnedCard>(&format!(
|
sqlx::query_as::<_, OwnedCard>(&format!("{OWNED_CARD_SELECT} WHERE id = ? AND club_id = ?"))
|
||||||
"{OWNED_CARD_SELECT} WHERE id = ? AND club_id = ?"
|
|
||||||
))
|
|
||||||
.bind(owned_card_id)
|
.bind(owned_card_id)
|
||||||
.bind(club_id)
|
.bind(club_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
@@ -64,8 +62,8 @@ pub async fn change_position(
|
|||||||
new_position: &str,
|
new_position: &str,
|
||||||
) -> AppResult<OwnedCard> {
|
) -> AppResult<OwnedCard> {
|
||||||
let valid_positions = [
|
let valid_positions = [
|
||||||
"GK", "RB", "LB", "CB", "RWB", "LWB", "CDM", "CM", "CAM", "RM", "LM", "RW", "LW",
|
"GK", "RB", "LB", "CB", "RWB", "LWB", "CDM", "CM", "CAM", "RM", "LM", "RW", "LW", "CF",
|
||||||
"CF", "ST",
|
"ST",
|
||||||
];
|
];
|
||||||
if !valid_positions.contains(&new_position) {
|
if !valid_positions.contains(&new_position) {
|
||||||
return Err(AppError::BadRequest(format!(
|
return Err(AppError::BadRequest(format!(
|
||||||
|
|||||||
@@ -294,7 +294,12 @@ async fn test_sbc_rejects_duplicate_cards() {
|
|||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.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);
|
assert_eq!(s, StatusCode::OK);
|
||||||
|
|
||||||
let (_, coll) = json_get(&app, "/collection").await;
|
let (_, coll) = json_get(&app, "/collection").await;
|
||||||
@@ -312,9 +317,16 @@ async fn test_sbc_rejects_duplicate_cards() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.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!(
|
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}"
|
"expected a duplicate-card error, got: {result}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user