From 3d50da35897bc485b8109bb055f938cfdf3b3249 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 25 Jun 2026 17:25:37 -0700 Subject: [PATCH] Phase 15: add GET /packs/store endpoint Returns all pack definitions with id, name, description, cost_coins, and total_cards so the dashboard can render a purchasable pack store without needing to know the definitions at compile time. Two new tests: store listing shape and buy-then-open round-trip. Co-Authored-By: Claude Sonnet 4.6 --- src/app.rs | 1 + src/routes/packs.rs | 19 +++++++++++++++++++ tests/integration_test.rs | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/app.rs b/src/app.rs index 7a3681c..6cd90ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -152,6 +152,7 @@ pub async fn build(pool: Pool, cfg: Config) -> Result { get(routes::upgrades::get_chemistry_styles), ) .route("/packs", get(routes::packs::get_packs)) + .route("/packs/store", get(routes::packs::get_pack_store)) .route("/packs/history", get(routes::packs::get_pack_history)) .route("/packs/buy", post(routes::packs::post_buy_pack)) .route("/packs/open/:pack_id", post(routes::packs::post_open_pack)) diff --git a/src/routes/packs.rs b/src/routes/packs.rs index 0f7c2b6..42ec4a5 100644 --- a/src/routes/packs.rs +++ b/src/routes/packs.rs @@ -35,6 +35,25 @@ pub async fn post_buy_pack( )) } +/// 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) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; diff --git a/tests/integration_test.rs b/tests/integration_test.rs index ce91694..79a5a0c 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1409,3 +1409,43 @@ async fn test_rivals_reward_increments_week_counter() { assert_eq!(s, StatusCode::OK, "{json}"); assert_eq!(json["week_number"], 2, "second claim should be week 2"); } + +// ── Phase 15: Pack Store ────────────────────────────────────────────────────── + +#[tokio::test] +async fn test_pack_store_lists_definitions() { + let app = build_test_app().await; + auth(&app, "StorePlayer").await; + + let (status, json) = json_get(&app, "/packs/store").await; + assert_eq!(status, StatusCode::OK, "{json}"); + let packs = json["packs"].as_array().expect("packs array"); + assert!(!packs.is_empty(), "store should have at least one pack definition"); + let first = &packs[0]; + assert!(first["id"].is_string(), "id missing"); + assert!(first["name"].is_string(), "name missing"); + assert!(first["cost_coins"].is_number(), "cost_coins missing"); + assert!(first["total_cards"].is_number(), "total_cards missing"); +} + +#[tokio::test] +async fn test_pack_store_buy_then_opens() { + let app = build_test_app().await; + auth(&app, "StoreBuyer").await; + + // Get cheapest pack from store + let (_, store) = json_get(&app, "/packs/store").await; + let packs = store["packs"].as_array().unwrap(); + let cheapest = packs.iter().min_by_key(|p| p["cost_coins"].as_i64().unwrap_or(i64::MAX)).unwrap(); + let def_id = cheapest["id"].as_str().unwrap(); + + // Buy it + let (status, json) = json_post(&app, "/packs/buy", serde_json::json!({ "pack_definition_id": def_id })).await; + assert_eq!(status, StatusCode::OK, "buy failed: {json}"); + let pack_id = json["pack"]["id"].as_str().expect("pack id").to_string(); + + // Open it + let (status, json) = json_post(&app, &format!("/packs/open/{pack_id}"), serde_json::json!({})).await; + assert_eq!(status, StatusCode::OK, "open failed: {json}"); + assert!(json["cards"].as_array().map(|a| !a.is_empty()).unwrap_or(false), "opened pack should contain cards"); +}