use axum::{ extract::{Path, Query, State}, Json, }; use serde::Deserialize; use serde_json::{json, Value}; use crate::{ app::AppState, error::{AppError, AppResult}, models::card::OwnedCard, services::{club as club_svc, profile as profile_svc}, }; /// 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 } } #[derive(Debug, Deserialize)] pub struct CardQuery { pub rarity: Option, pub position: Option, pub nation: Option, pub league: Option, pub club: Option, pub min_overall: Option, pub max_overall: Option, pub limit: Option, } pub async fn get_card( State(state): State, Path(card_id): Path, ) -> AppResult> { let card = state .card_db .get(&card_id) .ok_or_else(|| AppError::NotFound(format!("card '{card_id}' not found")))?; Ok(Json(json!({ "card": card }))) } pub async fn get_cards( State(state): State, Query(query): Query, ) -> AppResult> { let mut cards: Vec<_> = state .card_db .all() .into_iter() .filter(|c| { let rarity_ok = query .rarity .as_ref() .map(|r| format!("{:?}", c.rarity).to_lowercase() == r.to_lowercase()) .unwrap_or(true); let pos_ok = query .position .as_ref() .map(|p| c.position.eq_ignore_ascii_case(p)) .unwrap_or(true); let nation_ok = query .nation .as_ref() .map(|n| c.nation.eq_ignore_ascii_case(n)) .unwrap_or(true); let league_ok = query .league .as_ref() .map(|l| c.league.eq_ignore_ascii_case(l)) .unwrap_or(true); let club_ok = query .club .as_ref() .map(|cl| c.club.eq_ignore_ascii_case(cl)) .unwrap_or(true); let min_ok = query.min_overall.map(|m| c.overall >= m).unwrap_or(true); let max_ok = query.max_overall.map(|m| c.overall <= m).unwrap_or(true); rarity_ok && pos_ok && nation_ok && league_ok && club_ok && min_ok && max_ok }) .collect(); cards.sort_by_key(|c| std::cmp::Reverse(c.overall)); let total = cards.len(); if let Some(limit) = query.limit { cards.truncate(limit); } Ok(Json(json!({ "cards": cards, "total": total, "returned": cards.len() }))) } pub async fn get_collection(State(state): State) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let owned = sqlx::query_as::<_, OwnedCard>( "SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at, chemistry_style, position_override, training_bonus FROM owned_cards WHERE club_id = ?" ) .bind(&club.id) .fetch_all(&state.pool) .await?; let with_defs: Vec = owned .iter() .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); json!({ "owned_card_id": o.id, "is_loan": o.is_loan, "loan_matches_remaining": o.loan_matches_remaining, "acquired_at": o.acquired_at, "chemistry_style": o.chemistry_style, "position_override": o.position_override, "training_bonus": o.training_bonus, "effective_overall": effective_overall, "effective_position": effective_position, "card": def, }) }) }) .collect(); Ok(Json( json!({ "collection": with_defs, "total": with_defs.len() }), )) } /// Quick-sell an owned card for instant coins. The card is removed from the collection. pub async fn delete_owned_card( State(state): State, Path(owned_card_id): Path, ) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let owned = sqlx::query_as::<_, OwnedCard>( "SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at, \ chemistry_style, position_override, training_bonus \ FROM owned_cards WHERE id = ? AND club_id = ?", ) .bind(&owned_card_id) .bind(&club.id) .fetch_optional(&state.pool) .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 coins = quick_sell_coins(card.overall); sqlx::query("DELETE FROM owned_cards WHERE id = ?") .bind(&owned_card_id) .execute(&state.pool) .await?; club_svc::add_coins(&state.pool, &club.id, coins).await?; Ok(Json(json!({ "quick_sold": owned_card_id, "card_id": owned.card_id, "coins_received": coins, }))) }