Phase 15: add GET /packs/store endpoint
CI / Build, lint & test (push) Failing after 52s

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 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 17:25:37 -07:00
parent 26b8e9efef
commit 3d50da3589
3 changed files with 60 additions and 0 deletions
+1
View File
@@ -152,6 +152,7 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
get(routes::upgrades::get_chemistry_styles), get(routes::upgrades::get_chemistry_styles),
) )
.route("/packs", get(routes::packs::get_packs)) .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/history", get(routes::packs::get_pack_history))
.route("/packs/buy", post(routes::packs::post_buy_pack)) .route("/packs/buy", post(routes::packs::post_buy_pack))
.route("/packs/open/:pack_id", post(routes::packs::post_open_pack)) .route("/packs/open/:pack_id", post(routes::packs::post_open_pack))
+19
View File
@@ -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<AppState>) -> AppResult<Json<Value>> {
let store: Vec<Value> = 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<AppState>) -> AppResult<Json<Value>> { pub async fn get_packs(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?; let profile = profile_svc::get_active_profile(&state.pool).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?;
+40
View File
@@ -1409,3 +1409,43 @@ async fn test_rivals_reward_increments_week_counter() {
assert_eq!(s, StatusCode::OK, "{json}"); assert_eq!(s, StatusCode::OK, "{json}");
assert_eq!(json["week_number"], 2, "second claim should be week 2"); 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");
}