use crate::extractors::GameId; use axum::{ extract::{Path, State}, Json, }; use serde::Deserialize; use serde_json::{json, Value}; use crate::{ app::AppState, error::AppResult, models::pack::PackOpenResult, services::{club as club_svc, objective, pack as pack_svc, profile as profile_svc, statistics}, }; #[derive(Deserialize)] pub struct BuyPackRequest { pub pack_definition_id: String, } pub async fn post_buy_pack( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { 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 pack = pack_svc::buy_pack( &state.pool, &state.pack_defs, &club.id, &req.pack_definition_id, ) .await?; Ok(Json( json!({ "pack": pack, "message": "Pack purchased successfully" }), )) } /// List all purchasable pack definitions with their coin costs. pub async fn get_pack_store(State(state): State) -> AppResult> { let store: Vec = state .pack_defs .iter() .map(|d| { let total_cards: u8 = d.slots.iter().map(|s| s.count).sum(); json!({ "id": d.id, "name": d.name, "description": d.description, "cost_coins": d.cost_coins, "total_cards": total_cards, }) }) .collect(); Ok(Json(json!({ "packs": store }))) } pub async fn get_packs(State(state): State, game: GameId) -> AppResult> { 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 packs = pack_svc::get_unopened_packs(&state.pool, &club.id).await?; let with_defs: Vec = packs .iter() .map(|p| { let def = state.pack_defs.iter().find(|d| d.id == p.definition_id); json!({ "pack_id": p.id, "definition_id": p.definition_id, "name": def.map(|d| &d.name), "description": def.map(|d| &d.description), "created_at": p.created_at, }) }) .collect(); Ok(Json(json!({ "packs": with_defs }))) } /// Return recently opened packs with the card IDs they contained. pub async fn get_pack_history(State(state): State, game: GameId) -> AppResult> { 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 opened = sqlx::query_as::<_, crate::models::pack::Pack>( "SELECT id, club_id, definition_id, opened, created_at, opened_cards, opened_at \ FROM packs WHERE club_id = ? AND opened = 1 ORDER BY opened_at DESC LIMIT 50", ) .bind(&club.id) .fetch_all(&state.pool) .await?; let history: Vec = opened .iter() .map(|p| { let def = state.pack_defs.iter().find(|d| d.id == p.definition_id); // Expand card_ids into full card definitions let cards: Vec = p .opened_cards .as_deref() .and_then(|s| serde_json::from_str::>(s).ok()) .unwrap_or_default() .iter() .map(|id| { json!({ "card_id": id, "card": state.card_db.get(id), }) }) .collect(); json!({ "pack_id": p.id, "definition_id": p.definition_id, "name": def.map(|d| &d.name), "opened_at": p.opened_at, "cards": cards, }) }) .collect(); Ok(Json(json!({ "history": history, "total": history.len() }))) } pub async fn post_open_pack( State(state): State, game: GameId, Path(pack_id): Path, ) -> AppResult> { 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 result = pack_svc::open_pack( &state.pool, &state.card_db, &state.pack_defs, &club.id, &pack_id, ) .await?; statistics::increment_packs_opened(&state.pool, &profile.id).await?; objective::increment_metric(&state.pool, &profile.id, &state.obj_defs, "packsopened", 1) .await?; let _ = crate::services::achievement::check_and_unlock( &state.pool, &state.achievement_defs, &profile.id, &club.id, ).await; Ok(Json(result)) }