1127 lines
28 KiB
Python
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 |