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:
funman300
2026-08-17 20:50:51 +00:00
parent b6398c44e6
commit afadb13de4
4 changed files with 643 additions and 82 deletions
+25
View File
@@ -29,6 +29,7 @@
//! `CHECK` constraint even though it is a transient intermediate — omitting it
//! would make [`MarketStore::reserve_listing`] fail the constraint.
use std::collections::HashSet;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@@ -619,6 +620,30 @@ impl MarketStore {
.map_err(db)?
.rows_affected())
}
/// Core owned-instance ids whose listing state FORBIDS advertising them as an
/// unlisted pile member: `active` (a real auction is shown instead), `reserved`
/// (a sale is in flight) and `sold` (the card is gone).
///
/// `cancelled` is deliberately absent: a cancelled listing means the card came
/// back to the pile and IS an unlisted member again.
///
/// Needed because a pile row outlives its auction — after a sale the seller's
/// `trade` row is stale, and filtering on the active set alone would re-advertise
/// a sold card as an owned unlisted item.
pub async fn blocking_core_items(&self) -> Result<HashSet<String>, MarketError> {
let rows = sqlx::query(
"SELECT DISTINCT core_item_id FROM listings \
WHERE core_item_id IS NOT NULL AND state IN ('active','reserved','sold')",
)
.fetch_all(&self.pool)
.await
.map_err(db)?;
Ok(rows
.iter()
.filter_map(|r| r.get::<Option<String>, _>("core_item_id"))
.collect())
}
}
#[cfg(test)]