Files
mealno-backend/crud.py
T
2026-05-25 14:07:00 +00:00

1127 lines
28 KiB
Python

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