# app/sim_api.py from __future__ import annotations from typing import Dict, Any, List, Tuple, Optional from app.catalog import load_catalog, find_item_by_name, find_item_by_sku def _catalog() -> Dict[str, Any]: # Local indirection so we can override in tests if needed return load_catalog() def _pick_item(order_it: Dict[str, Any]) -> Optional[Dict[str, Any]]: it = None if order_it.get("sku"): it = find_item_by_sku(str(order_it["sku"])) if not it and order_it.get("name"): it = find_item_by_name(str(order_it["name"])) return it def _norm_qty(q: Any) -> Optional[int]: try: qi = int(q) return qi if qi >= 1 else None except Exception: return None def check_item_availability(order_it: Dict[str, Any], catalog: Optional[Dict[str, Any]] = None) -> Tuple[bool, Dict[str, Any]]: """ Returns (is_available, info) - info on success: {"price_each": float} - info on failure: {"reason": str, "item": dict, **context} """ it = _pick_item(order_it) if not it: return False, {"reason": "unknown_item", "item": order_it} qty = _norm_qty(order_it.get("qty")) if qty is None: return False, {"reason": "qty_missing_or_invalid", "item": order_it} # Normalize size if provided size = order_it.get("size") size_norm = str(size).lower() if isinstance(size, str) else None price_map = (it.get("price") or {}) stock_map = (it.get("stock") or {}) # One-size items if "one_size" in stock_map: have = int(stock_map.get("one_size", 0)) if have >= qty: unit = float(price_map.get("one_size", 0.0)) return True, {"price_each": unit} return False, {"reason": "insufficient_stock", "have": have, "item": order_it} # Size-required items if not size_norm: # schema enforcement will normally ask for size; we surface a nudge + available choices choices = [k for k in stock_map.keys()] return False, {"reason": "size_missing", "choices": choices, "item": order_it} have = int(stock_map.get(size_norm, 0)) if have >= qty: unit = float(price_map.get(size_norm, 0.0)) return True, {"price_each": unit} # Try alternatives that can satisfy qty alts = [] for s, have_s in stock_map.items(): try: hs = int(have_s) except Exception: continue if hs >= qty: alts.append({ "size": s, "have": hs, "price_each": float(price_map.get(s, 0.0)) }) return False, {"reason": "size_out_of_stock", "requested_size": size_norm, "alternatives": alts, "item": order_it} def place_order(order_items: List[Dict[str, Any]], catalog: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Validates all items via check_item_availability. Returns: - {"ok": True, "total": float, "lines": [ ... ] } - {"ok": False, "reason": str, "item": dict, "alternatives": [...]?} """ total = 0.0 lines: List[Dict[str, Any]] = [] for raw in order_items: it = _pick_item(raw) if not it: return {"ok": False, "reason": "unknown_item", "item": raw} ok, info = check_item_availability(raw, catalog=catalog) if not ok: # Bubble up first blocking failure fail = {"ok": False, "reason": info.get("reason", "unavailable"), "item": info.get("item", raw)} if "alternatives" in info: fail["alternatives"] = info["alternatives"] if "choices" in info: fail["choices"] = info["choices"] return fail qty = _norm_qty(raw.get("qty")) or 0 unit = float(info.get("price_each", 0.0)) line_total = unit * qty total += line_total # Echo back normalized line opts = {k: v for k, v in raw.items() if k not in ("name", "sku", "qty")} lines.append({ "sku": it["sku"], "name": it["name"], "qty": qty, "options": opts, "unit": unit, "line_total": round(line_total, 2), }) return {"ok": True, "total": round(total, 2), "lines": lines}