2846 lines
72 KiB
Python
2846 lines
72 KiB
Python
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) |