fix: critical bug fixes - routes UUID, image paths, favorites loading, bottom nav debounce

This commit is contained in:
2026-02-25 16:29:13 -05:00
parent c449083171
commit fd95df461b
14 changed files with 379 additions and 116 deletions

View File

@ -6,6 +6,8 @@ from app.models.business import Business
from app.models.user import User, UserRole
from app.api.deps import get_current_user
from app.services.image_handler import save_image, delete_image
router = APIRouter(prefix="/api/businesses", tags=["businesses"])
@router.get("", response_model=List[Business])
@ -41,17 +43,7 @@ async def create_business(
image_url = None
if image:
import os
import shutil
from uuid import uuid4
UPLOAD_DIR = "uploads/businesses"
os.makedirs(UPLOAD_DIR, exist_ok=True)
ext = os.path.splitext(image.filename)[1]
filename = f"{uuid4()}{ext}"
path = os.path.join(UPLOAD_DIR, filename)
with open(path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
image_url = f"/uploads/businesses/{filename}"
image_url = save_image(image, "businesses")
db_business = Business(
name=name,
@ -110,17 +102,9 @@ async def update_business(
db_business.longitude = longitude
if image:
import os
import shutil
from uuid import uuid4
UPLOAD_DIR = "uploads/businesses"
os.makedirs(UPLOAD_DIR, exist_ok=True)
ext = os.path.splitext(image.filename)[1]
filename = f"{uuid4()}{ext}"
path = os.path.join(UPLOAD_DIR, filename)
with open(path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
db_business.image_url = f"/uploads/businesses/{filename}"
if db_business.image_url:
delete_image(db_business.image_url)
db_business.image_url = save_image(image, "businesses")
session.add(db_business)
session.commit()
@ -153,6 +137,9 @@ async def delete_business(
if not db_business:
raise HTTPException(status_code=404, detail="Business not found")
if db_business.image_url:
delete_image(db_business.image_url)
session.delete(db_business)
session.commit()
return {"status": "success", "message": "Business deleted"}

View File

@ -1,11 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Form, File, UploadFile
from sqlmodel import Session, select
from sqlalchemy.orm import joinedload
from typing import List
from typing import List, Optional
from uuid import UUID
from datetime import datetime
from app.core.database import get_session
from app.models.coupon import Coupon, CouponCreate, CouponUpdate
from app.models.coupon import Coupon
from app.models.user import User, UserRole
from app.api.deps import get_current_user
from app.services.image_handler import save_image, delete_image
router = APIRouter(prefix="/api/coupons", tags=["coupons"])
@ -27,7 +30,19 @@ async def list_coupons(
async def create_coupon(
*,
session: Session = Depends(get_session),
coupon_in: CouponCreate,
title: str = Form(...),
business_id: Optional[UUID] = Form(None),
description: Optional[str] = Form(None),
business_name: Optional[str] = Form(None),
business_address: Optional[str] = Form(None),
business_phone: Optional[str] = Form(None),
discount_percentage: Optional[int] = Form(None),
discount_amount: Optional[float] = Form(None),
category: Optional[str] = Form(None),
valid_from: Optional[datetime] = Form(None),
valid_until: Optional[datetime] = Form(None),
is_active: bool = Form(True),
image: Optional[UploadFile] = File(None),
current_user: User = Depends(get_current_user)
):
"""Create a new coupon (Promoters and Admins only)."""
@ -37,7 +52,25 @@ async def create_coupon(
detail="Only promoters and admins can create coupons"
)
db_coupon = Coupon.from_orm(coupon_in)
image_url = None
if image:
image_url = save_image(image, "coupons")
db_coupon = Coupon(
title=title,
business_id=business_id,
description=description,
business_name=business_name,
business_address=business_address,
business_phone=business_phone,
discount_percentage=discount_percentage,
discount_amount=discount_amount,
category=category,
valid_from=valid_from,
valid_until=valid_until,
is_active=is_active,
image_url=image_url
)
session.add(db_coupon)
session.commit()
session.refresh(db_coupon)
@ -47,8 +80,13 @@ async def create_coupon(
async def update_coupon(
*,
session: Session = Depends(get_session),
coupon_id: str,
coupon_in: CouponUpdate,
coupon_id: UUID,
title: Optional[str] = Form(None),
description: Optional[str] = Form(None),
discount_percentage: Optional[int] = Form(None),
valid_until: Optional[datetime] = Form(None),
is_active: Optional[bool] = Form(None),
image: Optional[UploadFile] = File(None),
current_user: User = Depends(get_current_user)
):
"""Update a coupon (Promoters and Admins only)."""
@ -62,9 +100,21 @@ async def update_coupon(
if not db_coupon:
raise HTTPException(status_code=404, detail="Coupon not found")
coupon_data = coupon_in.dict(exclude_unset=True)
for key, value in coupon_data.items():
setattr(db_coupon, key, value)
if title is not None:
db_coupon.title = title
if description is not None:
db_coupon.description = description
if discount_percentage is not None:
db_coupon.discount_percentage = discount_percentage
if valid_until is not None:
db_coupon.valid_until = valid_until
if is_active is not None:
db_coupon.is_active = is_active
if image:
if db_coupon.image_url:
delete_image(db_coupon.image_url)
db_coupon.image_url = save_image(image, "coupons")
session.add(db_coupon)
session.commit()
@ -75,7 +125,7 @@ async def update_coupon(
async def delete_coupon(
*,
session: Session = Depends(get_session),
coupon_id: str,
coupon_id: UUID,
current_user: User = Depends(get_current_user)
):
"""Delete a coupon (Promoters and Admins only)."""
@ -89,6 +139,9 @@ async def delete_coupon(
if not db_coupon:
raise HTTPException(status_code=404, detail="Coupon not found")
if db_coupon.image_url:
delete_image(db_coupon.image_url)
session.delete(db_coupon)
session.commit()
return {"status": "success", "message": "Coupon deleted"}

View File

@ -46,16 +46,24 @@ async def get_route_stops(route_id: str, session: Session = Depends(get_session)
"""Get all stops for a route."""
from app.models.route_stop import RouteStop
from app.models.bus_stop import BusStop
from uuid import UUID
try:
route_id_uuid = UUID(route_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid route ID format")
statement = select(RouteStop, BusStop).join(
BusStop, RouteStop.stop_id == BusStop.id
).where(RouteStop.route_id == route_id).order_by(RouteStop.stop_order)
).where(RouteStop.route_id == route_id_uuid).order_by(RouteStop.stop_order)
results = session.exec(statement).all()
# Merge RouteStop data into BusStop response
stops = []
for route_stop, bus_stop in results:
stop_data = bus_stop.model_dump()
# Convert UUIDs to strings for JSON compatibility
stop_data['id'] = str(stop_data['id'])
stop_data['stop_order'] = route_stop.stop_order
stop_data['travel_time_minutes'] = route_stop.travel_time_minutes
stop_data['stop_delay_minutes'] = route_stop.stop_delay_minutes
@ -124,21 +132,28 @@ async def add_stop_to_route(
_: bool = Depends(get_current_admin)
):
"""Add a stop to a route with cascading order adjustment."""
from uuid import UUID
try:
route_id_uuid = UUID(route_id)
stop_id_uuid = UUID(stop_data.stop_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid UUID format")
# 1. Check if route exists
route = session.get(Route, route_id)
route = session.get(Route, route_id_uuid)
if not route:
raise HTTPException(status_code=404, detail="Route not found")
# 2. Determine stop order
if stop_data.stop_order is None:
# Append to end
max_order = session.exec(select(func.max(RouteStop.stop_order)).where(RouteStop.route_id == route_id)).one()
max_order = session.exec(select(func.max(RouteStop.stop_order)).where(RouteStop.route_id == route_id_uuid)).one()
# handle case where max_order is None (no stops)
stop_data.stop_order = (max_order or 0) + 1
else:
# Shift existing stops equal to or greater than new order
existing_stops = session.exec(
select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_order >= stop_data.stop_order)
select(RouteStop).where(RouteStop.route_id == route_id_uuid, RouteStop.stop_order >= stop_data.stop_order)
).all()
for stop in existing_stops:
stop.stop_order += 1
@ -146,8 +161,8 @@ async def add_stop_to_route(
# 3. Create new RouteStop
new_stop = RouteStop(
route_id=route_id,
stop_id=stop_data.stop_id,
route_id=route_id_uuid,
stop_id=stop_id_uuid,
stop_order=stop_data.stop_order,
travel_time_minutes=stop_data.travel_time_minutes,
stop_delay_minutes=stop_data.stop_delay_minutes or 0,
@ -168,12 +183,14 @@ async def update_route_stop_order(
_: bool = Depends(get_current_admin)
):
"""Update a route stop, potentially reordering others."""
# This assumes we find the connection by route_id and stop_id.
# NOTE: If a stop is on a route multiple times, this logic needs ID, but for now assuming unique stop per route.
# Actually RouteStop has its own ID but we are using stop_id in path. Let's find the RouteStop entry.
from uuid import UUID
try:
route_id_uuid = UUID(route_id)
stop_id_uuid = UUID(stop_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid UUID format")
route_stop = session.exec(
select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_id == stop_id)
select(RouteStop).where(RouteStop.route_id == route_id_uuid, RouteStop.stop_id == stop_id_uuid)
).first()
if not route_stop:
@ -199,7 +216,7 @@ async def update_route_stop_order(
# Moving down: shift stops between old+1 and new DOWN (-1)
stops_to_shift = session.exec(
select(RouteStop).where(
RouteStop.route_id == route_id,
RouteStop.route_id == route_id_uuid,
RouteStop.stop_order > old_order,
RouteStop.stop_order <= new_order
)
@ -211,7 +228,7 @@ async def update_route_stop_order(
# Moving up: shift stops between new and old-1 UP (+1)
stops_to_shift = session.exec(
select(RouteStop).where(
RouteStop.route_id == route_id,
RouteStop.route_id == route_id_uuid,
RouteStop.stop_order >= new_order,
RouteStop.stop_order < old_order
)
@ -227,3 +244,45 @@ async def update_route_stop_order(
session.refresh(route_stop)
return route_stop
@router.delete("/{route_id}/stops/{stop_id}")
async def remove_stop_from_route(
route_id: str,
stop_id: str,
session: Session = Depends(get_session),
_: bool = Depends(get_current_admin)
):
"""Remove a stop from a route."""
from uuid import UUID
try:
route_id_uuid = UUID(route_id)
stop_id_uuid = UUID(stop_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid UUID format")
route_stop = session.exec(
select(RouteStop).where(
RouteStop.route_id == route_id_uuid,
RouteStop.stop_id == stop_id_uuid
)
).first()
if not route_stop:
raise HTTPException(status_code=404, detail="Stop not found on this route")
# Re-order remaining stops
removed_order = route_stop.stop_order
remaining_stops = session.exec(
select(RouteStop).where(
RouteStop.route_id == route_id_uuid,
RouteStop.stop_order > removed_order
)
).all()
for s in remaining_stops:
s.stop_order -= 1
session.add(s)
session.delete(route_stop)
session.commit()
return {"ok": True}

View File

@ -8,9 +8,9 @@ from app.core.database import get_session
from app.models.shuttle import Shuttle
from app.api.deps import get_current_admin
router = APIRouter(prefix="/api/shuttles", tags=["shuttles"])
from app.services.image_handler import save_image, delete_image
UPLOAD_DIR = "uploads"
router = APIRouter(prefix="/api/shuttles", tags=["shuttles"])
@router.get("", response_model=List[Shuttle])
async def get_shuttles(
@ -63,12 +63,7 @@ async def create_shuttle(
"""Create a new shuttle trip (Admin only)."""
image_url = None
if image:
ext = os.path.splitext(image.filename)[1]
filename = f"{uuid4()}{ext}"
path = os.path.join(UPLOAD_DIR, "vehicles", filename)
with open(path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
image_url = f"/uploads/vehicles/{filename}"
image_url = save_image(image, "vehicles")
shuttle = Shuttle(
route_name=route_name,
@ -137,12 +132,10 @@ async def update_shuttle(
db_shuttle.is_active = is_active
if image:
ext = os.path.splitext(image.filename)[1]
filename = f"{uuid4()}{ext}"
path = os.path.join(UPLOAD_DIR, "vehicles", filename)
with open(path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
db_shuttle.image_url = f"/uploads/vehicles/{filename}"
# Delete old image if exists
if db_shuttle.image_url:
delete_image(db_shuttle.image_url)
db_shuttle.image_url = save_image(image, "vehicles")
session.add(db_shuttle)
session.commit()
@ -159,6 +152,11 @@ async def delete_shuttle(
db_shuttle = session.get(Shuttle, shuttle_id)
if not db_shuttle:
raise HTTPException(status_code=404, detail="Shuttle not found")
# Delete image from storage
if db_shuttle.image_url:
delete_image(db_shuttle.image_url)
session.delete(db_shuttle)
session.commit()
return {"ok": True}

View File

@ -8,11 +8,10 @@ from app.core.database import get_session
from app.models.taxi import Taxi
from app.api.deps import get_current_admin
from app.services.image_handler import save_image, delete_image
router = APIRouter(prefix="/api/taxis", tags=["taxis"])
UPLOAD_DIR = "uploads"
@router.get("", response_model=List[Taxi])
async def get_taxis(
corregimiento: Optional[str] = Query(None),
@ -62,12 +61,7 @@ async def create_taxi(
"""Create a new taxi entry (Admin only)."""
image_url = None
if image:
ext = os.path.splitext(image.filename)[1]
filename = f"{uuid4()}{ext}"
path = os.path.join(UPLOAD_DIR, "profiles", filename)
with open(path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
image_url = f"/uploads/profiles/{filename}"
image_url = save_image(image, "profiles")
taxi = Taxi(
owner_name=owner_name,
@ -121,12 +115,9 @@ async def update_taxi(
# Handle image upload
if image:
ext = os.path.splitext(image.filename)[1]
filename = f"{uuid4()}{ext}"
path = os.path.join(UPLOAD_DIR, "profiles", filename)
with open(path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
db_taxi.image_url = f"/uploads/profiles/{filename}"
if db_taxi.image_url:
delete_image(db_taxi.image_url)
db_taxi.image_url = save_image(image, "profiles")
session.add(db_taxi)
session.commit()
@ -144,6 +135,10 @@ async def delete_taxi(
db_taxi = session.get(Taxi, taxi_id)
if not db_taxi:
raise HTTPException(status_code=404, detail="Taxi not found")
if db_taxi.image_url:
delete_image(db_taxi.image_url)
session.delete(db_taxi)
session.commit()
return {"ok": True}