fix(fifa17): preserve owned-pack reveal state for GET /purchased
Closes the reveal contract gap: POST /purchased opens a pack and returns metadata; the client then polls GET /purchased for the opened items. Store BUY returns items inline, but owned reward-pack (e.g. pack 70) opens had no reveal read path, so a real FIFA session would show nothing after opening. Faithful to the Python oracle (fut_store.last_pack / purchased pile): the reveal is the set of owned items currently in the FIFA "purchased" pile — durable, idempotent on repeat GET, cleared per-item when a card is moved to the club, and appended-to by each open. Not a replay cache; presentation state derived from the durable pile store + Core inventory. - pile_store.rs: `list_by_pile(pile) -> Vec<core_item_id>` (reveal membership). - economy_store.rs: `PurchasedPileSink` trait + optional `StoreDeps.purchased`; handle_store_buy/handle_pack_open record each minted item into the "purchased" pile. `shape_purchased_reveal` (pure): filter Core inventory to the purchased pile, shape with the SAME `shape_club_response` /club uses. Grants nothing, consumes no entitlement, allocates no id, moves no coins. - lib.rs: EconomyRoute::PackReveal + classify_economy (GET purchased); BridgedPurchasedSink (records via the runtime bridge from the sync dispatch thread); dispatch reads the pile async + Core inventory sync + pure-shapes. Scoping: single fifa17 profile/club (like the Python oracle), so all sessions share one purchased pile — DIFFERENT-BY-DESIGN vs a per-SID cache, matching the oracle's single-profile model. Tests: pile_store::list_by_pile_filters_and_reflects_moves; and the dispatch E2E now opens pack 70 (entitlement seeded via the Core economy API) and asserts GET /purchased reveals the opened items and is idempotent on repeat. host 71 lib + 2 integration + 24 host_test green; clippy -D warnings + fmt clean.
This commit is contained in:
@@ -130,6 +130,22 @@ impl PileStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every Core owned-instance id currently recorded in `pile`, oldest first.
|
||||
/// Used to render the `GET /purchased` reveal (the FIFA "purchased" pile).
|
||||
pub async fn list_by_pile(&self, pile: &str) -> Result<Vec<String>, PileError> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT core_item_id FROM item_pile WHERE pile = ? ORDER BY updated_at ASC",
|
||||
)
|
||||
.bind(pile)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(db)?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| r.get::<String, _>("core_item_id"))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -184,4 +200,22 @@ mod tests {
|
||||
Some("purchased")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_by_pile_filters_and_reflects_moves() {
|
||||
let db = TempDb::new();
|
||||
let store = PileStore::open(db.path()).await.unwrap();
|
||||
store.set("a", "purchased").await.unwrap();
|
||||
store.set("b", "purchased").await.unwrap();
|
||||
store.set("c", "club").await.unwrap();
|
||||
let mut purchased = store.list_by_pile("purchased").await.unwrap();
|
||||
purchased.sort();
|
||||
assert_eq!(purchased, vec!["a".to_string(), "b".to_string()]);
|
||||
// Moving a card out of the purchased pile drops it from the reveal set.
|
||||
store.set("a", "club").await.unwrap();
|
||||
assert_eq!(
|
||||
store.list_by_pile("purchased").await.unwrap(),
|
||||
vec!["b".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user