import base64 import secrets from fastapi import APIRouter, Depends, Query from pydantic import BaseModel from sqlalchemy import text from . import __version__ from .auth import require_token from .db import get_db from .mirror import enrich as mirror_enrich # The wowplatter/v1 contract — RecordGod's front door (see RECORDGOD_PLAN.md §3). # Routes 1, 2, 5 live here. Routes 3-4 (price/labels) are deliberately NOT defined yet: # PRICEGOD treats 404 as "pending" and degrades gracefully. ponytail: 404 already says it. router = APIRouter(prefix="/wowplatter/v1", tags=["wowplatter/v1"]) @router.get("/ping") async def ping(ident=Depends(require_token)): """Connection test + identity — lights PRICEGOD's green dot and store chip.""" return {"ok": True, "store": ident["store"], "plan": ident["plan"], "version": __version__} @router.get("/inventory/lookup") async def inventory_lookup( release_id: int | None = Query(None), barcode: str | None = Query(None), ident=Depends(require_token), db=Depends(get_db), ): """"Do WE have this?" — the most-used call. release_id first, identifier (barcode) fallback.""" if release_id is None and barcode is None: return {"ok": False, "reason": "need release_id or barcode"} where, params = ( ("release_id = :rid", {"rid": release_id}) if release_id is not None else ("identifier = :bc", {"bc": barcode}) ) params["sid"] = ident["store_id"] row = (await db.execute(text( f"SELECT sku, price, qty, condition, status, product_url, release_id " f"FROM inventory WHERE {where} AND store_id = :sid AND in_stock LIMIT 1" ), params)).mappings().first() if not row: return {"ok": True, "in_stock": False, "release_id": release_id} return { "ok": True, "in_stock": True, **dict(row), "price": float(row["price"]) if row["price"] is not None else None, } class IntakeIn(BaseModel): release_id: int | None = None identifier: str | None = None condition: str | None = None sleeve: str | None = None notes: str | None = None price: float | None = None sku: str | None = None kind: str = "vinyl" def _new_sku() -> str: # ponytail: MRP- + 8 base32 chars ≈ 1e12 space — collisions negligible at shop scale; the # sku PK + ON CONFLICT upsert means a clash just updates that row. Tighten if it ever bites. return "MRP-" + base64.b32encode(secrets.token_bytes(5)).decode().rstrip("=")[:8] @router.post("/inventory/intake") async def inventory_intake(body: IntakeIn, ident=Depends(require_token), db=Depends(get_db)): """New-stock intake. Enriches from the discogs_full mirror by release_id, stages locally (staged=true). Publishing to live is a separate, non-destructive step (PLAN §6).""" title = artist = None weight = None if body.release_id: # ponytail: mirror down → stage un-enriched rather than fail the intake (enrich() swallows) meta = (await mirror_enrich([body.release_id])).get(body.release_id) if meta: title, artist, weight = meta["title"], meta["artist"], meta["weight"] sku = body.sku or _new_sku() await db.execute(text(""" INSERT INTO inventory (sku, store_id, kind, release_id, identifier, title, price, condition, sleeve_cond, weight_g, notes, staged, status) VALUES (:sku, :sid, :kind, :rid, :ident, :title, :price, :cond, :sleeve, :wt, :notes, true, 'staged') ON CONFLICT (sku) DO UPDATE SET release_id = EXCLUDED.release_id, title = EXCLUDED.title, price = EXCLUDED.price, condition = EXCLUDED.condition, sleeve_cond = EXCLUDED.sleeve_cond, notes = EXCLUDED.notes, updated_at = now() """), { "sku": sku, "sid": ident["store_id"], "kind": body.kind, "rid": body.release_id, "ident": body.identifier, "title": title, "price": body.price, "cond": body.condition, "sleeve": body.sleeve, "wt": weight, "notes": body.notes, }) await db.commit() return {"ok": True, "sku": sku, "staged": True, "title": title, "artist": artist, "enriched": title is not None}