Phase 25: division leaderboard, market trade history
CI / Build, lint & test (push) Failing after 2m10s
CI / Build, lint & test (push) Failing after 2m10s
- Market: record buy/sell history in market_history table; expose via GET /market/trade-history (last 30 events, newest first) - Division: GET /division/leaderboard returns 10-club table with 9 seeded NPC entries + player row, sorted by pts; stable within a season - rand feature small_rng enabled in Cargo.toml for SmallRng use - 3 new integration tests (leaderboard count, sort order, empty trade history) - Core: 96 tests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+72
-4
@@ -160,14 +160,37 @@ pub async fn buy_listing(
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
card_db
|
||||
let card = card_db
|
||||
.get(&listing.card_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| AppError::NotFound("card definition not found".into()))
|
||||
.ok_or_else(|| AppError::NotFound("card definition not found".into()))?;
|
||||
|
||||
// Record trade history (best-effort — never abort the buy on failure)
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO market_history (id, club_id, card_id, card_name, card_overall, \
|
||||
trade_type, price, traded_at) VALUES (?,?,?,?,?,?,?,?)",
|
||||
)
|
||||
.bind(Uuid::new_v4().to_string())
|
||||
.bind(club_id)
|
||||
.bind(&card.id)
|
||||
.bind(&card.name)
|
||||
.bind(card.overall as i64)
|
||||
.bind("buy")
|
||||
.bind(listing.price)
|
||||
.bind(chrono::Utc::now().to_rfc3339())
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
Ok(card)
|
||||
}
|
||||
|
||||
pub async fn sell_card(pool: &Pool, club_id: &str, req: &SellCardRequest) -> AppResult<i64> {
|
||||
let _owned = sqlx::query_as::<_, crate::models::card::OwnedCard>(
|
||||
pub async fn sell_card(
|
||||
pool: &Pool,
|
||||
card_db: &CardDb,
|
||||
club_id: &str,
|
||||
req: &SellCardRequest,
|
||||
) -> AppResult<i64> {
|
||||
let owned = sqlx::query_as::<_, crate::models::card::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 = ?",
|
||||
@@ -185,9 +208,54 @@ pub async fn sell_card(pool: &Pool, club_id: &str, req: &SellCardRequest) -> App
|
||||
|
||||
let coins = (req.price as f64 * 0.4) as i64;
|
||||
let new_balance = club::add_coins(pool, club_id, coins).await?;
|
||||
|
||||
// Record trade history
|
||||
if let Some(card) = card_db.get(&owned.card_id) {
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO market_history (id, club_id, card_id, card_name, card_overall, \
|
||||
trade_type, price, traded_at) VALUES (?,?,?,?,?,?,?,?)",
|
||||
)
|
||||
.bind(Uuid::new_v4().to_string())
|
||||
.bind(club_id)
|
||||
.bind(&card.id)
|
||||
.bind(&card.name)
|
||||
.bind(card.overall as i64)
|
||||
.bind("sell")
|
||||
.bind(coins)
|
||||
.bind(chrono::Utc::now().to_rfc3339())
|
||||
.execute(pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(new_balance)
|
||||
}
|
||||
|
||||
/// Return the last 30 buy/sell events for this club, newest first.
|
||||
pub async fn get_trade_history(pool: &Pool, club_id: &str) -> AppResult<Vec<serde_json::Value>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT card_name, card_overall, trade_type, price, traded_at \
|
||||
FROM market_history WHERE club_id = ? ORDER BY traded_at DESC LIMIT 30",
|
||||
)
|
||||
.bind(club_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
use sqlx::Row;
|
||||
let trades = rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
serde_json::json!({
|
||||
"card_name": r.get::<String, _>("card_name"),
|
||||
"card_overall": r.get::<i64, _>("card_overall"),
|
||||
"trade_type": r.get::<String, _>("trade_type"),
|
||||
"price": r.get::<i64, _>("price"),
|
||||
"traded_at": r.get::<String, _>("traded_at"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(trades)
|
||||
}
|
||||
|
||||
fn price_for_card(overall: u8) -> i64 {
|
||||
match overall {
|
||||
85..=u8::MAX => 50_000,
|
||||
|
||||
Reference in New Issue
Block a user