mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 04:41:23 +00:00
The progression-content substrate: `_get_progression_content` (a lazy, double-checked-locking content cache) is now published into the appstate seam as a CALLABLE. The cache global + lock + the function stay in server.py (startup uses it, and test_progression_api patches `server._progression_content` directly), so ZERO test retargeting — routers just call `appstate.get_progression_content()`. Because the accessor is defined at server.py:1152 but the import-top configure() runs at :346, a second `appstate.configure(get_progression_content=...)` publishes it right after the def (configure is idempotent/additive). First consumer: routers/shop.py (3 routes: buy/equip/list). Bodies verbatim; @app -> @router, meta_db -> appstate.meta_db, _clean_str from reqfields, _get_progression_content() -> appstate.get_progression_content(). This unblocks stats/progression/profile next (all share the accessor). server.py: 7,880 -> 7,845. Verified: pyflakes clean; route table IDENTICAL (143); pytest 2401 passed (test_progression_api's server._progression_content patch still works via the kept cache); packaging guard; eslint 0. Boot smoke: GET /api/shop 200 (drives appstate.get_progression_content), buy 400 on bad body. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.6 KiB
Python
63 lines
2.6 KiB
Python
"""Cosmetics shop (spec 010) — buy/equip avatars & themes with earned currency.
|
|
|
|
Extracted verbatim from ``server.py`` (R3); edits: ``@app`` -> ``@router``,
|
|
``meta_db`` -> ``appstate.meta_db``, ``_get_progression_content()`` ->
|
|
``appstate.get_progression_content()`` (the accessor is injected into the seam;
|
|
its lazy content cache stays in server.py).
|
|
"""
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
|
|
import appstate
|
|
from reqfields import _clean_str
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/api/shop")
|
|
def api_shop():
|
|
content = appstate.get_progression_content()
|
|
owned = appstate.meta_db.get_owned_items()
|
|
equipped = appstate.meta_db.get_equipped()
|
|
items = [
|
|
{**item, "owned": iid in owned, "equipped": equipped.get(item["slot"]) == iid}
|
|
for iid, item in sorted(content["shop"].items())
|
|
]
|
|
return {"items": items, "wallet": appstate.meta_db.get_wallet()}
|
|
|
|
|
|
@router.post("/api/shop/buy")
|
|
def api_shop_buy(data: dict):
|
|
"""Spend Decibels on a cosmetic. Atomic: balance check + spend + ownership
|
|
in one transaction. Decibels are earned by playing only — never purchasable."""
|
|
item_id = _clean_str(data.get("item_id"))
|
|
item = appstate.get_progression_content()["shop"].get(item_id)
|
|
if not item:
|
|
return JSONResponse({"error": f"unknown item: {item_id!r}"}, status_code=400)
|
|
status, wallet = appstate.meta_db.buy_shop_item(item)
|
|
if status == "owned":
|
|
return JSONResponse({"error": "already owned", "wallet": wallet}, status_code=409)
|
|
if status == "insufficient":
|
|
return JSONResponse({"error": "insufficient balance", "wallet": wallet}, status_code=402)
|
|
return {"ok": True, "item_id": item_id, "wallet": wallet}
|
|
|
|
|
|
@router.post("/api/shop/equip")
|
|
def api_shop_equip(data: dict):
|
|
"""Equip an owned cosmetic into its slot. Body: {slot, item_id|null}
|
|
(null unequips, restoring the default look)."""
|
|
import progression as progression_mod
|
|
slot = _clean_str(data.get("slot"))
|
|
if slot not in progression_mod.SHOP_SLOTS:
|
|
return JSONResponse({"error": f"slot must be one of {sorted(progression_mod.SHOP_SLOTS)}"}, status_code=400)
|
|
item_id = data.get("item_id")
|
|
if item_id is not None:
|
|
item_id = _clean_str(item_id)
|
|
item = appstate.get_progression_content()["shop"].get(item_id)
|
|
if not item or item["slot"] != slot:
|
|
return JSONResponse({"error": f"unknown item for slot {slot}: {item_id!r}"}, status_code=400)
|
|
if item_id not in appstate.meta_db.get_owned_items():
|
|
return JSONResponse({"error": "item not owned"}, status_code=403)
|
|
return {"ok": True, "equipped": appstate.meta_db.equip_item(slot, item_id)}
|