Files
2026-05-25 14:07:00 +00:00

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)