fix(market): make the transfer market work end-to-end (live-verified)
Four defects found by driving a real FIFA 17 client. Each was independently
sufficient to break listing, so all four had to go:
1. Every owned card was shaped `untradeable: true` (adapter item.rs), so the
client greyed out "Place/List on Transfer Market" for the whole club. Owned
and pack-pulled cards are TRADEABLE in FIFA 17; the oracle forces this off
for owned copies too (item_def keeps `true`; instances do not).
2. `POST /auctionhouse` required `itemData.resourceId`, which the client's
FutISStart body never sends (the oracle lists by wire id ALONE). Missing it,
the handler fail-closed and returned 200 while persisting NOTHING. It now
resolves server-side: wire id -> Core owned instance -> its card_id (minted on
a synthetic buy) + FIFA resourceId (the auction record). This also enforces
that a listing can only name a card the club actually owns.
3. An auction record's `itemData` was a 4-field STUB, so the Transfer List had a
row the client could not draw -> "1 item listed" but no visible sale. A
listing now persists a full shaped-card SNAPSHOT (new `listings.item_json`,
additive migration) built by the same `shape_item` shaper `/club` and the
squad projection use, so the auction card renders identically to the club
card. The seller's own pile stamps `itemState: listFS`; market search keeps
`forSale` (the oracle distinguishes these).
4. `/tradePile/counts` shared a handler with `/tradePile`. They are DIFFERENT
deserializers: `/counts` is FutGetAuctionCount, five scalar ints
(count/maxAuctionsAllowed/offered/selling/sold) that it reads and skips
everything else. Served the `auctionInfo` body it left every count at 0, so
the Transfer List screen showed no active sale while the hub tile showed one.
New Route::MarketCounts, classified BEFORE the base tradePile matcher (which
also accepts the /counts path).
Also: a listed card no longer appears in the club. `/club` and the hub's
`clubPlayers` now exclude the transfer pile. Pile membership is host-owned state
Core cannot filter on, so when anything is hidden `/club` reuses the existing
local-filter path (the one `rare=SP` already needed) and paginates the
club-visible set -- letting Core paginate would return short pages. With nothing
hidden the fast Core-paginated path is untouched, and only an EXPLICIT non-club
pile hides a card, so no-pile-row items still default to the club.
Fixed 5 pre-existing test fixtures across 4 targets that listed FABRICATED wire
ids -- only "valid" because the old handler skipped the ownership check.
Tests: 14 targets green + clippy clean, incl. new coverage for the 5-int tally
(asserting it must NOT carry auctionInfo), the full-card snapshot + listFS, and
club pile-exclusion with full-width pagination. The differential test against the
live Python oracle passes.
Verified live on prod: listed=true with a 21-field snapshot; counts
{count:1,selling:1,maxAuctionsAllowed:100}; tradePile renders the 94-rated card;
clubPlayers 1966 -> 1961 (exactly the 5 trade-pile items); listed wire absent
from the club page. Operator confirmed the card is visible in the Transfer List.
This commit is contained in:
@@ -257,7 +257,6 @@ struct FailHarness {
|
||||
bridge: Arc<AsyncBridge>,
|
||||
core: Arc<dyn CoreAccess>,
|
||||
entities: Arc<Fifa17Entities>,
|
||||
sample_resource: i64,
|
||||
}
|
||||
|
||||
fn catalog_from_core(core: &dyn CoreAccess) -> Fifa17CardCatalog {
|
||||
@@ -341,7 +340,6 @@ fn build_fail_harness(base: &str, dir: &std::path::Path) -> FailHarness {
|
||||
bridge,
|
||||
core,
|
||||
entities,
|
||||
sample_resource: 20000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,16 +674,15 @@ fn case_move_pile_failure(h: &FailHarness) -> String {
|
||||
|
||||
/// MARKET RESERVE failure → no debit, no grant, listing stays legal (active).
|
||||
fn case_market_reserve_failure(h: &FailHarness) -> String {
|
||||
// List a genuinely-owned card: the server resolves card_id + resourceId from
|
||||
// Core inventory via the wire id (you can only list what you own).
|
||||
let (item_id, _core) = h.mint_one();
|
||||
set_balance(&h.client, 50_000);
|
||||
let item_id = 700_001i64;
|
||||
let list = h.dispatch(
|
||||
"POST",
|
||||
"/ut/game/fifa17/auctionhouse",
|
||||
format!(
|
||||
r#"{{"itemData":{{"id":{item_id},"resourceId":{}}},"buyNowPrice":1000,"startingBid":500}}"#,
|
||||
h.sample_resource
|
||||
)
|
||||
.as_bytes(),
|
||||
format!(r#"{{"itemData":{{"id":{item_id}}},"buyNowPrice":1000,"startingBid":500}}"#)
|
||||
.as_bytes(),
|
||||
);
|
||||
let trade_id = bj(&list)["id"].as_i64().expect("trade id");
|
||||
let before = h.client.balance().unwrap();
|
||||
@@ -712,16 +709,13 @@ fn case_market_reserve_failure(h: &FailHarness) -> String {
|
||||
/// MARKET Core purchase_item failure AFTER reserve → reservation rolls back to
|
||||
/// active, no debit, no mint.
|
||||
fn case_market_purchase_failure(h: &FailHarness) -> String {
|
||||
let (item_id, _core) = h.mint_one();
|
||||
set_balance(&h.client, 50_000);
|
||||
let item_id = 700_002i64;
|
||||
let list = h.dispatch(
|
||||
"POST",
|
||||
"/ut/game/fifa17/auctionhouse",
|
||||
format!(
|
||||
r#"{{"itemData":{{"id":{item_id},"resourceId":{}}},"buyNowPrice":1000,"startingBid":500}}"#,
|
||||
h.sample_resource
|
||||
)
|
||||
.as_bytes(),
|
||||
format!(r#"{{"itemData":{{"id":{item_id}}},"buyNowPrice":1000,"startingBid":500}}"#)
|
||||
.as_bytes(),
|
||||
);
|
||||
let trade_id = bj(&list)["id"].as_i64().expect("trade id");
|
||||
let before = h.client.balance().unwrap();
|
||||
@@ -754,16 +748,13 @@ fn case_market_purchase_failure(h: &FailHarness) -> String {
|
||||
/// active), so no further `active -> reserved` CAS can succeed → not buyable,
|
||||
/// with exactly one debit + one mint. Returns ("SAFE"|"E3", detail).
|
||||
fn case_market_complete_sale_failure(h: &FailHarness) -> (String, String) {
|
||||
let (item_id, _core) = h.mint_one();
|
||||
set_balance(&h.client, 50_000);
|
||||
let item_id = 700_003i64;
|
||||
let list = h.dispatch(
|
||||
"POST",
|
||||
"/ut/game/fifa17/auctionhouse",
|
||||
format!(
|
||||
r#"{{"itemData":{{"id":{item_id},"resourceId":{}}},"buyNowPrice":1000,"startingBid":500}}"#,
|
||||
h.sample_resource
|
||||
)
|
||||
.as_bytes(),
|
||||
format!(r#"{{"itemData":{{"id":{item_id}}},"buyNowPrice":1000,"startingBid":500}}"#)
|
||||
.as_bytes(),
|
||||
);
|
||||
let trade_id = bj(&list)["id"].as_i64().expect("trade id");
|
||||
let mint_id = format!("market-buy:{trade_id}");
|
||||
|
||||
Reference in New Issue
Block a user