feat(club): semantic owned-item query + FIFA17 filter/pagination fix
Game-independent owned-inventory query (services::inventory::{OwnedItemQuery,
apply_query} + a Quality tier) that filters (AND) -> orders deterministically
(effective_overall desc, owned_card_id asc) -> paginates, wired into
GET /collection. Fixes the FIFA17 My Squad search: the Python oracle applied
only league+team and ignored level/rare/position/nation/start/count (proven by
response sha256 identity across pages -> the request-amplification bug); Core
now applies all proven filters and paginates. rare=SP left UNKNOWN.
Tests: +9 inventory unit, +14 /collection integration (full matrix incl. the
repeated-first-page regression); 9 mutations killed.
Isolated from an unrelated dirty working tree via a clean worktree at eab522a;
touches only the 5 slice files, no unrelated reformatting.
This commit is contained in:
+31
-8
@@ -9,7 +9,11 @@ use crate::{
|
||||
app::AppState,
|
||||
error::{AppError, AppResult},
|
||||
models::card::OwnedCard,
|
||||
services::{club as club_svc, profile as profile_svc},
|
||||
services::{
|
||||
club as club_svc,
|
||||
inventory::{self, OwnedItemQuery, OwnedItemView},
|
||||
profile as profile_svc,
|
||||
},
|
||||
};
|
||||
|
||||
/// Quick-sell value for a card based on overall rating.
|
||||
@@ -93,7 +97,10 @@ pub async fn get_cards(
|
||||
Ok(Json(json!({ "cards": cards, "total": total, "returned": cards.len() })))
|
||||
}
|
||||
|
||||
pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
pub async fn get_collection(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<OwnedItemQuery>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let profile = profile_svc::get_active_profile(&state.pool).await?;
|
||||
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||
|
||||
@@ -104,14 +111,14 @@ pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Val
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let with_defs: Vec<Value> = owned
|
||||
let views: Vec<OwnedItemView> = 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!({
|
||||
let body = json!({
|
||||
"owned_card_id": o.id,
|
||||
"is_loan": o.is_loan,
|
||||
"loan_matches_remaining": o.loan_matches_remaining,
|
||||
@@ -122,14 +129,30 @@ pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Val
|
||||
"effective_overall": effective_overall,
|
||||
"effective_position": effective_position,
|
||||
"card": def,
|
||||
})
|
||||
});
|
||||
OwnedItemView {
|
||||
owned_card_id: o.id.clone(),
|
||||
base_overall: def.overall,
|
||||
effective_overall,
|
||||
position: effective_position.to_string(),
|
||||
nation: def.nation.clone(),
|
||||
league: def.league.clone(),
|
||||
club: def.club.clone(),
|
||||
body,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(
|
||||
json!({ "collection": with_defs, "total": with_defs.len() }),
|
||||
))
|
||||
let page = inventory::apply_query(views, &query);
|
||||
let returned = page.items.len();
|
||||
Ok(Json(json!({
|
||||
"collection": page.items,
|
||||
"total": page.total,
|
||||
"returned": returned,
|
||||
"offset": page.offset,
|
||||
"limit": page.limit,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Quick-sell an owned card for instant coins. The card is removed from the collection.
|
||||
|
||||
Reference in New Issue
Block a user