| import os |
| import time |
| import asyncio |
| from typing import List, Optional |
| from pydantic import BaseModel |
| from fastapi import APIRouter, Depends, HTTPException |
| from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer |
| from sqlalchemy.orm import Session |
| from backend.db.session import get_db |
| from backend.db.models import Restaurant, Coupon, DineoutReservation, ExpenseLog, SystemSetting |
| from backend.core.logger import get_logger |
| from backend.api.utils import call_swiggy_mcp_sync |
|
|
| logger = get_logger(__name__) |
|
|
| security = HTTPBearer(auto_error=False) |
|
|
| router = APIRouter() |
|
|
| class RestaurantCreate(BaseModel): |
| name: str |
| cuisine: str |
| rating: float |
| distance: str |
| time: str |
| slaConfidence: int |
| isAIPick: bool |
| isExclusive: bool |
| image: Optional[str] = None |
|
|
| class CouponCreate(BaseModel): |
| code: str |
| pct: int |
| minOrder: int |
| desc: str |
|
|
| class DineoutReserve(BaseModel): |
| hotel: str |
| time: str |
| party: int |
|
|
|
|
| @router.get("/restaurants", response_model=List[dict]) |
| async def list_restaurants( |
| db: Session = Depends(get_db), |
| authorization: Optional[HTTPAuthorizationCredentials] = Depends(security) |
| ): |
| token = authorization.credentials if authorization else os.getenv("SWIGGY_ACCESS_TOKEN") |
| if token and len(token) > 50 and not token.startswith("YOUR_") and "INVALID" not in token: |
| try: |
| loop = asyncio.get_running_loop() |
| addr_res = await loop.run_in_executor( |
| None, |
| call_swiggy_mcp_sync, |
| "food", |
| "get_addresses", |
| {} |
| ) |
| |
| address_id = None |
| if "structuredContent" in addr_res and "addresses" in addr_res["structuredContent"]: |
| addrs = addr_res["structuredContent"]["addresses"] |
| if addrs: |
| address_id = addrs[0]["id"] |
| |
| if address_id: |
| rest_res = await loop.run_in_executor( |
| None, |
| call_swiggy_mcp_sync, |
| "food", |
| "search_restaurants", |
| {"addressId": address_id, "query": "food"} |
| ) |
| |
| if "structuredContent" in rest_res and "restaurants" in rest_res["structuredContent"]: |
| mcp_rests = rest_res["structuredContent"]["restaurants"] |
| result = [] |
| for r in mcp_rests: |
| raw_rating = r.get("avgRating") |
| try: |
| rating = float(raw_rating) if raw_rating and raw_rating != "undefined" else 4.2 |
| except ValueError: |
| rating = 4.2 |
| |
| sla_conf = 90 + int(hash(r.get("id", "")) % 10) |
| |
| result.append({ |
| "id": r.get("id"), |
| "name": r.get("name"), |
| "cuisine": " · ".join([c.strip() for c in r.get("cuisines", [])]) if isinstance(r.get("cuisines"), list) else r.get("cuisine", "Global Cuisine"), |
| "rating": rating, |
| "distance": f"{r.get('distanceKm', 2.5)} km", |
| "time": f"{r.get('deliveryTimeMinutes', 30)} min", |
| "slaConfidence": sla_conf, |
| "isAIPick": rating >= 4.5, |
| "isExclusive": hash(r.get("id", "")) % 3 == 0, |
| "image": r.get("imageUrl") or "https://lh3.googleusercontent.com/aida-public/AB6AXuBO8RUON-0Yl8JERiePhVFj8Z-Nmfi7A-U5kybOvYLPadTK0uNxXuT-6WSHyhmSSwZXdN6Fte6CFkXWaaytQN_GxD8URqNGmiThzbzJomV7WsXP5b5sMfO2GYRMLj8sagiUXcgUTLUwIUFJnGiJUSs-7ScqHOOE8RUkPjgy4cV7DtmYfeHPZKv-H3fBL4IixTqcLluBWbgeMFRFUL-KmmR84fv2SqqNVNdbM0gRUOUSAZJtf_kj549UYqg7Gm_Ch9KT0OcG1BiTR2qH" |
| }) |
| if result: |
| return result |
| except Exception as e: |
| logger.warning(f"[Swiggy MCP REST] Failed to load from real API: {e}. Falling back to PostgreSQL DB.") |
| |
| rows = db.query(Restaurant).all() |
| return [ |
| { |
| "id": r.id, |
| "name": r.name, |
| "cuisine": r.cuisine, |
| "rating": r.rating, |
| "distance": r.distance, |
| "time": r.time, |
| "slaConfidence": r.slaConfidence, |
| "isAIPick": r.isAIPick, |
| "isExclusive": r.isExclusive, |
| "image": r.image |
| } for r in rows |
| ] |
|
|
| @router.get("/restaurants/{restaurant_id}/menu", response_model=List[dict]) |
| async def list_restaurant_menu( |
| restaurant_id: str, |
| db: Session = Depends(get_db), |
| authorization: Optional[HTTPAuthorizationCredentials] = Depends(security) |
| ): |
| token = authorization.credentials if authorization else os.getenv("SWIGGY_ACCESS_TOKEN") |
| if token and len(token) > 50 and not token.startswith("YOUR_") and "INVALID" not in token: |
| try: |
| loop = asyncio.get_running_loop() |
| addr_res = await loop.run_in_executor( |
| None, |
| call_swiggy_mcp_sync, |
| "food", |
| "get_addresses", |
| {} |
| ) |
| address_id = None |
| if "structuredContent" in addr_res and "addresses" in addr_res["structuredContent"]: |
| addrs = addr_res["structuredContent"]["addresses"] |
| if addrs: |
| address_id = addrs[0]["id"] |
| |
| if address_id: |
| menu_res = await loop.run_in_executor( |
| None, |
| call_swiggy_mcp_sync, |
| "food", |
| "get_restaurant_menu", |
| {"addressId": address_id, "restaurantId": restaurant_id} |
| ) |
| |
| if "structuredContent" in menu_res and "categories" in menu_res["structuredContent"]: |
| cats = menu_res["structuredContent"]["categories"] |
| flat_menu = [] |
| seen_names = set() |
| for cat in cats: |
| for item in cat.get("items", []): |
| name = item.get("name") |
| if name in seen_names: |
| continue |
| seen_names.add(name) |
| |
| raw_rating = item.get("rating") |
| try: |
| rating = float(raw_rating) if raw_rating and raw_rating != "undefined" else 4.2 |
| except ValueError: |
| rating = 4.2 |
| |
| item_hash = hash(item.get("id", "")) |
| protein = 10 + (item_hash % 25) |
| calories = 200 + (item_hash % 300) |
| |
| flat_menu.append({ |
| "id": item.get("id"), |
| "name": name, |
| "price": int(item.get("price") or 199), |
| "rating": rating, |
| "desc": item.get("description") or f"Delicious {name} prepared with premium ingredients.", |
| "protein": protein, |
| "cal": calories, |
| "veg": item.get("isVeg", False), |
| "image": item.get("imageUrl") or "https://lh3.googleusercontent.com/aida-public/AB6AXuAy8Ulq_axTRp6t2EagRb5G-YtqpRnvPzPmyNLG-1FBJ0_p-83Hb7anlB2ZhXsi9Yd0x4n4HVmWhRYJ4r1J0aeYhAKyBpAHs5R59gryk1trq626wW1LuUFZ7SkM8OvhMdS78RXzvNqpn-E03C047MfVamHP-NIetglvLA2A5zzJjsUUJ8KlWdV_E4DdUow8sK7YValAPmnwch_EcyAii9s8yhA-yi925HvzzqKBSoWyYDzGpNFU46e2dbF68cDx_CA1jI2gcAKBGs_E" |
| }) |
| if flat_menu: |
| return flat_menu |
| except Exception as e: |
| logger.error(f"[Swiggy MCP REST] Failed to load menu for {restaurant_id}: {e}") |
| |
| local_menus = { |
| "rest_behrouz": [ |
| { "id": "dum_gosht", "name": "Dum Gosht Biryani", "price": 349, "rating": 4.6, "desc": "Fragrant long-grain basmati rice layered with juicy mutton in royal spices.", "protein": 36, "cal": 540, "veg": False, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuAy8Ulq_axTRp6t2EagRb5G-YtqpRnvPzPmyNLG-1FBJ0_p-83Hb7anlB2ZhXsi9Yd0x4n4HVmWhRYJ4r1J0aeYhAKyBpAHs5R59gryk1trq626wW1LuUFZ7SkM8OvhMdS78RXzvNqpn-E03C047MfVamHP-NIetglvLA2A5zzJjsUUJ8KlWdV_E4DdUow8sK7YValAPmnwch_EcyAii9s8yhA-yi925HvzzqKBSoWyYDzGpNFU46e2dbF68cDx_CA1jI2gcAKBGs_E" }, |
| { "id": "lazeez_chicken", "name": "Lazeez Bhuna Murgh Biryani", "price": 299, "rating": 4.5, "desc": "Tender boneless chicken in bhuna spices layered with basmati rice.", "protein": 32, "cal": 480, "veg": False, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuBVH7_iiDjEwAqM-iOH8jm3r4ljZMINGVU_Xp5Q-c5wjp04ir3wyacHOLYmjmdPdsAEKmN7NFvNQ8ccPIwOAUEqVu7ESWWZFV7ECSWX7JzlbDWyCtYJ_7mti2MWNy3Yuj77gJG8cjX2qVom1OGcFA8kzAFxQ4u3CBk-mzNORIV01WqDHbcX9ae4xKUwXCM69aXnh0vKIHvWcTm7xzkbIx4a_pAK1gBNf1lGPPzRLuDKikphdzej965g0gpkdAKQ1V-5hDx9OoV1vQMF" }, |
| { "id": "mint_raita", "name": "Mint Raita", "price": 49, "rating": 4.2, "desc": "Refreshing raita flavored with fresh mint leaves.", "protein": 2, "cal": 60, "veg": True, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuA9dB7F5xSnF4KMn9vZmYR-rdDJJynymGxYucwoE-YBitPw0VKGSu-DN14kA90BSzp-2uy6VqlvfPFGUv1w1bAkAncDACJEjmjyIs5U_edIxKkwyJXxKBdiWMNunXofnk0gpGuMhOYRmiAlpBLt1eDqi27iQu4sKk2m2BOZdHLrGxGFXuHSxNxRfZrvdjjDlDh9Qzm9Bq8gJA1kCDLJqJ4Wt4tvK3bGLCdxh0ENy_AR1ED6oHIrCU53WfftTybXUz_QCYlouZZvj1fU" } |
| ], |
| "rest_carbon_grill": [ |
| { "id": "truffle_burger", "name": "Truffle Cheese Burger", "price": 280, "rating": 4.5, "desc": "Gourmet double-patty burger with Swiss cheese and black truffle aioli.", "protein": 34, "cal": 620, "veg": False, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuD9C62CkwFO1Ta65rOPGt_zkQb3NWBfpIVfhSCWsS173P7Hw1t8O2CFnA1Swhsh03BFAJeCU4v8zMcs2FtgfS9UKrkQ-pgIxmQV0atKwEY1VvIrOO2nqjJirHB5LtlEy7v2E23zmpz5QUROCmGsEwpUTOxc6-W7bqEnwZTpjlEj84W0_wRNkm3oiChRsbQBbdUsj6iQ4IQ8MjgCXDjvXHjIGyb2EehurUmG2rcFE5E_2NQqMXhnC7sZPl5JUl0b-89s8s1A5HghkpjV" }, |
| { "id": "peri_fries", "name": "Spicy Peri Peri Fries", "price": 149, "rating": 4.3, "desc": "Crispy golden skin-on fries tossed in house peri-peri spice dust.", "protein": 5, "cal": 320, "veg": True, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuD9C62CkwFO1Ta65rOPGt_zkQb3NWBfpIVfhSCWsS173P7Hw1t8O2CFnA1Swhsh03BFAJeCU4v8zMcs2FtgfS9UKrkQ-pgIxmQV0atKwEY1VvIrOO2nqjJirHB5LtlEy7v2E23zmpz5QUROCmGsEwpUTOxc6-W7bqEnwZTpjlEj84W0_wRNkm3oiChRsbQBbdUsj6iQ4IQ8MjgCXDjvXHjIGyb2EehurUmG2rcFE5E_2NQqMXhnC7sZPl5JUl0b-89s8s1A5HghkpjV" } |
| ], |
| "rest_yoko_ono": [ |
| { "id": "salmon_nigiri", "name": "Salmon Nigiri (2pcs)", "price": 320, "rating": 4.7, "desc": "Slices of premium fresh Atlantic salmon laid over seasoned sushi rice.", "protein": 14, "cal": 180, "veg": False, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuCBY63vuIkeBp6l5cHYDUYAUxyfZjekeIUDrgoaWXdYWfRsIItON9yVcNgasVY5EVJ_z9UCEYE7ifS6es_em8GXuQSZjL4elMAOcYKY-mFqvK7XoIYiCdoO9fXcs76s27BFjIlZ-jibt94sXMKAMiW-HDhL8Fx6YgFDMjXCKJuqgQvL6f2QokApfLDSvnpgf5uRCpVCyjlevWvENzKb2pD1gJvWBrOj_kU8HsHYg8siO1GP2yGFdEgOS79jFlelYdFjbEs_cIizY-X6" }, |
| { "id": "tuna_maki", "name": "Tuna Maki Roll (6pcs)", "price": 280, "rating": 4.5, "desc": "Yellowfin tuna wrapped in nori sheet with sushi rice.", "protein": 18, "cal": 220, "veg": False, "image": "https://lh3.googleusercontent.com/aida-public/AB6AXuCBY63vuIkeBp6l5cHYDUYAUxyfZjekeIUDrgoaWXdYWfRsIItON9yVcNgasVY5EVJ_z9UCEYE7ifS6es_em8GXuQSZjL4elMAOcYKY-mFqvK7XoIYiCdoO9fXcs76s27BFjIlZ-jibt94sXMKAMiW-HDhL8Fx6YgFDMjXCKJuqgQvL6f2QokApfLDSvnpgf5uRCpVCyjlevWvENzKb2pD1gJvWBrOj_kU8HsHYg8siO1GP2yGFdEgOS79jFlelYdFjbEs_cIizY-X6" } |
| ] |
| } |
| return local_menus.get(restaurant_id, []) |
|
|
| @router.post("/restaurants") |
| async def create_restaurant(req: RestaurantCreate, db: Session = Depends(get_db)): |
| new_id = f"rest_{int(time.time() * 1000)}" |
| new_rest = Restaurant( |
| id=new_id, |
| name=req.name, |
| cuisine=req.cuisine, |
| rating=req.rating, |
| distance=req.distance, |
| time=req.time, |
| slaConfidence=req.slaConfidence, |
| isAIPick=req.isAIPick, |
| isExclusive=req.isExclusive, |
| image=req.image or "https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=300&auto=format&fit=crop&q=60" |
| ) |
| db.add(new_rest) |
| db.commit() |
| db.refresh(new_rest) |
| return { |
| "status": "success", |
| "restaurant": { |
| "id": new_rest.id, |
| "name": new_rest.name, |
| "cuisine": new_rest.cuisine, |
| "rating": new_rest.rating, |
| "distance": new_rest.distance, |
| "time": new_rest.time, |
| "slaConfidence": new_rest.slaConfidence, |
| "isAIPick": new_rest.isAIPick, |
| "isExclusive": new_rest.isExclusive, |
| "image": new_rest.image |
| } |
| } |
|
|
| @router.get("/coupons", response_model=List[dict]) |
| async def list_coupons(db: Session = Depends(get_db)): |
| rows = db.query(Coupon).all() |
| return [ |
| { |
| "code": c.code, |
| "pct": c.discount_percentage, |
| "minOrder": int(c.min_cart_value), |
| "desc": f"{c.discount_percentage}% off above ₹{c.min_cart_value}" |
| } for c in rows |
| ] |
|
|
| @router.post("/coupons") |
| async def create_coupon(req: CouponCreate, db: Session = Depends(get_db)): |
| new_cop = Coupon( |
| code=req.code.upper(), |
| discount_percentage=req.pct, |
| min_cart_value=req.minOrder, |
| active=True |
| ) |
| db.add(new_cop) |
| db.commit() |
| db.refresh(new_cop) |
| return { |
| "status": "success", |
| "coupon": { |
| "code": new_cop.code, |
| "pct": new_cop.discount_percentage, |
| "minOrder": int(new_cop.min_cart_value), |
| "desc": f"{new_cop.discount_percentage}% off above ₹{new_cop.min_cart_value}" |
| } |
| } |
|
|
| @router.get("/dineout/reservations", response_model=List[dict]) |
| async def list_dineout_reservations(db: Session = Depends(get_db)): |
| rows = db.query(DineoutReservation).all() |
| return [ |
| { |
| "id": f"res_{r.id}", |
| "hotel": r.restaurant_id, |
| "time": r.time_slot, |
| "party": r.guests, |
| "status": "CONFIRMED" |
| } for r in rows |
| ] |
|
|
| @router.post("/dineout/reserve") |
| async def reserve_dineout(req: DineoutReserve, db: Session = Depends(get_db)): |
| new_res = DineoutReservation( |
| customer_name="HyperFlow Customer", |
| restaurant_id=req.hotel, |
| time_slot=req.time, |
| guests=req.party |
| ) |
| db.add(new_res) |
| db.commit() |
| db.refresh(new_res) |
| return { |
| "status": "success", |
| "reservation": { |
| "id": f"res_{new_res.id}", |
| "hotel": new_res.restaurant_id, |
| "time": new_res.time_slot, |
| "party": new_res.guests, |
| "status": "CONFIRMED" |
| } |
| } |
|
|
| @router.get("/user/expenses", response_model=List[dict]) |
| async def list_user_expenses(db: Session = Depends(get_db)): |
| rows = db.query(ExpenseLog).all() |
| return [ |
| { |
| "id": e.id, |
| "date": e.timestamp.strftime("%b %d") if e.timestamp else "Today", |
| "amount": int(e.amount), |
| "category": e.category, |
| "desc": e.description |
| } for e in rows |
| ] |
|
|
| @router.get("/settings/festival") |
| async def get_festival_settings(db: Session = Depends(get_db)): |
| setting = db.query(SystemSetting).filter(SystemSetting.key == "festival_theme").first() |
| theme = setting.value if setting else "nominal" |
| return {"festival_theme": theme} |
|
|
| @router.post("/settings/festival") |
| async def update_festival_settings(theme_name: str, db: Session = Depends(get_db)): |
| if theme_name not in ["nominal", "diwali", "holi"]: |
| raise HTTPException(status_code=400, detail="Invalid festival theme.") |
| setting = db.query(SystemSetting).filter(SystemSetting.key == "festival_theme").first() |
| if not setting: |
| setting = SystemSetting(key="festival_theme", value=theme_name) |
| db.add(setting) |
| else: |
| setting.value = theme_name |
| db.commit() |
| return {"status": "success", "festival_theme": theme_name} |
|
|