from fastapi import APIRouter, Depends from sqlalchemy import text from .db import get_db from .mirror import enrich as mirror_enrich # The virtual store API — PUBLIC read (it's the customer-facing storefront). Replaces # WowPlatter's payload.php: same scene shape, but from Postgres instead of a full WP bootstrap. router = APIRouter(prefix="/virtual", tags=["virtual-store"]) # Decal/logo assets were copied out of WP's uploads into webstore/assets (served at /store/assets) # so the store is self-contained — no live-WP dependency. Rewrite the stored WP paths on the way out. _WP_PREFIX = "/wp-content/uploads/" _ASSET_BASE = "/store/assets/" def _asset(u): return _ASSET_BASE + u[len(_WP_PREFIX):] if isinstance(u, str) and u.startswith(_WP_PREFIX) else u async def _all(db, table, where="", params=None): rows = await db.execute(text(f"SELECT * FROM {table} {where}"), params or {}) return [dict(r) for r in rows.mappings()] def _attach(recs, meta): for r in recs: m = meta.get(r["release_id"], {}) r["title"], r["artist"], r["thumb"] = m.get("title"), m.get("artist"), m.get("thumb") return recs @router.get("/scene") async def scene(space: int | None = None, db=Depends(get_db)): """Everything the renderer needs to draw ONE room. Pass ?space= to load a linked room (the renderer calls it when you walk through a portal); default = the marked-default space. Records are SLIM — one front cover per crate; full per-crate list lazy-loads via /crate/{id}/records.""" spaces = await _all(db, "virtual_space", "WHERE visible='y'") active = (next((s for s in spaces if s["id"] == space), None) if space else None) or next( (s for s in spaces if s.get("is_default") == "y"), spaces[0] if spaces else None) if not active: return {"space": None, "spaces": spaces, "racks": [], "crates": [], "records": []} sid = active["id"] if active.get("logo_texture"): active["logo_texture"] = _asset(active["logo_texture"]) p = {"s": sid} # one front cover per crate, restricted to crates in THIS room recs = [dict(r) for r in (await db.execute(text(""" SELECT DISTINCT ON (i.crate_id) i.crate_id, i.slot_number, i.release_id, i.sku, i.price, i.condition FROM inventory i JOIN virtual_crate c ON c.id = i.crate_id WHERE i.in_stock AND i.release_id IS NOT NULL AND c.space_id = :s ORDER BY i.crate_id, i.slot_number NULLS LAST"""), p)).mappings()] recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs})) decals = await _all(db, "virtual_decal", "WHERE visible <> 'n' AND object_type='space' AND object_id = :s", p) for d in decals: d["image_url"], d["asset_url"] = _asset(d.get("image_url")), _asset(d.get("asset_url")) # logos on crates/racks — crate_type decals apply to EVERY crate of that type; rack/crate decals # to the named object. Placed on the parent's preferred_face (front/back/left/right/top/bottom). obj_decals = await _all(db, "virtual_decal", """ WHERE visible <> 'n' AND ( object_type='crate_type' OR (object_type='rack' AND object_id IN (SELECT id FROM virtual_rack WHERE space_id=:s AND visible='y')) OR (object_type='crate' AND object_id IN (SELECT id FROM virtual_crate WHERE space_id=:s AND visible='y')) )""", p) for d in obj_decals: d["image_url"], d["asset_url"] = _asset(d.get("image_url")), _asset(d.get("asset_url")) # links_to_id points at the PAIRED portal, not a space — resolve to the room it lives in. portals = [dict(r) for r in (await db.execute(text(""" SELECT p.*, t.space_id AS to_space FROM virtual_portal p LEFT JOIN virtual_portal t ON t.id = p.links_to_id WHERE p.space_id = :s AND p.enabled = 'y'"""), p)).mappings()] return { "space": active, "spaces": spaces, "racks": await _all(db, "virtual_rack", "WHERE visible='y' AND space_id = :s", p), "rack_types": await _all(db, "virtual_rack_type"), "rack_type_levels": await _all(db, "virtual_rack_type_level"), "crates": await _all(db, "virtual_crate", "WHERE visible='y' AND space_id = :s", p), "crate_types": await _all(db, "virtual_crate_type"), "cameras": await _all(db, "virtual_camera", "WHERE space_id = :s", p), "lights": await _all(db, "virtual_light", "WHERE space_id = :s", p), "decals": decals, "object_decals": obj_decals, "portals": portals, "records": recs, } @router.get("/crate/{crate_id}/records") async def crate_records(crate_id: int, db=Depends(get_db)): """Full contents of one crate — loaded when the shopper starts digging it.""" recs = [dict(r) for r in (await db.execute(text(""" SELECT release_id, slot_number, sku, price, condition FROM inventory WHERE crate_id = :c AND in_stock AND release_id IS NOT NULL ORDER BY slot_number NULLS LAST"""), {"c": crate_id})).mappings()] recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs})) return {"ok": True, "crate_id": crate_id, "records": recs} @router.get("/locate") async def locate(release_id: int, db=Depends(get_db)): """Locate-stock: where in the physical store is this record? Returns the crate + position so the UI can fly the camera there and flash the bin.""" row = (await db.execute(text(""" SELECT i.crate_id, i.slot_number, i.sku, c.name, c.label_text, c.pos_x, c.pos_y, c.pos_z FROM inventory i LEFT JOIN virtual_crate c ON c.id = i.crate_id WHERE i.release_id = :r AND i.in_stock AND i.crate_id IS NOT NULL ORDER BY i.slot_number NULLS LAST LIMIT 1"""), {"r": release_id})).mappings().first() if not row: return {"ok": True, "located": False, "release_id": release_id} d = dict(row) return { "ok": True, "located": True, "release_id": release_id, "crate_id": d["crate_id"], "slot_number": d["slot_number"], "sku": d["sku"], "crate": {"name": d["name"], "label_text": d["label_text"], "pos": [float(d["pos_x"] or 0), float(d["pos_y"] or 0), float(d["pos_z"] or 0)]}, }