feat(market): PHASE C — expose every unlisted trade-pile item as tradeState "inactive"
Q2 is LIVE-CONFIRMED (operator saw the inactive row under TRANSFER LIST with Start Price 0 and no Buy Now / Current Bid / timer, active rows still separate under LISTED ITEMS, and the state survived a full FUT exit/re-entry). Promoting from the bounded one-item probe to the real behaviour: the env gate is gone and /tradePile now enumerates the whole trade pile. Mechanism: read the pile (async), resolve each member to a shaped card (sync, because the identity/Core resolvers are not `Send`), then build the response (async). The core->wire lookup is `wire_for_owned_id`, which uses the identity store's NON-allocating `external_for` -- enumerating a pile is a READ and must never mint a wire id for an item the client has not seen. Items with no mapping, no Core record or no resolvable FIFA identity are skipped, never faked. Includes a bug the DIFFERENTIAL caught and unit tests did not: a pile row OUTLIVES its auction, so after a sale the seller's `trade` row is stale, and filtering only on ACTIVE listings re-advertised a SOLD card as an owned unlisted item. Suppression is now by listing state via `blocking_core_items()` -- active (real auction shown instead), reserved (sale in flight) and sold (card gone) -- while `cancelled` is deliberately NOT suppressed, because a cancelled listing means the card came back to the pile. New test covers all three plus the store-level rule. counts semantics deliberately unchanged: `count`/`selling` still track auctions only. 341 tests pass, 0 failed, clippy clean. Deployed: the 6 previously stranded pile items now render, alongside the 1 active listing, with Ronaldo correctly in /club and out of the pile. Body preserved as phase-c-full-pile-exposed.json.
This commit is contained in:
@@ -1052,6 +1052,21 @@ impl Fifa17IdentityResolver {
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
/// The wire item id ALREADY mapped to a Core owned instance, or `None`.
|
||||
///
|
||||
/// NEVER allocates, unlike the resolver's forward path: enumerating the trade
|
||||
/// pile must not mint identities for items the client has never seen, or a
|
||||
/// read would quietly consume wire ids.
|
||||
pub fn wire_for_owned_id(&self, core_id: &str) -> Option<i64> {
|
||||
self.store
|
||||
.external_for(
|
||||
Fifa17WireItemIdPolicy::GAME,
|
||||
Fifa17WireItemIdPolicy::OWNED_ITEM_KIND,
|
||||
core_id,
|
||||
)
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
/// The FIFA `cardsubtypeid` for an owned item's definition (0 if unknown /
|
||||
/// a player), from the catalog — used by club-stats family aggregation.
|
||||
pub fn subtype_of(&self, item: &CoreOwnedItem) -> i64 {
|
||||
@@ -2347,39 +2362,44 @@ impl Server {
|
||||
})
|
||||
}
|
||||
EconomyRoute::MarketQuery => {
|
||||
// BOUNDED Q2 EXPERIMENT (off unless the env names one wire item id):
|
||||
// expose a SINGLE unlisted trade-pile item as a non-active auction
|
||||
// record. Resolved synchronously here because the identity/Core
|
||||
// resolvers are not `Send`, exactly like the market-list path.
|
||||
let unlisted = std::env::var("OPENFUT_FIFA17_UNLISTED_PROBE")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<i64>().ok())
|
||||
.filter(|id| *id > 0)
|
||||
.and_then(|id| {
|
||||
let lookup = CoreItemLookup {
|
||||
core: self.core.as_ref(),
|
||||
};
|
||||
crate::market::resolve_unlisted_candidate(
|
||||
id,
|
||||
self.resolver.as_ref(),
|
||||
self.resolver.as_ref(),
|
||||
&lookup,
|
||||
self.entities.as_ref(),
|
||||
)
|
||||
});
|
||||
// Unlisted trade-pile members are exposed as `tradeState: "inactive"`
|
||||
// rows (LIVE-CONFIRMED; see docs/FIFA17_TRANSFER_MARKET_WIRE.md).
|
||||
//
|
||||
// Three steps, in this order for a reason: the pile read is async, the
|
||||
// identity/Core resolvers are NOT `Send` so they cannot cross an await,
|
||||
// and the response build is async again.
|
||||
let (bridge, market, econ, piles) = (
|
||||
svc.bridge.clone(),
|
||||
svc.market.clone(),
|
||||
svc.econ.clone(),
|
||||
svc.piles.clone(),
|
||||
);
|
||||
let trade_ids = {
|
||||
let p = piles.clone();
|
||||
bridge
|
||||
.block_on(async move { p.list_by_pile("trade").await })
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let unlisted = if trade_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let lookup = CoreItemLookup {
|
||||
core: self.core.as_ref(),
|
||||
};
|
||||
crate::market::resolve_unlisted_pile(
|
||||
&trade_ids,
|
||||
|core_id| self.resolver.wire_for_owned_id(core_id),
|
||||
self.resolver.as_ref(),
|
||||
&lookup,
|
||||
self.entities.as_ref(),
|
||||
)
|
||||
};
|
||||
bridge.block_on(async move {
|
||||
crate::market::handle_market_query(
|
||||
"active",
|
||||
econ.as_ref(),
|
||||
market.as_ref(),
|
||||
piles.as_ref(),
|
||||
unlisted.as_ref(),
|
||||
&unlisted,
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user