from sqlalchemy.orm import Session from models import VendorMenu, MenuItem import random from models import Vendor,Order, OrderItem, MenuItem,Delivery, Order,User from utils import verify_password, create_token,haversine,hash_password from models import DeliveryBoy, Batch,SupportTicket from notifications.sendnotification import ( send_vendor_new_order_notification, send_vendor_delivered_notification, send_vendor_cancelled_notification ) def find_nearest_vendor(db, lat, lng): vendors = db.query(Vendor).filter( Vendor.is_active == True, Vendor.is_open == True # โœ… ADD THIS ).all() available = [] for v in vendors: if v.lat is None or v.lng is None: continue distance = haversine(lat, lng, v.lat, v.lng) if distance <= v.radius: available.append((v, distance)) if not available: return None available.sort(key=lambda x: x[1]) return available[0] # (vendor, distance) def get_vendor_menu(db: Session, vendor_id: int): results = ( db.query(VendorMenu, MenuItem) .join(MenuItem, VendorMenu.menu_item_id == MenuItem.id) .filter( VendorMenu.vendor_id == vendor_id, VendorMenu.is_available == True ) .all() ) menu = [] for vm, item in results: menu.append({ "id": item.id, "name": item.name, "category": item.category, "description": item.description, "price": item.selling_price, "image_url": item.image_url # ๐Ÿ”ฅ ADD THIS }) return menu def create_order(db: Session, order_data, backend_ip): total = 0 order_items_data = [] # ========================= # ๐Ÿ”น PROCESS ITEMS # ========================= for item in order_data.items: menu_item = db.query(MenuItem).filter( MenuItem.id == item.menu_item_id ).first() if not menu_item: raise Exception(f"Item {item.menu_item_id} not found") # ๐Ÿ”ฅ CHECK ITEM AVAILABLE FOR THIS VENDOR vm = db.query(VendorMenu).filter( VendorMenu.menu_item_id == menu_item.id, VendorMenu.vendor_id == item.vendor_id, VendorMenu.is_available == True ).first() if not vm: raise Exception(f"Item {menu_item.name} not available") vendor_id = vm.vendor_id item_total = menu_item.selling_price * item.quantity total += item_total order_items_data.append({ "menu_item_id": menu_item.id, "vendor_id": vendor_id, "quantity": item.quantity, "name": menu_item.name, "vendor_price": menu_item.vendor_price, "selling_price": menu_item.selling_price }) # ========================= # โŒ SAFETY CHECK # ========================= if not order_items_data: raise Exception("No valid items in order") # ========================= # ๐Ÿ”ฅ ENSURE SAME ZONAL ADMIN # ========================= admin_ids = set() for item in order_items_data: vendor = db.query(Vendor).filter( Vendor.id == item["vendor_id"] ).first() if vendor: admin_ids.add(vendor.zonal_admin_id) if len(admin_ids) != 1: raise Exception("All items must belong to same zonal admin") zonal_admin_id = list(admin_ids)[0] # ========================= # ๐Ÿ“ GET USER ADDRESS # ========================= from models import UserAddress address = db.query(UserAddress).filter( UserAddress.user_id == order_data.user_id, UserAddress.is_default == True ).first() # ========================= # ๐Ÿ’ฐ CALCULATIONS # ========================= gst = round(total * 0.05, 2) final_amount = round(total + gst, 2) # ========================= # ๐Ÿงพ CREATE ORDER # ========================= # โœ… Prevent duplicate orders from rapid double click recent_order = ( db.query(Order) .filter(Order.user_id == order_data.user_id) .order_by(Order.created_at.desc()) .first() ) if recent_order: time_diff = datetime.utcnow() - recent_order.created_at # block duplicate within 5 seconds if time_diff.total_seconds() < 5: raise Exception("Duplicate order detected") order = Order( user_id=order_data.user_id, frontend_ip=order_data.frontend_ip, backend_ip=backend_ip, user_lat=order_data.lat, user_lng=order_data.lng, # ๐Ÿ”ฅ SAVE ADDRESS SNAPSHOT address_flat=address.flat if address else "", address_building=address.building if address else "", address_landmark=address.landmark if address else "", address_type=address.address_type if address else "", address_lat=address.lat if address else None, address_lng=address.lng if address else None, total_amount=total, gst=gst, final_amount=final_amount, status="PLACED", payment_status="PENDING", zonal_admin_id=zonal_admin_id ) db.add(order) db.flush() # get order.id before commit # ========================= # ๐Ÿ“ฆ INSERT ORDER ITEMS # ========================= for item_data in order_items_data: db_item = OrderItem(order_id=order.id, **item_data) db.add(db_item) db.commit() db.refresh(order) # ========================= # ๐Ÿ”” SEND VENDOR PUSH # ========================= vendor_ids = set() for item in order_items_data: vendor_ids.add(item["vendor_id"]) for vendor_id in vendor_ids: send_vendor_new_order_notification( vendor_id, order.id ) # ========================= # โœ… RESPONSE # ========================= return { "order_id": order.id, "final_amount": final_amount } # def update_delivery_status(db, data, user): # if user.get("role") != "DELIVERY_BOY": # return {"error": "Unauthorized"} # boy = db.query(DeliveryBoy).filter( # DeliveryBoy.id == user["delivery_boy_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"} # boy.status = new_status # # ========================= # # โœ… SINGLE ORDER FLOW # # ========================= # if boy.current_order_id: # order = db.query(Order).filter( # Order.id == boy.current_order_id # ).first() # if new_status != "AT_VENDOR": # order.status = new_status # if new_status == "DELIVERED": # order.status = "DELIVERED" # boy.status = "IDLE" # boy.current_order_id = None # # ========================= # # โœ… BATCH FLOW # # ========================= # elif boy.current_batch_id: # batch = db.query(Batch).filter( # Batch.id == boy.current_batch_id # ).first() # if new_status == "PICKED_UP": # batch.status = "PICKED_UP" # for o in batch.orders: # o.status = "PICKED_UP" # elif new_status == "OUT_FOR_DELIVERY": # batch.status = "OUT_FOR_DELIVERY" # for o in batch.orders: # o.status = "OUT_FOR_DELIVERY" # elif new_status == "DELIVERED": # # ๐Ÿ”ฅ Deliver ONE ORDER at a time (frontend controls which) # order_id = data.get("order_id") # order = db.query(Order).filter(Order.id == order_id).first() # order.status = "DELIVERED" # # ๐Ÿ”ฅ FINAL RULE (YOUR CORE LOGIC) # if all(o.status == "DELIVERED" for o in batch.orders): # batch.status = "DELIVERED" # boy.status = "IDLE" # boy.current_batch_id = None # db.commit() # return {"message": f"Updated to {new_status}"} def add_payment(db, data, user): if user.get("role") != "DELIVERY_BOY": return {"error": "Unauthorized"} delivery = db.query(Delivery).filter( Delivery.order_id == data.order_id ).first() if not delivery: return {"error": "Delivery not found"} order = db.query(Order).filter(Order.id == data.order_id).first() if not order: return {"error": "Order not found"} # โœ… VALIDATION if data.payment_method not in ["COD", "UPI"]: return {"error": "Invalid payment method"} # โœ… UPI proof required if data.payment_method == "UPI": if not data.payment_proof: return {"error": "Payment screenshot required"} # โœ… basic cloudinary validation if "cloudinary.com" not in data.payment_proof: return {"error": "Invalid payment proof"} # โœ… STORE PAYMENT delivery.payment_method = data.payment_method delivery.payment_proof = data.payment_proof delivery.payment_amount = data.amount delivery.status = "COMPLETED" # โœ… UPDATE ORDER order.payment_status = "PAID" order.status = "DELIVERED" # โœ… RESET DELIVERY BOY boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == user["delivery_boy_id"] ).first() if boy.current_order_id == order.id: boy.current_order_id = None boy.status = "IDLE" db.commit() return {"message": "Payment recorded & order completed"} # User Login # def create_user(db, data): # existing = db.query(User).filter( # (User.phone == data.phone) | (User.email == data.email) # ).first() # if existing: # return {"error": "User already exists"} # user = User( # name=data.name, # phone=data.phone, # email=data.email, # password=hash_password(data.password), # address=data.address # ) # db.add(user) # db.commit() # db.refresh(user) # # ๐Ÿ”ฅ ADD TOKEN # token = create_token({"user_id": user.id}) # return { # "token": token, # "user": { # "id": user.id, # "name": user.name # } # } def login_user(db, data): user = db.query(User).filter( (User.phone == data.phone) | (User.email == data.email) ).first() if not user: return {"error": "User not found"} if not verify_password(data.password, user.password): return {"error": "Wrong password"} token = create_token({"user_id": user.id}) return { "token": token, "user": { "id": user.id, "name": user.name } } # twilo from dotenv import load_dotenv import os from twilio.rest import Client load_dotenv() # โœ… helper def get_twilio_client(): return Client( os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN") ) def normalize_phone(phone: str): return phone.replace("+91", "").strip() # ================= OTP ================= def send_otp(data): try: if not data.phone: return {"success": False, "error": "Phone required"} clean_phone = normalize_phone(data.phone) phone = "+91" + clean_phone client = get_twilio_client() client.verify.v2.services( os.getenv("TWILIO_VERIFY_SERVICE_SID") ).verifications.create( to=phone, channel='sms' ) return {"success": True} except Exception as e: print("TWILIO ERROR:", e) return {"success": False, "error": str(e)} def verify_otp(db, data, backend_ip): try: clean_phone = normalize_phone(data.phone) phone = "+91" + clean_phone client = get_twilio_client() verification_check = client.verify.v2.services( os.getenv("TWILIO_VERIFY_SERVICE_SID") ).verification_checks.create( to=phone, code=data.otp ) if verification_check.status == "approved": # โœ… FIND EXISTING USER user = db.query(User).filter( User.phone == clean_phone ).first() # ===================================== # โœ… NEW USER REGISTRATION # ===================================== if not user: print("\n==============================") print("๐Ÿ”ฅ NEW USER REGISTERED") print("๐Ÿ“ฑ Frontend IP:", data.frontend_ip) print("๐Ÿ–ฅ๏ธ Backend IP :", backend_ip) print("==============================\n") user = User( phone=clean_phone, name=data.name or "User", email=data.email, # โœ… SAVE IPS frontend_ip=data.frontend_ip, backend_ip=backend_ip ) db.add(user) db.commit() db.refresh(user) # ===================================== # โœ… EXISTING USER LOGIN # ===================================== else: # โœ… UPDATE LATEST IP user.frontend_ip = data.frontend_ip user.backend_ip = backend_ip db.commit() # โœ… TOKEN token = create_token({ "user_id": user.id, "role": "USER" }) return { "success": True, "token": token, "user": { "id": user.id, "phone": user.phone, "name": user.name, "email": user.email, "dob": user.dob, "gender": user.gender, # โœ… OPTIONAL RETURN "frontend_ip": user.frontend_ip, "backend_ip": user.backend_ip } } return { "success": False, "message": "Invalid OTP" } except Exception as e: print("VERIFY ERROR:", e) return { "success": False, "error": str(e) } # import random # import requests # import os # def send_otp(data): # phone = data.phone # AUTH_KEY = os.getenv("MSG91_AUTH_KEY") # url = f"https://control.msg91.com/api/v5/otp?mobile=91{phone}" # headers = { # "authkey": AUTH_KEY # } # response = requests.get(url, headers=headers) # print("MSG91 RESPONSE:", response.text) # return {"message": "OTP sent"} # def verify_otp(db, data): # AUTH_KEY = os.getenv("MSG91_AUTH_KEY") # url = "https://control.msg91.com/api/v5/otp/verify" # payload = { # "mobile": f"91{data.phone}", # "otp": data.otp # } # headers = { # "authkey": AUTH_KEY, # "Content-Type": "application/json" # } # response = requests.post(url, json=payload, headers=headers) # result = response.json() # print("VERIFY RESPONSE:", result) # if result.get("type") != "success": # return {"error": "Invalid OTP"} # # existing logic (KEEP SAME) # user = db.query(User).filter(User.phone == data.phone).first() # if not user: # user = User( # phone=data.phone, # name=getattr(data, "name", "User"), # email=getattr(data, "email", None) # ) # db.add(user) # db.commit() # db.refresh(user) # token = create_token({"user_id": user.id}) # return { # "token": token, # "user": { # "id": user.id, # "phone": user.phone, # "name": user.name # } # } ## Admin from models import Admin def login_admin(db, data): admin = db.query(Admin).filter(Admin.email == data.email).first() if not admin: return {"error": "Admin not found"} if not verify_password(data.password, admin.password): return {"error": "Wrong password"} token = create_token({ "admin_id": admin.id, "role": admin.role, "city": admin.city, "zone": admin.zone }) return { "token": token, "admin": { "id": admin.id, "name": admin.name, "email": admin.email, "role": admin.role, "city": admin.city, "zone": admin.zone } } def create_zonal_admin(db, data, current_admin): if current_admin["role"] != "CITY_ADMIN": return {"error": "Not allowed"} existing = db.query(Admin).filter(Admin.email == data.email).first() if existing: return {"error": "Admin already exists"} admin = Admin( name=data.name, email=data.email, password=hash_password(data.password), role="ZONAL_ADMIN", city=current_admin["city"], zone=data.zone ) db.add(admin) db.commit() return {"message": "Zonal admin created"} def get_vendors_by_admin(db, admin): vendors = [] if admin["role"] == "CITY_ADMIN": data = db.query(Vendor).filter( Vendor.city == admin["city"] ).all() elif admin["role"] == "ZONAL_ADMIN": data = db.query(Vendor).filter( Vendor.city == admin["city"], Vendor.zone == admin["zone"] ).all() else: return [] for v in data: vendors.append({ "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, "zonal_admin_id": v.zonal_admin_id }) return vendors def create_vendor(db, data, admin): if admin["role"] != "ZONAL_ADMIN": return {"error": "Only zonal admin can create vendor"} if data.lat is None or data.lng is None: return {"error": "Latitude & Longitude required"} vendor = Vendor( name=data.name, location=data.location, flat=data.flat, building=data.building, landmark=data.landmark, lat=data.lat, lng=data.lng, email=data.email, password=hash_password(data.password), city=admin["city"], zone=admin["zone"], zonal_admin_id=admin["admin_id"], is_active=True, # โœ… vendor is approved by default is_open=False # โœ… shop closed initially ) db.add(vendor) db.commit() db.refresh(vendor) return { "message": "Vendor created", "vendor_id": vendor.id } def get_zonal_admins(db, admin): if admin["role"] != "CITY_ADMIN": return {"error": "Not allowed"} admins = db.query(Admin).filter( Admin.role == "ZONAL_ADMIN", Admin.city == admin["city"] ).all() result = [] for a in admins: result.append({ "id": a.id, "name": a.name, "email": a.email, "zone": a.zone }) return result # user address from models import UserAddress def ensure_default_address(db, user_id): default = db.query(UserAddress).filter( UserAddress.user_id == user_id, UserAddress.is_default == True ).first() if not default: first = db.query(UserAddress).filter( UserAddress.user_id == user_id ).first() if first: first.is_default = True db.commit() def add_address(db, data, backend_ip): address = UserAddress( user_id=data.user_id, flat=data.flat, building=data.building, landmark=data.landmark, lat=data.lat, lng=data.lng, address_type=data.address_type, is_default=False, frontend_ip=data.frontend_ip, backend_ip=backend_ip, ) db.add(address) db.commit() db.refresh(address) # โœ… auto default logic first = db.query(UserAddress).filter( UserAddress.user_id == data.user_id ).count() if first == 1: address.is_default = True db.commit() # ๐Ÿ”ฅ RETURN JSON (FIX) return { "id": address.id, "flat": address.flat, "building": address.building, "landmark": address.landmark, "lat": address.lat, "lng": address.lng, "address_type": address.address_type, "is_default": address.is_default } def get_user_addresses(db, user_id): return db.query(UserAddress).filter( UserAddress.user_id == user_id ).all() def get_default_address(db, user_id): return db.query(UserAddress).filter( UserAddress.user_id == user_id, UserAddress.is_default == True ).first() from models import DeliveryBoy def create_delivery_boy(db, data, admin): if admin["role"] != "ZONAL_ADMIN": return {"error": "Only zonal admin can add delivery boy"} existing = db.query(DeliveryBoy).filter( DeliveryBoy.email == data.email ).first() if existing: return {"error": "Delivery boy already exists"} boy = DeliveryBoy( name=data.name, email=data.email, phone=data.phone, password=hash_password(data.password), city=admin["city"], zone=admin["zone"], zonal_admin_id=admin["admin_id"] ) db.add(boy) db.commit() return {"message": "Delivery boy created"} def login_delivery_boy( db, data, backend_ip ): boy = db.query(DeliveryBoy).filter( DeliveryBoy.email == data.email ).first() if not boy: return {"error": "Delivery boy not found"} if not verify_password( data.password, boy.password ): return {"error": "Wrong password"} # โœ… SAVE IPS boy.frontend_ip = data.frontend_ip boy.backend_ip = backend_ip db.commit() token = create_token({ "delivery_boy_id": boy.id, "role": "DELIVERY_BOY", "city": boy.city, "zone": boy.zone }) return { "token": token, "delivery_boy": { "id": boy.id, "name": boy.name, "email": boy.email, "phone": boy.phone, "status": boy.status, "city": boy.city, "zone": boy.zone, # โœ… OPTIONAL RETURN "frontend_ip": boy.frontend_ip, "backend_ip": boy.backend_ip } } from models import DeliveryBoy # โœ… GET ALL DELIVERY BOYS def get_delivery_boys(db, admin): if admin["role"] != "ZONAL_ADMIN": return [] boys = db.query(DeliveryBoy).filter( DeliveryBoy.zonal_admin_id == admin["admin_id"] ).all() return [ { "id": b.id, "name": b.name, "email": b.email, "phone": b.phone, "status": b.status, "active": b.status != "OFFLINE", "zone": b.zone, "city": b.city, "current_order_id": b.current_order_id, "current_batch_id": b.current_batch_id } for b in boys ] # โœ… UPDATE DELIVERY BOY def update_delivery_boy(db, boy_id, data, admin): boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == boy_id, DeliveryBoy.zonal_admin_id == admin["admin_id"] ).first() if not boy: return {"error": "Not found"} if admin["role"] != "ZONAL_ADMIN": return {"error": "Not allowed"} boy.name = data.get("name", boy.name) boy.email = data.get("email", boy.email) boy.phone = data.get("phone", boy.phone) if data.get("password"): boy.password = hash_password(data["password"]) db.commit() return {"message": "Updated"} # โœ… DELETE DELIVERY BOY def delete_delivery_boy(db, boy_id, admin): boy = db.query(DeliveryBoy).filter( DeliveryBoy.id == boy_id, DeliveryBoy.zonal_admin_id == admin["admin_id"] ).first() if not boy: return {"error": "Not found"} if admin["role"] != "ZONAL_ADMIN": return {"error": "Not allowed"} db.delete(boy) db.commit() return {"message": "Deleted"} #################################################### Batching Function #################################################### from datetime import datetime, timedelta from models import Order, Batch from utils import haversine def auto_create_batches(db): print("๐Ÿš€ Running auto batching...") time_threshold = datetime.utcnow() - timedelta(minutes=3) orders = db.query(Order).filter( Order.batch_id == None, Order.status.in_(["PLACED", "ACCEPTED", "READY"]), Order.created_at <= time_threshold, ~Order.id.in_(db.query(Delivery.order_id)) ).all() # โœ… GROUP BY ZONAL ADMIN orders_by_admin = {} for o in orders: if o.zonal_admin_id not in orders_by_admin: orders_by_admin[o.zonal_admin_id] = [] orders_by_admin[o.zonal_admin_id].append(o) # โœ… PROCESS EACH ADMIN SEPARATELY for admin_id, admin_orders in orders_by_admin.items(): admin_orders = sorted(admin_orders, key=lambda x: x.created_at) used = set() for i in range(len(admin_orders)): if admin_orders[i].id in used: continue base = admin_orders[i] group = [base] used.add(base.id) for j in range(i + 1, len(admin_orders)): if admin_orders[j].id in used: continue dist = haversine( base.user_lat, base.user_lng, admin_orders[j].user_lat, admin_orders[j].user_lng ) if dist <= 0.7: group.append(admin_orders[j]) used.add(admin_orders[j].id) if len(group) >= 4: break if len(group) < 2: continue # โœ… CREATE BATCH WITH ADMIN ID batch = Batch( status="CREATED", zonal_admin_id=admin_id # ๐Ÿ”ฅ CRITICAL ) db.add(batch) db.commit() db.refresh(batch) for o in group: o.batch_id = batch.id db.commit() print(f"โœ… Batch {batch.id} created for admin {admin_id}") # ========================= # CREATE SUPPORT TICKET # ========================= def create_support_ticket(db, data, backend_ip): user = db.query(User).filter( User.id == data.user_id ).first() if not user: return {"error": "User not found"} ticket = SupportTicket( user_id=user.id, user_name=user.name, mobile_number=user.phone, message=data.message, frontend_ip=data.frontend_ip, backend_ip=backend_ip ) db.add(ticket) db.commit() db.refresh(ticket) return { "message": "Support ticket submitted", "ticket_id": ticket.id } def get_support_tickets(db): tickets = db.query(SupportTicket).order_by( SupportTicket.created_at.desc() ).all() result = [] for t in tickets: result.append({ "id": t.id, "user_id": t.user_id, "user_name": t.user_name, "mobile_number": t.mobile_number, "message": t.message, "status": t.status, "created_at": t.created_at }) return result