from fastapi import FastAPI,Depends,Form, File, UploadFile,Request from database import engine, Base,get_db import crud from pydantic import BaseModel from utils import haversine, calculate_eta, get_current_admin,verify_password,create_token,hash_password from sqlalchemy.orm import Session from schemas import OrderCreate,AddressCreate,AssignDelivery, UpdateDeliveryStatus, DeliveryPayment, UserCreate,SendOTP,VerifyOTP,AdminLogin, CreateZonalAdmin,VendorCreate, DeliveryBoyCreate, DeliveryBoyLogin, ReviewCreate ,SupportTicketCreate from fastapi.middleware.cors import CORSMiddleware from models import OrderItem, Order, Delivery, VendorMenu, MenuItem, Vendor, User,Admin,UserAddress import cloudinary_config from datetime import datetime, timedelta import notifications.firebase_config from notifications.notification_service import send_push_notification from notifications.sendnotification import ( router as notification_router, send_vendor_delivered_notification, send_vendor_cancelled_notification, send_deliveryboy_order_notification, send_deliveryboy_batch_notification, send_user_out_for_delivery_notification, send_user_delivered_notification, send_user_cancelled_notification, ) from notifications.sendnotification import router as notification_router # ✅ FIRST create app app = FastAPI() app.include_router(notification_router) app.add_middleware( CORSMiddleware, allow_origins=["*"], # 🔥 for development allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ✅ THEN use app @app.on_event("startup") def create_tables(): Base.metadata.create_all(bind=engine) # @app.on_event("startup") # def reset_database(): # # 🔥 DROP ALL TABLES # Base.metadata.drop_all(bind=engine) # # 🔥 CREATE ALL TABLES AGAIN # Base.metadata.create_all(bind=engine) # Test route @app.get("/") def read_root(): return {"message": "Backend is running 🚀"} # ===== INPUT SCHEMA ===== class LocationRequest(BaseModel): lat: float lng: float # ===== API ===== @app.post("/check-location") def check_location(data: LocationRequest, db: Session = Depends(get_db)): # 🔥 ONLY ACTIVE + OPEN KITCHENS vendors = db.query(Vendor).filter( Vendor.is_active == True, Vendor.is_open == True # ✅ IMPORTANT ).all() available = [] for v in vendors: if v.lat is None or v.lng is None: continue distance = haversine(data.lat, data.lng, v.lat, v.lng) if distance <= v.radius: available.append({ "vendor_id": v.id, "distance": distance }) if not available: return { "serviceable": False, "message": "No kitchens are open in your area" } available.sort(key=lambda x: x["distance"]) nearest = available[0] return { "serviceable": True, "vendor_id": nearest["vendor_id"], "distance": round(nearest["distance"], 2), "eta": calculate_eta(nearest["distance"]) } # # test route # # menu # @app.get("/menu/{vendor_id}") # def get_menu(vendor_id: int, db: Session = Depends(get_db)): # menu = crud.get_vendor_menu(db, vendor_id) # return { # "vendor_id": vendor_id, # "menu": menu # } # ## testing st # from database import SessionLocal # from models import Vendor, MenuItem, VendorMenu # @app.get("/seed") # def seed_data(): # db = SessionLocal() # # Create vendor # vendor = Vendor(name="Test Kitchen", location="Newtown") # db.add(vendor) # db.commit() # db.refresh(vendor) # # Create menu items # rice = MenuItem(name="Rice", category="Rice", vendor_price=10, selling_price=20) # chicken = MenuItem(name="Chicken", category="Non-Veg", vendor_price=50, selling_price=70) # db.add_all([rice, chicken]) # db.commit() # # Link vendor menu # vm1 = VendorMenu(vendor_id=vendor.id, menu_item_id=rice.id, is_available=True) # vm2 = VendorMenu(vendor_id=vendor.id, menu_item_id=chicken.id, is_available=True) # db.add_all([vm1, vm2]) # db.commit() # return {"message": "Test data inserted"} # ## testing ed @app.post("/order") def create_order_api( order: OrderCreate, request: Request, db: Session = Depends(get_db) ): backend_ip = request.client.host result = crud.create_order( db, order, backend_ip ) return result # @app.post("/assign-delivery") # def assign_delivery_api(data: AssignDelivery, db: Session = Depends(get_db)): # return crud.assign_delivery(db, data) # @app.put("/delivery/status") # def update_status_api(data: UpdateDeliveryStatus, db: Session = Depends(get_db)): # return crud.update_delivery_status(db, data) @app.post("/delivery/complete-order") def complete_order( data: DeliveryPayment, db: Session = Depends(get_db), user=Depends(get_current_admin) ): return crud.add_payment(db, data, user) @app.post("/menu-by-location") def menu_by_location(data: LocationRequest, db: Session = Depends(get_db)): vendors = db.query(Vendor).filter( Vendor.is_active == True, Vendor.is_open == True ).all() available_vendors = [] # ✅ STEP 1: FILTER VENDORS IN RADIUS for v in vendors: if v.lat is None or v.lng is None: continue distance = haversine(data.lat, data.lng, v.lat, v.lng) if distance <= v.radius: available_vendors.append(v) if not available_vendors: return {"error": "No kitchens available right now"} # ✅ STEP 2: GET ALL ITEMS FROM ALL VENDORS all_items = [] for v in available_vendors: results = ( db.query(VendorMenu, MenuItem) .join(MenuItem, VendorMenu.menu_item_id == MenuItem.id) .filter( VendorMenu.vendor_id == v.id, VendorMenu.is_available == True, MenuItem.is_deleted == False ) .all() ) for vm, item in results: all_items.append({ "id": item.id, "name": item.name, "category": item.category, "description": item.description, "price": item.selling_price, "vendor_id": v.id, "vendor_name": v.name, # 🔥 IMPORTANT FOR UI "image": item.image_url # ✅ THIS LINE IS MISSING }) return { "menu": all_items } # User login @app.post("/check-user") def check_user(data: SendOTP, db: Session = Depends(get_db)): user = db.query(User).filter(User.phone == data.phone).first() return { "exists": True if user else False } @app.post("/user/update-profile") def update_profile(data: dict, db: Session = Depends(get_db)): user = db.query(User).filter(User.id == data["user_id"]).first() if not user: return {"error": "User not found"} user.name = data.get("name", user.name) user.email = data.get("email", user.email) user.dob = data.get("dob", user.dob) # ✅ ADD THIS user.gender = data.get("gender", user.gender) # ✅ ADD THIS db.commit() return {"message": "Profile updated"} @app.post("/send-otp") def send_otp_api(data: SendOTP): return crud.send_otp(data) @app.post("/verify-otp") def verify_otp_api( data: VerifyOTP, request: Request, db: Session = Depends(get_db) ): backend_ip = request.client.host frontend_ip = data.frontend_ip result = crud.verify_otp( db, data, backend_ip ) return result ## Admin @app.post("/admin/login") def admin_login(data: AdminLogin, db: Session = Depends(get_db)): return crud.login_admin(db, data) @app.post("/admin/create-zonal-admin") def create_zonal_admin_api( data: CreateZonalAdmin, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) # ✅ THIS LINE IS KEY ): return crud.create_zonal_admin(db, data, admin) @app.get("/admin/vendors") def get_vendors( db: Session = Depends(get_db), admin = Depends(get_current_admin) ): return crud.get_vendors_by_admin(db, admin) ## test st @app.get("/create-admin") def create_admin(db: Session = Depends(get_db)): admin = Admin( name="City Admin", email="admin@test.com", password=hash_password("1234"), role="CITY_ADMIN", city="Kolkata" ) db.add(admin) db.commit() return {"message": "Admin created"} ## test ed @app.post("/vendors") def create_vendor_api( data: VendorCreate, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): return crud.create_vendor(db, data, admin) @app.get("/admin/zonal-admins") def get_zonal_admins_api( db: Session = Depends(get_db), admin = Depends(get_current_admin) ): return crud.get_zonal_admins(db, admin) @app.get("/vendor/{vendor_id}/financial-summary") def vendor_financial_summary( vendor_id: int, filter: str = "today", start_date: str = None, end_date: str = None, db: Session = Depends(get_db), user=Depends(get_current_admin) ): # ✅ SECURITY if user.get("role") != "VENDOR": return {"error": "Unauthorized"} if user.get("vendor_id") != vendor_id: return {"error": "Access denied"} # ========================= # DATE FILTERS # ========================= today = datetime.utcnow() date_from = None date_to = today if filter == "today": date_from = today.replace( hour=0, minute=0, second=0, microsecond=0 ) elif filter == "week": date_from = today - timedelta(days=today.weekday()) date_from = date_from.replace( hour=0, minute=0, second=0, microsecond=0 ) elif filter == "month": date_from = today.replace( day=1, hour=0, minute=0, second=0, microsecond=0 ) elif filter == "year": date_from = today.replace( month=1, day=1, hour=0, minute=0, second=0, microsecond=0 ) elif filter == "custom": if not start_date or not end_date: return { "error": "start_date and end_date required" } date_from = datetime.strptime( start_date, "%Y-%m-%d" ) date_to = datetime.strptime( end_date, "%Y-%m-%d" ) date_to = date_to.replace( hour=23, minute=59, second=59 ) # ========================= # QUERY # ========================= results = ( db.query(OrderItem, Order) .join(Order, OrderItem.order_id == Order.id) .filter( # ✅ THIS VENDOR ONLY OrderItem.vendor_id == vendor_id, # ✅ ONLY SUCCESSFUL ORDERS Order.status == "DELIVERED", Order.payment_status == "PAID", # ❌ EXCLUDE CANCELLED Order.cancelled_at == None, # ✅ DATE FILTER Order.created_at >= date_from, Order.created_at <= date_to ) .all() ) # ========================= # CALCULATIONS # ========================= total_amount = 0 total_orders = set() items_sold = 0 orders_data = {} for item, order in results: # ✅ VENDOR TOTAL ONLY vendor_total = ( item.vendor_price * item.quantity ) total_amount += vendor_total items_sold += item.quantity total_orders.add(order.id) # ========================= # ORDER GROUPING # ========================= if order.id not in orders_data: orders_data[order.id] = { "order_id": order.id, "created_at": order.created_at, "amount": 0, "items": [] } orders_data[order.id]["amount"] += vendor_total orders_data[order.id]["items"].append({ "name": item.name, "qty": item.quantity, "vendor_price": item.vendor_price }) # ========================= # RESPONSE # ========================= return { "total_amount": round(total_amount, 2), "total_orders": len(total_orders), "items_sold": items_sold, "orders": list(orders_data.values()) } @app.put("/vendor/menu/{menu_id}") def toggle_menu(menu_id: int, db: Session = Depends(get_db)): vm = db.query(VendorMenu).filter(VendorMenu.id == menu_id).first() vm.is_available = not vm.is_available db.commit() return {"message": "Updated"} @app.get("/vendor/{vendor_id}/menu") def get_vendor_menu_admin(vendor_id: int, db: Session = Depends(get_db)): results = ( db.query(VendorMenu, MenuItem) .join(MenuItem, VendorMenu.menu_item_id == MenuItem.id) .filter( VendorMenu.vendor_id == vendor_id, MenuItem.is_deleted == False ) .all() ) data = [] for vm, item in results: data.append({ "id": item.id, "name": item.name, "category": item.category, "description": item.description or "", "vendor_price": item.vendor_price, "selling_price": item.selling_price, "is_available": vm.is_available, "image_url": item.image_url }) return data @app.put("/vendor/menu-item/{item_id}") async def update_menu_item_vendor( item_id: int, name: str = Form(...), category: str = Form(...), description: str = Form(None), image: UploadFile = File(None), db: Session = Depends(get_db) ): item = db.query(MenuItem).filter(MenuItem.id == item_id).first() if not item: return {"error": "Item not found"} # 🔒 ONLY THESE FIELDS ALLOWED item.name = name item.category = category item.description = description if image: try: upload = cloudinary.uploader.upload(image.file) item.image_url = upload["secure_url"] except Exception: return {"error": "Image upload failed"} db.commit() return {"message": "Updated"} @app.get("/vendor/{vendor_id}") def get_vendor(vendor_id: int, db: Session = Depends(get_db)): v = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not v: return {"error": "Vendor not found"} return { "id": v.id, "name": v.name, "location": v.location, "email": v.email, "lat": v.lat, "lng": v.lng, "city": v.city, "zone": v.zone, "is_active": v.is_active, "is_open": v.is_open } # vendor menu set # @app.post("/menu-item") # def create_menu_item(data: dict, db: Session = Depends(get_db)): # item = MenuItem( # name=data["name"], # category=data["category"], # description=data.get("description"), # ✅ NEW # vendor_price=data["vendor_price"], # selling_price=data["selling_price"] # ) # db.add(item) # db.commit() # db.refresh(item) # return {"id": item.id} # @app.put("/menu-item/{item_id}") # def update_menu_item(item_id: int, data: dict, db: Session = Depends(get_db)): # item = db.query(MenuItem).filter(MenuItem.id == item_id).first() # if not item: # return {"error": "Item not found"} # item.name = data["name"] # item.category = data["category"] # item.description = data.get("description") # ✅ NEW # item.vendor_price = data["vendor_price"] # item.selling_price = data["selling_price"] # db.commit() # return {"message": "Updated"} # iamge upload test from fastapi import UploadFile, File, Form import cloudinary.uploader @app.post("/menu-item") async def create_menu_item( name: str = Form(...), category: str = Form(...), description: str = Form(None), vendor_price: float = Form(...), selling_price: float = Form(...), image: UploadFile = File(None), db: Session = Depends(get_db) ): image_url = None if image: upload = cloudinary.uploader.upload(image.file) image_url = upload["secure_url"] item = MenuItem( name=name, category=category, description=description, vendor_price=vendor_price, selling_price=selling_price, image_url=image_url ) db.add(item) db.commit() db.refresh(item) return {"id": item.id, "image_url": image_url} @app.put("/menu-item/{item_id}") async def update_menu_item( item_id: int, name: str = Form(...), category: str = Form(...), description: str = Form(None), vendor_price: float = Form(...), selling_price: float = Form(...), image: UploadFile = File(None), db: Session = Depends(get_db) ): item = db.query(MenuItem).filter(MenuItem.id == item_id).first() if not item: return {"error": "Item not found"} if image: upload = cloudinary.uploader.upload(image.file) item.image_url = upload["secure_url"] item.name = name item.category = category item.description = description item.vendor_price = vendor_price item.selling_price = selling_price db.commit() return {"message": "Updated"} @app.delete("/menu-item/{item_id}") def delete_menu_item(item_id: int, db: Session = Depends(get_db)): item = db.query(MenuItem).filter(MenuItem.id == item_id).first() if not item: return {"error": "Item not found"} # 🔥 SOFT DELETE item.is_deleted = True db.commit() return {"message": "Item hidden successfully"} # VENDOR lOgin @app.post("/vendor/login") def vendor_login( data: dict, request: Request, db: Session = Depends(get_db) ): vendor = db.query(Vendor).filter( Vendor.email == data["email"] ).first() if not vendor: return {"error": "Vendor not found"} if not verify_password( data["password"], vendor.password ): return {"error": "Wrong password"} # ✅ SAVE IPS frontend_ip = data.get("frontend_ip") backend_ip = request.client.host vendor.frontend_ip = frontend_ip vendor.backend_ip = backend_ip db.commit() token = create_token({ "vendor_id": vendor.id, "role": "VENDOR", "city": vendor.city, "zone": vendor.zone }) return { "token": token, "vendor": { "id": vendor.id, "name": vendor.name, "email": vendor.email, "city": vendor.city, "zone": vendor.zone } } @app.get("/vendor/{vendor_id}/menu/vendor-view") def get_vendor_menu_view(vendor_id: int, db: Session = Depends(get_db)): results = ( db.query(VendorMenu, MenuItem) .join(MenuItem, VendorMenu.menu_item_id == MenuItem.id) .filter( VendorMenu.vendor_id == vendor_id, MenuItem.is_deleted == False # ✅ ADD ) .all() ) data = [] for vm, item in results: data.append({ "menu_id": vm.id, "item_id": item.id, # 🔥 ADD THIS (VERY IMPORTANT) "name": item.name, "category": item.category, "description": item.description, "vendor_price": item.vendor_price, "is_available": vm.is_available, "image_url": item.image_url # 🔥 ADD THIS }) return data @app.post("/vendor/{vendor_id}/menu") def add_item_to_vendor(vendor_id: int, data: dict, db: Session = Depends(get_db)): # 🔥 prevent duplicate existing = db.query(VendorMenu).filter( VendorMenu.vendor_id == vendor_id, VendorMenu.menu_item_id == data["menu_item_id"] ).first() if existing: return {"error": "Item already added"} vm = VendorMenu( vendor_id=vendor_id, menu_item_id=data["menu_item_id"], is_available=True ) db.add(vm) db.commit() return {"message": "Item added"} @app.put("/vendor/{vendor_id}/toggle-open") def toggle_vendor_open(vendor_id: int, db: Session = Depends(get_db)): vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: return {"error": "Vendor not found"} vendor.is_open = not vendor.is_open db.commit() return { "message": "Updated", "is_open": vendor.is_open } @app.put("/vendor/{vendor_id}/toggle-status") def toggle_vendor_status(vendor_id: int, db: Session = Depends(get_db)): vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: return {"error": "Vendor not found"} vendor.is_open = not vendor.is_open db.commit() db.refresh(vendor) # ✅ ADD THIS return { "message": "Updated", "is_open": vendor.is_open } @app.post("/user/address") def add_user_address( data: AddressCreate, request: Request, db: Session = Depends(get_db) ): backend_ip = request.client.host return crud.add_address( db, data, backend_ip ) @app.put("/user/address/{address_id}/set-default") def set_default_address( address_id: int, request: Request, db: Session = Depends(get_db) ): address = db.query(UserAddress).filter(UserAddress.id == address_id).first() if not address: return {"error": "Address not found"} # ❗ remove old default db.query(UserAddress).filter( UserAddress.user_id == address.user_id, UserAddress.is_default == True ).update({"is_default": False}) # ✅ set new default address.is_default = True db.commit() return {"message": "Default address updated"} @app.get("/user/{user_id}/addresses") def get_addresses(user_id: int, db: Session = Depends(get_db)): addresses = crud.get_user_addresses(db, user_id) return [ { "id": a.id, "flat": a.flat, "building": a.building, "landmark": a.landmark, "address_type": a.address_type, "is_default": a.is_default, "lat": a.lat, # ✅ ADD THIS "lng": a.lng } for a in addresses ] @app.get("/user/{user_id}/default-address") def default_address(user_id: int, db: Session = Depends(get_db)): crud.ensure_default_address(db, user_id) addr = crud.get_default_address(db, user_id) if not addr: return {"error": "No address found"} return { "id": addr.id, "flat": addr.flat, "building": addr.building, "landmark": addr.landmark, "address_type": addr.address_type, "lat": addr.lat, "lng": addr.lng } @app.put("/user/address/{address_id}") def update_address( address_id: int, data: dict, request: Request, db: Session = Depends(get_db) ): address = db.query(UserAddress).filter(UserAddress.id == address_id).first() if not address: return {"error": "Address not found"} address.flat = data.get("flat", address.flat) address.building = data.get("building", address.building) address.landmark = data.get("landmark", address.landmark) address.address_type = data.get("address_type", address.address_type) db.commit() return {"message": "Address updated"} @app.delete("/user/address/{address_id}") def delete_address( address_id: int, request: Request, db: Session = Depends(get_db) ): address = db.query(UserAddress).filter(UserAddress.id == address_id).first() if not address: return {"error": "Address not found"} user_id = address.user_id was_default = address.is_default db.delete(address) db.commit() # ✅ auto assign new default if was_default: other = db.query(UserAddress).filter( UserAddress.user_id == user_id ).first() if other: other.is_default = True db.commit() return {"message": "Deleted"} @app.put("/vendors/{vendor_id}") def update_vendor( vendor_id: int, data: VendorCreate, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: return {"error": "Vendor not found"} vendor.name = data.name vendor.location = data.location vendor.flat = data.flat vendor.building = data.building vendor.landmark = data.landmark vendor.lat = data.lat vendor.lng = data.lng vendor.email = data.email if data.password: vendor.password = hash_password(data.password) db.commit() return {"message": "Vendor updated"} @app.delete("/vendors/{vendor_id}") def delete_vendor( vendor_id: int, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: return {"error": "Vendor not found"} db.delete(vendor) db.commit() return {"message": "Vendor deleted"} @app.get("/user/{user_id}/orders") def get_user_orders(user_id: int, db: Session = Depends(get_db)): # 1. Fetch orders orders = db.query(Order).filter(Order.user_id == user_id).all() result = [] for o in orders: item_list = [] for i in o.items: # Look up the actual menu item to get its category menu_item = db.query(MenuItem).filter(MenuItem.id == i.menu_item_id).first() item_list.append({ "name": i.name, "quantity": i.quantity, "price": i.selling_price, "category": menu_item.category if menu_item else "Veg", "menu_item_id": i.menu_item_id, # ✅ ADD THIS LINE "vendor_id": i.vendor_id, # ✅ ADD THIS LINE "image": menu_item.image_url if menu_item else None # 👈 ADD THIS LINE }) # 3. Create a safe address dictionary # If address_ob is None, we provide fallback strings addr_data = { "flat": o.address_flat, "building": o.address_building, "landmark": o.address_landmark } # 👇 ADD THIS LINE to check if a review exists has_review = db.query(Review).filter(Review.order_id == o.id).first() is not None result.append({ "order_id": o.id, "status": o.status, "final": o.final_amount, "items": item_list, "payment_method": ( o.delivery.payment_method if o.delivery and o.delivery.payment_method else "UPI" ), "payment_status": o.payment_status, "created_at": o.created_at, "address": addr_data, "is_reviewed": has_review }) return result @app.get("/order/live/{order_id}") def get_live_order(order_id: int, db: Session = Depends(get_db)): # ========================= # GET ORDER # ========================= order = db.query(Order).filter( Order.id == order_id ).first() if not order: return {"error": "Order not found"} # ========================= # ORDER ITEMS # ========================= items_data = [] for item in order.items: items_data.append({ "name": item.name, "quantity": item.quantity, "price": item.selling_price, "status": item.status }) # ========================= # DELIVERY DETAILS # ========================= delivery = db.query(Delivery).filter( Delivery.order_id == order.id ).first() delivery_boy = None if delivery and delivery.delivery_boy_id: boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == delivery.delivery_boy_id ).first() if boy: delivery_boy = { "id": boy.id, "name": boy.name, "phone": boy.phone, "status": boy.status, "lat": boy.lat, "lng": boy.lng } # ========================= # RESPONSE # ========================= return { "order_id": order.id, "status": order.status, "payment_status": order.payment_status, "total_amount": order.total_amount, "gst": order.gst, "final_amount": order.final_amount, "created_at": order.created_at, "items": items_data, "delivery_boy": delivery_boy, "address": { "flat": order.address_flat, "building": order.address_building, "landmark": order.address_landmark } } @app.put("/vendor/order/accept/{order_id}") def accept_order(order_id: int, db: Session = Depends(get_db)): order = db.query(Order).filter(Order.id == order_id).first() if not order: return {"error": "Order not found"} order.status = "ACCEPTED" db.commit() return {"message": "Order accepted"} @app.put("/vendor/order/reject/{order_id}") def reject_order(order_id: int, data: dict, db: Session = Depends(get_db)): order = db.query(Order).filter(Order.id == order_id).first() if not order: return {"error": "Order not found"} order.status = "REJECTED" order.reject_reason = data.get("reason", "Not available") db.commit() return {"message": "Order rejected"} @app.get("/vendor/{vendor_id}/orders") def get_vendor_orders( vendor_id: int, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "VENDOR" or user.get("vendor_id") != vendor_id: return {"error": "Unauthorized"} orders_map = {} results = ( db.query(OrderItem, Order) .join(Order, OrderItem.order_id == Order.id) .filter(OrderItem.vendor_id == vendor_id) .all() ) orders_map = {} for i, order in results: if order.id not in orders_map: orders_map[order.id] = { "order_id": order.id, "user_id": order.user_id, "status": order.status, "items": [], "total_vendor_price": 0, "total_selling_price": 0, "gst": order.gst, "final_amount": order.final_amount } vendor_total = i.vendor_price * i.quantity selling_total = i.selling_price * i.quantity orders_map[order.id]["items"].append({ "name": i.name, "quantity": i.quantity, "vendor_price": i.vendor_price, "selling_price": i.selling_price, "status": i.status }) orders_map[order.id]["total_vendor_price"] += vendor_total orders_map[order.id]["total_selling_price"] += selling_total return list(orders_map.values()) @app.get("/admin/order/{order_id}") def get_order_details( order_id: int, db: Session = Depends(get_db), admin=Depends(get_current_admin) ): order = db.query(Order).filter(Order.id == order_id).first() if not order: return {"error": "Order not found"} if admin["role"] == "ZONAL_ADMIN" and order.zonal_admin_id != admin["admin_id"]: return {"error": "Not allowed"} user = db.query(User).filter(User.id == order.user_id).first() if not user: return {"error": "User not found"} # 🔥 ADDRESS full_address = f"{order.address_flat}, {order.address_building}, {order.address_landmark}" items = db.query(OrderItem).filter(OrderItem.order_id == order_id).all() # 🔥 DELIVERY BOY DETAILS delivery = db.query(Delivery).filter( Delivery.order_id == order_id ).first() delivery_boy = None if delivery and delivery.delivery_boy_id: boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == delivery.delivery_boy_id ).first() if boy: delivery_boy = { "id": boy.id, "name": boy.name, "phone": boy.phone } item_list = [] total_vendor = 0 total_selling = 0 for i in items: item_list.append({ "name": i.name, "quantity": i.quantity, "vendor_price": i.vendor_price, "selling_price": i.selling_price }) total_vendor += i.vendor_price * i.quantity total_selling += i.selling_price * i.quantity return { "order_id": order.id, "delivery_boy": delivery_boy, "status": order.status, "user_id": user.id, "user_name": user.name, "user_address": full_address, "items": item_list, "total_vendor": total_vendor, "total_selling": total_selling, "gst": order.gst, "final_amount": order.final_amount } @app.post("/admin/create-delivery-boy") def create_delivery_boy_api( data: DeliveryBoyCreate, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): return crud.create_delivery_boy(db, data, admin) @app.post("/delivery/login") def delivery_login( data: DeliveryBoyLogin, request: Request, db: Session = Depends(get_db) ): backend_ip = request.client.host return crud.login_delivery_boy( db, data, backend_ip ) @app.get("/delivery/me") def get_delivery_profile( db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() if not boy: return {"error": "Delivery boy not found"} return { "id": boy.id, "name": boy.name, "email": boy.email, "phone": boy.phone, "status": boy.status, "city": boy.city, "zone": boy.zone } # GET ALL @app.get("/admin/delivery-boys") def get_delivery_boys_api( db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): return crud.get_delivery_boys(db, admin) # UPDATE @app.put("/admin/delivery-boy/{boy_id}") def update_delivery_boy_api( boy_id: int, data: dict, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): return crud.update_delivery_boy(db, boy_id, data, admin) # DELETE @app.delete("/admin/delivery-boy/{boy_id}") def delete_delivery_boy_api( boy_id: int, db: Session = Depends(get_db), admin: dict = Depends(get_current_admin) ): return crud.delete_delivery_boy(db, boy_id, admin) ############################################## Batching functions ############################################## import asyncio from database import SessionLocal import crud from models import Batch async def batch_worker(): while True: try: db = SessionLocal() try: crud.auto_create_batches(db) finally: db.close() except Exception as e: print("Batch Error:", e) await asyncio.sleep(120) # every 2 minutes @app.on_event("startup") async def start_batching(): asyncio.create_task(batch_worker()) from models import Batch, DeliveryBoy @app.get("/admin/batches") def get_batches( db: Session = Depends(get_db), admin=Depends(get_current_admin) ): if not admin: return {"error": "Unauthorized"} if admin["role"] == "ZONAL_ADMIN": batches = db.query(Batch).filter( Batch.zonal_admin_id == admin["admin_id"] ).order_by(Batch.created_at.desc()).all() else: batches = db.query(Batch).order_by(Batch.created_at.desc()).all() result = [] for b in batches: batch_ready = True for o in b.orders: for i in o.items: if i.status != "READY": batch_ready = False break if not batch_ready: break # 🔥 GET DELIVERY BOY delivery_boy = None if b.delivery_boy_id: boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == b.delivery_boy_id ).first() if boy: delivery_boy = { "id": boy.id, "name": boy.name } result.append({ "batch_id": b.id, "status": b.status, "total_orders": len(b.orders), "created_at": b.created_at, "delivery_boy": delivery_boy, "ready_status": "FULL_READY" if batch_ready else "PARTIAL_READY" }) return result @app.get("/admin/batch/{batch_id}") def get_batch_details(batch_id: int, db: Session = Depends(get_db)): batch = db.query(Batch).filter(Batch.id == batch_id).first() if not batch: return {"error": "Batch not found"} orders_data = [] batch_ready = True # 🔥 track full batch for o in batch.orders: items = o.items item_list = [] vendor_map = {} order_ready = True # 🔥 track per order for i in items: item_list.append({ "name": i.name, "quantity": i.quantity, "vendor_id": i.vendor_id, "status": i.status # ✅ ADD THIS }) # group by vendor if i.vendor_id not in vendor_map: vendor_map[i.vendor_id] = [] vendor_map[i.vendor_id].append({ "name": i.name, "qty": i.quantity, "status": i.status # ✅ ADD THIS }) # ❌ if any item not ready if i.status != "READY": order_ready = False batch_ready = False orders_data.append({ "order_id": o.id, "user_id": o.user_id, "status": "READY" if order_ready else o.status, "ready_status": "READY" if order_ready else "PARTIAL_READY", "items": item_list, "vendors": vendor_map }) return { "batch_id": batch.id, "status": batch.status, "ready_status": "FULL_READY" if batch_ready else "PARTIAL_READY", "orders": orders_data } @app.get("/admin/single-orders") def get_single_orders( db: Session = Depends(get_db), admin = Depends(get_current_admin) ): if not admin: return {"error": "Unauthorized"} query = db.query(Order).filter( Order.batch_id == None, Order.status.notin_(["REJECTED"]) ) if admin["role"] == "ZONAL_ADMIN": query = query.filter( Order.zonal_admin_id == admin["admin_id"] ) orders = query.order_by( Order.created_at.desc() ).all() result = [] for o in orders: items = db.query(OrderItem).filter( OrderItem.order_id == o.id ).all() # 🔥 READY CHECK is_ready = all(i.status == "READY" for i in items) # 🔥 DELIVERY CHECK delivery = db.query(Delivery).filter( Delivery.order_id == o.id ).first() delivery_boy = None if delivery: boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == delivery.delivery_boy_id ).first() if boy: delivery_boy = { "id": boy.id, "name": boy.name } result.append({ "order_id": o.id, "user_id": o.user_id, "status": o.status, "created_at": o.created_at, "lat": o.user_lat, "lng": o.user_lng, # ✅ ASSIGNMENT INFO "assigned": True if delivery else False, "delivery_boy": delivery_boy, "ready_status": "READY" if is_ready else "NOT_READY" }) return result @app.post("/admin/order/assign") def assign_single_order( data: dict, db: Session = Depends(get_db), admin = Depends(get_current_admin) ): if not admin: return {"error": "Unauthorized"} # ✅ SAFE CHECK (ADD THIS) if "order_id" not in data or "delivery_boy_id" not in data: return {"error": "order_id and delivery_boy_id required"} order = db.query(Order).filter(Order.id == data["order_id"]).first() if admin["role"] == "ZONAL_ADMIN" and order.zonal_admin_id != admin["admin_id"]: return {"error": "Not your order"} if not order: return {"error": "Order not found"} if order.batch_id: return {"error": "This order is part of a batch"} # ❌ prevent duplicate assignment existing = db.query(Delivery).filter( Delivery.order_id == order.id ).first() if existing: return {"error": "Already assigned"} # ✅ GET DELIVERY BOY boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == data["delivery_boy_id"] ).first() if not boy: return {"error": "Delivery boy not found"} if boy.zonal_admin_id != admin["admin_id"]: return {"error": "Not your delivery boy"} # 🔥 IMPORTANT CHECKS if boy.status != "IDLE": return {"error": "Delivery boy is not available"} if boy.current_batch_id is not None or boy.current_order_id is not None: return {"error": "Delivery boy already assigned"} # ✅ CREATE DELIVERY delivery = Delivery( order_id=order.id, delivery_boy_id=boy.id, status="ASSIGNED" ) db.add(delivery) order.status = "ASSIGNED" boy.status = "BUSY" boy.current_order_id = order.id boy.current_batch_id = None db.commit() # ✅ PUSH DELIVERY BOY send_deliveryboy_order_notification( boy.id, order.id ) return { "message": "Order assigned successfully", "delivery_boy": { "id": boy.id, "name": boy.name } } @app.post("/admin/batch/assign") def assign_batch( data: dict, db: Session = Depends(get_db), admin=Depends(get_current_admin) ): if not admin or admin.get("role") not in ["CITY_ADMIN", "ZONAL_ADMIN"]: return {"error": "Unauthorized"} batch = db.query(Batch).filter( Batch.id == data.get("batch_id") ).first() if not batch: return {"error": "Batch not found"} if batch.zonal_admin_id != admin["admin_id"]: return {"error": "Not your batch"} if batch.delivery_boy_id: return {"error": "Batch already assigned"} # ✅ READY CHECK (FULL BATCH) for o in batch.orders: for i in o.items: if i.status != "READY": return {"error": "Batch not fully ready"} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == data.get("delivery_boy_id") ).first() if not boy: return {"error": "Delivery boy not found"} if boy.zonal_admin_id != admin["admin_id"]: return {"error": "Not your delivery boy"} if boy.status != "IDLE": return {"error": "Delivery boy not available"} if boy.current_batch_id is not None or boy.current_order_id is not None: return {"error": "Delivery boy already assigned"} try: # ✅ ASSIGN BATCH batch.delivery_boy_id = boy.id batch.status = "ASSIGNED" # ✅ UPDATE DELIVERY BOY boy.status = "BUSY" boy.current_batch_id = batch.id boy.current_order_id = None # ✅ CREATE DELIVERY ENTRY FOR EACH ORDER for o in batch.orders: o.status = "ASSIGNED" delivery = Delivery( order_id=o.id, delivery_boy_id=boy.id, status="ASSIGNED" ) db.add(delivery) db.commit() # ✅ PUSH DELIVERY BOY send_deliveryboy_batch_notification( boy.id, batch.id, len(batch.orders) ) except: db.rollback() return {"error": "Assignment failed"} return {"message": "Batch assigned successfully"} @app.get("/delivery/batches") def get_my_batches( db: Session = Depends(get_db), user=Depends(get_current_admin) # using same token system ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} batches = db.query(Batch).filter( Batch.delivery_boy_id == user["delivery_boy_id"] ).order_by(Batch.created_at.desc()).all() result = [] for b in batches: result.append({ "batch_id": b.id, "status": b.status, "total_orders": len(b.orders), "created_at": b.created_at }) return result from models import User, UserAddress @app.get("/delivery/batch/{batch_id}") def get_batch_for_delivery( batch_id: int, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} batch = db.query(Batch).filter(Batch.id == batch_id).first() if not batch: return {"error": "Batch not found"} if batch.delivery_boy_id != user["delivery_boy_id"]: return {"error": "Not your batch"} orders_data = [] for o in batch.orders: user_data = db.query(User).filter(User.id == o.user_id).first() orders_data.append({ "order_id": o.id, "lat": o.user_lat, "lng": o.user_lng, "status": o.status, "final_amount": o.final_amount, # ✅ USER "user": { "name": user_data.name if user_data else "", "phone": user_data.phone if user_data else "" }, # ✅ ADDRESS "address": { "flat": o.address_flat, "building": o.address_building, "landmark": o.address_landmark }, # ✅ ITEMS "items": [ { "name": i.name, "qty": i.quantity } for i in o.items ] }) return { "batch_id": batch.id, "status": batch.status, "orders": orders_data } @app.put("/delivery/batch/status") def update_batch_status( data: dict, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} batch = db.query(Batch).filter( Batch.id == data.get("batch_id") ).first() if not batch: return {"error": "Batch not found"} if batch.delivery_boy_id != user.get("delivery_boy_id"): return {"error": "Not your batch"} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() # ❌ prevent conflict with single order if boy.current_order_id: return {"error": "You have active single order"} new_status = data.get("status") allowed = ["AT_VENDOR", "PICKED_UP", "OUT_FOR_DELIVERY", "DELIVERED"] if new_status not in allowed: return {"error": "Invalid status"} # ========================= # 🚀 STATUS FLOW # ========================= if new_status == "AT_VENDOR": boy.status = "BUSY" batch.status = "AT_VENDOR" elif new_status == "PICKED_UP": batch.status = "PICKED_UP" boy.status = "ON_DELIVERY" for o in batch.orders: o.status = "PICKED_UP" delivery = db.query(Delivery).filter( Delivery.order_id == o.id ).first() if delivery: delivery.status = "PICKED_UP" else: print(f"⚠️ Missing delivery entry for order {o.id}") elif new_status == "OUT_FOR_DELIVERY": batch.status = "OUT_FOR_DELIVERY" boy.status = "ON_DELIVERY" for o in batch.orders: o.status = "OUT_FOR_DELIVERY" delivery = db.query(Delivery).filter( Delivery.order_id == o.id ).first() if delivery: delivery.status = "OUT_FOR_DELIVERY" send_user_out_for_delivery_notification( o.user_id, o.id ) elif new_status == "DELIVERED": order_id = data.get("order_id") if not order_id: return {"error": "order_id required"} order = db.query(Order).filter( Order.id == order_id ).first() if not order: return {"error": "Order not found"} # ✅ UPDATE ORDER order.status = "DELIVERED" delivery = db.query(Delivery).filter( Delivery.order_id == order.id ).first() if delivery: delivery.status = "DELIVERED" # ✅ SAVE CURRENT ORDER FIRST db.commit() # ✅ RELOAD BATCH db.refresh(batch) # ✅ check full batch all_done = all( o.status == "DELIVERED" for o in batch.orders ) if all_done: batch.status = "DELIVERED" boy.status = "IDLE" boy.current_batch_id = None db.commit() # ✅ SEND PUSH AFTER SAVE send_user_delivered_notification( order.user_id, order.id ) # ✅ VENDOR PUSH vendor_ids = set() for item in order.items: vendor_ids.add(item.vendor_id) for vendor_id in vendor_ids: send_vendor_delivered_notification( vendor_id, order.id, boy.name ) print("✅ BATCH DELIVERED PUSH SENT") # ✅ SAVE NORMAL STATUS if new_status != "DELIVERED": db.commit() return {"message": f"Updated to {new_status}"} @app.put("/vendor/order/ready") def mark_vendor_ready( data: dict, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "VENDOR": return {"error": "Unauthorized"} if user.get("vendor_id") != data.get("vendor_id"): return {"error": "Not your vendor"} items = db.query(OrderItem).filter( OrderItem.order_id == data["order_id"], OrderItem.vendor_id == data["vendor_id"] ).all() if not items: return {"error": "No items found"} # ✅ mark vendor items ready for i in items: i.status = "READY" db.commit() # 🔥 CHECK FULL ORDER READY all_items = db.query(OrderItem).filter( OrderItem.order_id == data["order_id"] ).all() all_ready = all(i.status == "READY" for i in all_items) if all_ready: order = db.query(Order).filter( Order.id == data["order_id"] ).first() if order: order.status = "READY" db.commit() return {"message": "Marked READY"} @app.get("/admin/batch/{batch_id}/ready") def is_batch_ready(batch_id: int, db: Session = Depends(get_db)): batch = db.query(Batch).filter(Batch.id == batch_id).first() if not batch: return {"error": "Batch not found"} for o in batch.orders: items = o.items for i in items: if i.status != "READY": return {"ready": False} return {"ready": True} @app.put("/delivery/order/status") def update_single_order_status( data: dict, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() order_id = data.get("order_id") # ✅ USE ORDER ID DIRECTLY if order_id: order = db.query(Order).filter( Order.id == order_id ).first() else: if not boy or not boy.current_order_id: return {"error": "No active order"} order = db.query(Order).filter( Order.id == boy.current_order_id ).first() if not order: return {"error": "Order not found"} delivery = db.query(Delivery).filter( Delivery.order_id == order.id ).first() new_status = data.get("status") allowed = ["AT_VENDOR", "PICKED_UP", "OUT_FOR_DELIVERY", "DELIVERED"] if new_status not in allowed: return {"error": "Invalid status"} # 🚀 FLOW CONTROL if new_status == "AT_VENDOR": order.status = "AT_VENDOR" boy.status = "BUSY" elif new_status == "PICKED_UP": order.status = "PICKED_UP" boy.status = "ON_DELIVERY" elif new_status == "OUT_FOR_DELIVERY": order.status = "OUT_FOR_DELIVERY" boy.status = "ON_DELIVERY" # 🔥 PUSH NOTIFICATION try: send_user_out_for_delivery_notification( order.user_id, order.id ) print("✅ OUT_FOR_DELIVERY PUSH SENT") except Exception as e: print("❌ OUT_FOR_DELIVERY PUSH ERROR:", e) elif new_status == "DELIVERED": # ✅ UPDATE STATUS order.status = "DELIVERED" if delivery: delivery.status = "DELIVERED" # ✅ RESET DELIVERY BOY boy.status = "IDLE" boy.current_order_id = None boy.current_batch_id = None # ✅ SAVE FIRST db.commit() # ✅ SEND PUSH AFTER DB UPDATE send_user_delivered_notification( order.user_id, order.id ) # ✅ VENDOR PUSH vendor_ids = set() for item in order.items: vendor_ids.add(item.vendor_id) for vendor_id in vendor_ids: send_vendor_delivered_notification( vendor_id, order.id, boy.name ) print("✅ DELIVERED PUSH SENT") return {"message": "Updated to DELIVERED"} # ✅ skip second commit for delivered if new_status != "DELIVERED": if delivery: delivery.status = new_status db.commit() return {"message": f"Updated to {new_status}"} @app.put("/delivery/order/cancel") def cancel_single_order( data: dict, db: Session = Depends(get_db), user=Depends(get_current_admin) ): # ========================= # AUTH CHECK # ========================= if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} # ========================= # DELIVERY BOY # ========================= boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() if not boy: return {"error": "Delivery boy not found"} # ========================= # ORDER # ========================= order = db.query(Order).filter( Order.id == data.get("order_id") ).first() if not order: return {"error": "Order not found"} # ========================= # REASON REQUIRED # ========================= reason = data.get("reason") if not reason or reason.strip() == "": return {"error": "Cancel reason required"} # ========================= # PREVENT INVALID CANCEL # ========================= if order.status == "DELIVERED": return {"error": "Delivered order cannot be cancelled"} if order.status == "CANCELLED": return {"error": "Order already cancelled"} # ========================= # UPDATE ORDER # ========================= order.status = "CANCELLED" order.cancel_reason = reason order.cancelled_by = boy.name order.cancelled_at = datetime.utcnow() # ========================= # UPDATE DELIVERY # ========================= delivery = db.query(Delivery).filter( Delivery.order_id == order.id ).first() if delivery: delivery.status = "CANCELLED" # ========================= # RESET DELIVERY BOY # ========================= if boy.current_order_id == order.id: boy.current_order_id = None boy.status = "IDLE" # ========================= # HANDLE BATCH # ========================= if order.batch_id: batch = db.query(Batch).filter( Batch.id == order.batch_id ).first() if batch: remaining = db.query(Order).filter( Order.batch_id == batch.id, Order.status.notin_(["DELIVERED", "CANCELLED"]) ).count() if remaining == 0: batch.status = "COMPLETED" boy.current_batch_id = None boy.status = "IDLE" # ========================= # ✅ VENDOR PUSH # ========================= vendor_ids = set() for item in order.items: vendor_ids.add(item.vendor_id) for vendor_id in vendor_ids: send_vendor_cancelled_notification( vendor_id, order.id, reason ) # ✅ USER PUSH send_user_cancelled_notification( order.user_id, order.id, reason ) db.commit() return { "success": True, "message": "Order cancelled successfully" } @app.post("/delivery/update-location") def update_location( data: dict, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() if not boy: return {"error": "Delivery boy not found"} boy.lat = data.get("lat") boy.lng = data.get("lng") db.commit() return {"message": "Location updated"} @app.get("/admin/delivery-boy/{boy_id}/location") def get_delivery_boy_location( boy_id: int, db: Session = Depends(get_db), admin=Depends(get_current_admin) ): boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == boy_id ).first() if not boy: return {"error": "Not found"} return { "lat": boy.lat, "lng": boy.lng, "status": boy.status } @app.get("/delivery/my-work") def get_my_work( db: Session = Depends(get_db), user: dict = Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} # 🔥 SAFE QUERY boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user.get("delivery_boy_id") ).first() if not boy: return {"error": "Delivery boy not found"} # ========================= # ✅ BATCH WORK # ========================= if boy.current_batch_id: batch = db.query(Batch).filter( Batch.id == boy.current_batch_id ).first() # ❌ stale batch cleanup if not batch: boy.current_batch_id = None boy.status = "IDLE" db.commit() return { "type": "NONE", "delivery_status": boy.status, "delivery_boy_name": boy.name } # ✅ check remaining active orders remaining_orders = db.query(Order).filter( Order.batch_id == batch.id, Order.status.notin_(["DELIVERED", "CANCELLED"]) ).count() # ✅ all done if remaining_orders == 0: batch.status = "COMPLETED" boy.current_batch_id = None boy.status = "IDLE" db.commit() return { "type": "NONE", "delivery_status": boy.status, "delivery_boy_name": boy.name } return { "type": "BATCH", "batch_id": batch.id, "status": batch.status, "delivery_status": boy.status, "delivery_boy_name": boy.name } # ========================= # ✅ SINGLE ORDER WORK # ========================= if boy.current_order_id: order = db.query(Order).filter( Order.id == boy.current_order_id ).first() # 🔥 FIX STALE ORDER if not order: return {"error": "Order not found"} user_data = db.query(User).filter( User.id == order.user_id ).first() return { "type": "ORDER", "delivery_boy_name": boy.name, "delivery_status": boy.status, "order_id": order.id, "status": order.status, "lat": order.user_lat, "lng": order.user_lng, "user": { "name": user_data.name if user_data else "", "phone": user_data.phone if user_data else "" }, "address": { "flat": order.address_flat, "building": order.address_building, "landmark": order.address_landmark } } # ========================= # ✅ NO ACTIVE WORK # ========================= return { "type": "NONE", "delivery_status": boy.status, "delivery_boy_name": boy.name } @app.put("/delivery/toggle-status") def toggle_delivery_status( db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() if not boy: return {"error": "Delivery boy not found"} # ❌ cannot go offline during work if boy.current_batch_id or boy.current_order_id: return {"error": "Complete current delivery first"} # 🔥 TOGGLE if boy.status == "OFFLINE": boy.status = "IDLE" is_active = True else: boy.status = "OFFLINE" is_active = False db.commit() return { "success": True, "active": is_active, "status": boy.status } @app.get("/delivery/order/{order_id}") def get_order_for_delivery( order_id: int, db: Session = Depends(get_db), user=Depends(get_current_admin) ): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized", "items": []} # ✅ ALWAYS include items order = db.query(Order).filter(Order.id == order_id).first() if not order: return {"error": "Order not found", "items": []} boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() if not boy or boy.current_order_id != order.id: return {"error": "Not your order", "items": []} user_data = db.query(User).filter(User.id == order.user_id).first() # ✅ SAFELY BUILD ITEMS items_list = [] if order.items: for i in order.items: items_list.append({ "name": i.name, "qty": i.quantity }) return { "order_id": order.id, "status": order.status, "lat": order.user_lat, "lng": order.user_lng, # 💰 PAYMENT INFO (🔥 ADD THIS) "final_amount": order.final_amount, "payment_status": order.payment_status, # 👤 USER "user": { "name": user_data.name if user_data else "", "phone": user_data.phone if user_data else "" }, # 📍 ADDRESS "address": { "flat": order.address_flat, "building": order.address_building, "landmark": order.address_landmark }, # 📦 ITEMS (ALWAYS SAFE) "items": items_list } from helpers.payment import generate_upi_link @app.get("/delivery/order/{order_id}/upi") def get_upi_payment(order_id: int, db: Session = Depends(get_db)): order = db.query(Order).filter(Order.id == order_id).first() if not order: return {"error": "Order not found"} upi_url = generate_upi_link(order.final_amount, order.id) return { "upi_url": upi_url, "amount": order.final_amount } @app.post("/delivery/upload-payment-proof") async def upload_payment_proof( file: UploadFile = File(...), user=Depends(get_current_admin) ): # ✅ only delivery boys if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} # ✅ validate type allowed = [ "image/jpeg", "image/png", "image/webp" ] if file.content_type not in allowed: return {"error": "Invalid image type"} try: upload = cloudinary.uploader.upload( file.file, folder="payment_proofs" ) return { "success": True, "url": upload["secure_url"] } except Exception as e: print("UPLOAD ERROR:", e) return { "error": "Upload failed" } @app.get("/invoice/{order_id}") def get_invoice_data( order_id: int, db: Session = Depends(get_db) ): # ========================= # GET ORDER # ========================= order = db.query(Order).filter( Order.id == order_id ).first() if not order: return {"error": "Order not found"} # ========================= # OPTIONAL SECURITY # ========================= # Only paid orders should have invoice if order.payment_status != "PAID": return {"error": "Invoice unavailable until payment completed"} # ========================= # GET USER # ========================= user = db.query(User).filter( User.id == order.user_id ).first() # ========================= # GET ITEMS # ========================= items = db.query(OrderItem).filter( OrderItem.order_id == order.id ).all() item_data = [] for item in items: taxable = item.selling_price * item.quantity gst_amount = round(taxable * 0.05, 2) item_data.append({ "name": item.name, "quantity": item.quantity, "price": item.selling_price, "hsn": "2106", "gst_rate": "5%", "taxable_value": taxable, "total": taxable + gst_amount }) # ========================= # RESPONSE # ========================= return { # COMPANY "company": { "name": "MEALNO (Partnership Firm)", "email": "support@mealno.com", "gstin": "19ABCDE1234F1Z5", "pan": "ABCDE1234F", "fssai": "1234567890", "address": "Kolkata, West Bengal" }, # INVOICE DETAILS "invoice": { "invoice_no": f"INV-{order.id}", "order_id": order.id, "invoice_date": order.created_at.strftime("%d-%m-%Y"), "place_of_supply": "West Bengal (19)" }, # CUSTOMER "customer": { "name": user.name, "phone": user.phone, "address": { "flat": order.address_flat, "building": order.address_building, "landmark": order.address_landmark } }, # ITEMS "items": item_data, # SUMMARY "summary": { "taxable_value": order.total_amount, "cgst": round(order.gst / 2, 2), "sgst": round(order.gst / 2, 2), "gst": order.gst, "grand_total": order.final_amount }, # PAYMENT "payment_status": order.payment_status, # ORDER STATUS "order_status": order.status } # Updated get_user_orders function for main.py from models import Review # Ensure this is at the top of the file @app.post("/order/review") def submit_review(data: ReviewCreate, db: Session = Depends(get_db)): print("DEBUG: Review endpoint was hit!") # 1. Check if review already exists existing = db.query(Review).filter(Review.order_id == data.order_id).first() if existing: return {"error": "Order already reviewed"} # 2. Get the order to extract the zonal_admin_id automatically order = db.query(Order).filter(Order.id == data.order_id).first() if not order: return {"error": "Order not found"} # 3. Save the review review = Review( user_id=data.user_id, order_id=data.order_id, zonal_admin_id=order.zonal_admin_id, rating=data.rating, comment=data.comment ) db.add(review) db.commit() return {"success": True, "message": "Review submitted successfully!"} @app.get("/user/{user_id}/reviews") def get_user_reviews(user_id: int, db: Session = Depends(get_db)): # Fetch reviews joined with order data reviews = db.query(Review).filter(Review.user_id == user_id).order_by(Review.created_at.desc()).all() result = [] for r in reviews: order = db.query(Order).filter(Order.id == r.order_id).first() # Get item names for context (e.g., "Chicken Biryani, Roti...") item_names = [] if order: items = db.query(OrderItem).filter(OrderItem.order_id == order.id).all() item_names = [i.name for i in items] result.append({ "review_id": r.id, "order_id": r.order_id, "rating": r.rating, "comment": r.comment, "created_at": r.created_at, "items_summary": ", ".join(item_names) if item_names else "Order Items" }) return result # ========================= # SUPPORT TICKET # ========================= @app.post("/support-ticket") def create_support_ticket_api( data: SupportTicketCreate, request: Request, db: Session = Depends(get_db) ): backend_ip = request.client.host return crud.create_support_ticket( db, data, backend_ip ) @app.get("/admin/support-tickets") def admin_support_tickets( db: Session = Depends(get_db), admin=Depends(get_current_admin) ): return crud.get_support_tickets(db)