fix(market): returning an item to the club ends its auction; close the panel probe

CLOSES the active-own-auction Actions-panel investigation. Live client plus the RE
corpus plus historical FUT behaviour all agree: an active auction is COMMITTED until
sale or expiry and is not seller-actionable, while an expired unsold item becomes
actionable (relist / return to club). Every observation fits that lifecycle --
active+frozen expires was non-selectable, expired was selectable and relisted fine,
relisting made it active and non-selectable again, and the client never emits a
cancel. Documented with confidence tags, and the dead ends are named so they are not
retried: MAY_BE_REMOVED is a constant 1, and the eight-flag array is the CLUB-CARD
menu with no auction-cancellation flag in it.

Implements the return-to-club transition that closure exposes. A pile move to `club`
now cancels any ACTIVE listing on that item, because the auction that put the card
in the pile has to end with it. Otherwise the pile reads `club` while the row stays
`active`, so the card is filtered out of /club (exclusion keys on active listings)
AND still rendered in the Transfer List: the move appears to do nothing. This is the
same limbo class as the earlier pile-vs-listing bug, found by reasoning about the
transition rather than by another live failure.

Scoped to `active` only: a `reserved` row is mid-sale and a `sold` row is already
gone, so cancelling either would let one card be both sold and returned. Two tests
cover exactly that boundary.

338 tests pass, 0 failed, clippy clean.
This commit is contained in:
funman300
2026-08-17 19:56:06 +00:00
parent 6cc22e5cc5
commit 4e31fb98a2
4 changed files with 206 additions and 8 deletions
+27
View File
@@ -592,6 +592,33 @@ impl MarketStore {
}
self.get_listing(listing_id).await
}
/// Cancel any `active` listing held by a Core owned item, returning how many
/// rows were cancelled (0 when the item has no live auction).
///
/// This is the RETURN-TO-CLUB transition: the client sends a pile move for an
/// expired transfer-list item, and the auction that put it there has to end with
/// it. Without this the pile says `club` while the listing row stays `active`,
/// so the card is still filtered out of `/club` AND still rendered in the
/// Transfer List — the item appears not to move at all.
///
/// Deliberately scoped to `active`: a `reserved` row is mid-sale and a `sold`
/// row is already gone, and cancelling either would let a card be both sold and
/// returned.
pub async fn cancel_active_for_core_item(
&self,
core_item_id: &str,
) -> Result<u64, MarketError> {
Ok(sqlx::query(
"UPDATE listings SET state = 'cancelled' \
WHERE core_item_id = ? AND state = 'active'",
)
.bind(core_item_id)
.execute(&self.pool)
.await
.map_err(db)?
.rows_affected())
}
}
#[cfg(test)]