fifa17-recon: transfer market sell/list flow (stateful, tested)
Complete the market loop (browse + buy + sell). POST auctionhouse (FutISStart)
lists an owned club item -> profile.listings + returns {id:tradeId}. tradePile
builds a validated auction record per listing from the owned item + prices
(freeze-safe, same 0x18013e410 shape). DELETE trade/{id} removes the listing.
fut_store gains list_for_sale/listings/remove_listing (tradeId space 900500000+).
test_market_buy.py extended with sell/delist checks (temp profile, no real-save
mutation): list -> tradePile shows it with prices -> delist empties it. All pass.
Read-only contract suite still 311/311. Functional (FIFA's exact sell params)
pending live test; freeze-safe by construction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
This commit is contained in:
@@ -165,6 +165,28 @@ class Store:
|
||||
out["players"] = players
|
||||
return out
|
||||
|
||||
# ---- transfer-market listings (user's own sale pile) -------------------
|
||||
def list_for_sale(self, item_id, start, buynow):
|
||||
with _LOCK:
|
||||
p = self.load()
|
||||
p.setdefault("listings", [])
|
||||
p["listings"] = [l for l in p["listings"] if l.get("itemId") != item_id]
|
||||
tid = 900500000 + p.get("nextListingSeq", 0)
|
||||
p["nextListingSeq"] = p.get("nextListingSeq", 0) + 1
|
||||
p["listings"].append({"tradeId": tid, "itemId": item_id,
|
||||
"startingBid": start, "buyNowPrice": buynow})
|
||||
self._save()
|
||||
return tid
|
||||
|
||||
def listings(self):
|
||||
return self.load().get("listings", [])
|
||||
|
||||
def remove_listing(self, tid):
|
||||
with _LOCK:
|
||||
p = self.load()
|
||||
p["listings"] = [l for l in p.get("listings", []) if l.get("tradeId") != tid]
|
||||
self._save()
|
||||
|
||||
def new_item_id(self):
|
||||
with _LOCK:
|
||||
p = self.load(); i = p["nextItemId"]; p["nextItemId"] += 1; self._save()
|
||||
|
||||
@@ -62,8 +62,26 @@ def main():
|
||||
ok("insufficient funds -> 461", c2 == 461, str(c2))
|
||||
ok("coins unchanged on failed buy", u.STORE.coins() == 10, str(u.STORE.coins()))
|
||||
|
||||
# ---- SELL / list flow ----
|
||||
owned_id = u.STORE.items()[0]["id"]
|
||||
_, s = u.auctionhouse_route(FakeH("POST", "/ut/game/fifa17/auctionhouse",
|
||||
json.dumps({"itemData": {"id": owned_id}, "startingBid": 1000, "buyNowPrice": 5000}).encode()))
|
||||
ltid = s.get("id")
|
||||
ok("list returns a tradeId", isinstance(ltid, int), repr(s))
|
||||
_, tp = u.tradepile_route(FakeH("GET", "/ut/game/fifa17/tradePile"))
|
||||
ok("tradePile shows the listing", tp.get("total") == 1, repr(tp.get("total")))
|
||||
if tp.get("auctionInfo"):
|
||||
rec = tp["auctionInfo"][0]
|
||||
ok("listing tradeId matches", rec.get("tradeId") == ltid)
|
||||
ok("listing buyNowPrice preserved", rec.get("buyNowPrice") == 5000)
|
||||
ok("listing itemData is object", isinstance(rec.get("itemData"), dict))
|
||||
ok("listing itemData.attributeList is array", isinstance(rec.get("itemData", {}).get("attributeList"), list))
|
||||
u.delete_trade_route(FakeH("DELETE", f"/ut/delete/game/fifa17/trade/{ltid}"))
|
||||
_, tp2 = u.tradepile_route(FakeH("GET", "/ut/game/fifa17/tradePile"))
|
||||
ok("delist empties tradePile", tp2.get("total") == 0, repr(tp2.get("total")))
|
||||
|
||||
os.remove(os.environ["FUT_PROFILE"])
|
||||
print(f"{'PASS' if not fails else 'FAIL'} - market buy flow "
|
||||
print(f"{'PASS' if not fails else 'FAIL'} - market buy+sell flow "
|
||||
f"({0 if fails else 'all'} checks; {len(fails)} failed)")
|
||||
for f in fails:
|
||||
print(" FAIL:", f)
|
||||
|
||||
@@ -190,7 +190,7 @@ ROUTES = [
|
||||
(re.compile(G + r"/watchList"), lambda m, h: watchlist_route(h)),
|
||||
(re.compile(G + r"/auctionhouse"), lambda m, h: auctionhouse_route(h)),
|
||||
(re.compile(G + r"/marketdata"), lambda m, h: (200, {"minPrice": 150, "maxPrice": 15000})),
|
||||
(re.compile(r"/ut/delete/game/[^/]+/trade"), lambda m, h: (200, {})),
|
||||
(re.compile(r"/ut/delete/game/[^/]+/trade"), lambda m, h: delete_trade_route(h)),
|
||||
(re.compile(r"/ut/delete/game/[^/]+/watchList"), lambda m, h: (200, {})),
|
||||
(re.compile(G + r"/club"), lambda m, h: (200, {"itemData": STORE.items()})),
|
||||
]
|
||||
@@ -383,6 +383,16 @@ def auctionhouse_route(h):
|
||||
# POST = FutISStart (list item for sale) -> {"id": new tradeId}
|
||||
# PUT = .../relist (relist all expired) -> ack {}
|
||||
if h.command == "POST":
|
||||
# FutISStart: list an owned club item for sale -> {"id": tradeId}
|
||||
try:
|
||||
b = json.loads(h._body.decode()) if getattr(h, "_body", b"") else {}
|
||||
except Exception:
|
||||
b = {}
|
||||
item_id = (b.get("itemData") or {}).get("id") or b.get("itemId")
|
||||
if item_id:
|
||||
tid = STORE.list_for_sale(item_id, b.get("startingBid", 150), b.get("buyNowPrice", 0))
|
||||
log(" MARKET: listed item %s -> tradeId %d" % (item_id, tid))
|
||||
return 200, {"id": tid}
|
||||
return 200, {"id": STORE.new_item_id()}
|
||||
if h.command == "PUT":
|
||||
return 200, {}
|
||||
@@ -427,8 +437,31 @@ def trade_route(h):
|
||||
|
||||
|
||||
def tradepile_route(h):
|
||||
# The user's OWN sale pile -- empty until they list something (no live sell flow yet).
|
||||
return 200, {"auctionInfo": [], "credits": STORE.coins(), "total": 0}
|
||||
# The user's OWN sale pile: build a validated auction record per active listing
|
||||
# from the owned club item + its list prices. Freeze-safe (same record shape).
|
||||
by_id = {it["id"]: it for it in STORE.items()}
|
||||
seller = STORE.profile().get("personaName", "OpenFUT")
|
||||
recs = []
|
||||
for l in STORE.listings():
|
||||
it = by_id.get(l["itemId"])
|
||||
if not it:
|
||||
continue
|
||||
card = dict(it); card["itemState"] = "listFS"
|
||||
recs.append({
|
||||
"tradeId": l["tradeId"], "itemData": card, "tradeState": "active",
|
||||
"buyNowPrice": l.get("buyNowPrice", 0), "startingBid": l.get("startingBid", 150),
|
||||
"currentBid": 0, "bidState": "none", "expires": 3600,
|
||||
"sellerName": seller, "sellerEstablished": 1, "watched": False, "coinsProcessed": 0,
|
||||
})
|
||||
return 200, {"auctionInfo": recs, "credits": STORE.coins(), "total": len(recs)}
|
||||
|
||||
|
||||
def delete_trade_route(h):
|
||||
# DELETE ut/delete/game/fifa17/trade/{id} -- remove a listing from the sale pile.
|
||||
m = re.search(r"/trade/(\d+)", h.path)
|
||||
if m:
|
||||
STORE.remove_listing(int(m.group(1)))
|
||||
return 200, {}
|
||||
|
||||
|
||||
def watchlist_route(h):
|
||||
|
||||
Reference in New Issue
Block a user