Spaces:
Sleeping
Sleeping
| # 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} |