Eyob-Sol's picture
Upload 41 files
ac1f51b verified
raw
history blame
4.25 kB
# 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}