From fc607bfe15e3ccbc462d013708af886c2de233c0 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 15:14:46 +0200 Subject: [PATCH] fix(marketguard): read purse and inventories from the v2 Hypixel profile layout Hypixel's v2 profile payload keeps the coin purse under members..currencies and the inventories under members..inventory. The player and finance services still read the legacy member-root fields, so every player reported purse, armor and equipment as unavailable and the finance known total was never computed. Both helpers now prefer the v2 layout and fall back to the legacy keys. Co-Authored-By: Claude Opus 5 --- app/marketguard_api/player_service.py | 44 ++++++++++++++++----------- tests/test_marketguard_api.py | 37 ++++++++++++++++------ 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/app/marketguard_api/player_service.py b/app/marketguard_api/player_service.py index f37b67e..829b324 100644 --- a/app/marketguard_api/player_service.py +++ b/app/marketguard_api/player_service.py @@ -424,6 +424,10 @@ def _bank_balance(profile: dict[str, Any]) -> float | None: def _coin_purse(member: dict[str, Any]) -> float | None: + # Hypixel's v2 profile format keeps the purse under "currencies"; older payloads had it at the member root. + currencies = member.get("currencies") + if isinstance(currencies, dict) and "coin_purse" in currencies: + return _non_negative_number(currencies.get("coin_purse")) return _non_negative_number(member.get("coin_purse")) @@ -580,25 +584,29 @@ def _known_total(bank: float | None, purse: float | None, museum_value: object) def _inventory(member: dict[str, Any], *keys: str) -> list[dict[str, object]] | None: - for key in keys: - inventory = member.get(key) - if not isinstance(inventory, dict): + # Hypixel's v2 profile format nests the inventories under "inventory"; older payloads had them at the member root. + for container in (member.get("inventory"), member): + if not isinstance(container, dict): continue - encoded = inventory.get("data") - if not isinstance(encoded, str) or not encoded.strip(): - continue - parsed = parse_inventory_nbt(encoded) - if parsed is None: - return None - return [ - { - "slot": item.slot, - "id": item.item_id, - "name": item.name, - "count": item.count, - } - for item in parsed - ] + for key in keys: + inventory = container.get(key) + if not isinstance(inventory, dict): + continue + encoded = inventory.get("data") + if not isinstance(encoded, str) or not encoded.strip(): + continue + parsed = parse_inventory_nbt(encoded) + if parsed is None: + return None + return [ + { + "slot": item.slot, + "id": item.item_id, + "name": item.name, + "count": item.count, + } + for item in parsed + ] return None diff --git a/tests/test_marketguard_api.py b/tests/test_marketguard_api.py index debd155..ed54a46 100644 --- a/tests/test_marketguard_api.py +++ b/tests/test_marketguard_api.py @@ -291,16 +291,18 @@ async def _hypixel_handler(request: httpx.Request) -> httpx.Response: "banking": {"balance": 125_000_000}, "members": { player_uuid: { - "coin_purse": 4_250_000.5, - "equippment_contents": { - "data": _encode_inventory_bytes( - [("GAUNTLET_OF_CONTAGION", "Gauntlet of Contagion", 1)] - ) - }, - "inv_armor": { - "data": _encode_inventory_bytes( - [("NECRON_HELMET", "Necron's Helmet", 1)] - ) + "currencies": {"coin_purse": 4_250_000.5}, + "inventory": { + "equipment_contents": { + "data": _encode_inventory_bytes( + [("GAUNTLET_OF_CONTAGION", "Gauntlet of Contagion", 1)] + ) + }, + "inv_armor": { + "data": _encode_inventory_bytes( + [("NECRON_HELMET", "Necron's Helmet", 1)] + ) + }, }, "pets_data": { "pets": [ @@ -3562,3 +3564,18 @@ async def add_page(self, auctions) -> None: f"{live['peak']} pages were in flight at once, " f"max_parallel_pages is {settings.max_parallel_pages}" ) + + +def test_member_purse_and_inventories_accept_v2_and_legacy_layouts() -> None: + from app.marketguard_api.player_service import _coin_purse, _inventory + + armor = {"data": _encode_inventory_bytes([("NECRON_HELMET", "Necron's Helmet", 1)])} + v2_member = {"currencies": {"coin_purse": 12.5}, "inventory": {"inv_armor": armor}} + legacy_member = {"coin_purse": 7.0, "inv_armor": armor} + + assert _coin_purse(v2_member) == 12.5 + assert _coin_purse(legacy_member) == 7.0 + assert _coin_purse({"currencies": {}}) is None + assert _inventory(v2_member, "inv_armor")[0]["id"] == "NECRON_HELMET" + assert _inventory(legacy_member, "inv_armor")[0]["id"] == "NECRON_HELMET" + assert _inventory({"inventory": {}}, "inv_armor") is None