from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import desc from pydantic import BaseModel, Field from typing import Optional, List from datetime import datetime from ..database import get_db from ..models import User, VehicleRequest, Review, RequestVehicle from .auth import get_current_user from .notification import notify_system router = APIRouter(prefix="/reviews", tags=["reviews"]) # ===================== # Pydantic Schemas # ===================== class ReviewCreate(BaseModel): request_id: int rating: int = Field(..., ge=1, le=5) title: str = Field(..., max_length=255) content: str class ReviewUpdate(BaseModel): rating: Optional[int] = Field(None, ge=1, le=5) title: Optional[str] = Field(None, max_length=255) content: Optional[str] = None class VehicleInfo(BaseModel): car_id: Optional[int] = None car_name: Optional[str] = None main_image: Optional[str] = None year: Optional[int] = None mileage: Optional[int] = None fuel: Optional[str] = None final_price: Optional[int] = None class ReviewListItem(BaseModel): id: int rating: int title: str content_preview: str author_name: Optional[str] = None main_image: Optional[str] = None view_count: int created_at: datetime class Config: from_attributes = True class ReviewDetail(BaseModel): id: int request_id: int rating: int title: str content: str author_id: int author_name: Optional[str] = None author_email: Optional[str] = None cc_rewarded: bool is_published: bool view_count: int vehicles: List[VehicleInfo] = [] created_at: datetime updated_at: Optional[datetime] = None class ReviewListResponse(BaseModel): reviews: List[ReviewListItem] total: int page: int page_size: int total_pages: int class ReviewableRequestItem(BaseModel): request_id: int maker_name: Optional[str] = None model_name: Optional[str] = None grade_name: Optional[str] = None status: str completed_at: Optional[datetime] = None vehicle_count: int = 0 main_image: Optional[str] = None class AdminReviewListItem(BaseModel): id: int rating: int title: str author_name: Optional[str] = None author_email: Optional[str] = None is_published: bool cc_rewarded: bool view_count: int created_at: datetime # ===================== # Helper Functions # ===================== def _extract_vehicles(request: VehicleRequest) -> List[VehicleInfo]: """Extract vehicle info from a request's recommended vehicles""" vehicles = [] for rv in request.recommended_vehicles: if not rv.is_approved: continue car_data = rv.car_data or {} vehicles.append(VehicleInfo( car_id=rv.car_id or car_data.get("local_car_id"), car_name=car_data.get("car_name"), main_image=car_data.get("main_image"), year=car_data.get("year"), mileage=car_data.get("mileage"), fuel=car_data.get("fuel"), final_price=car_data.get("final_price"), )) return vehicles def _get_main_image(request: VehicleRequest) -> Optional[str]: """Get the first approved vehicle's main image""" for rv in request.recommended_vehicles: if rv.is_approved and rv.car_data: img = rv.car_data.get("main_image") if img: return img return None # ===================== # Public Endpoints # ===================== @router.get("/", response_model=ReviewListResponse) def get_reviews( page: int = Query(1, ge=1), page_size: int = Query(12, ge=1, le=50), rating: Optional[int] = Query(None, ge=1, le=5), db: Session = Depends(get_db), ): """Get published reviews list""" query = db.query(Review).filter(Review.is_published == True) if rating: query = query.filter(Review.rating == rating) total = query.count() total_pages = (total + page_size - 1) // page_size reviews = query.order_by(desc(Review.created_at)) \ .offset((page - 1) * page_size) \ .limit(page_size) \ .all() items = [] for r in reviews: main_image = _get_main_image(r.request) if r.request else None items.append(ReviewListItem( id=r.id, rating=r.rating, title=r.title, content_preview=r.content[:150] + ("..." if len(r.content) > 150 else ""), author_name=r.author.name if r.author else None, main_image=main_image, view_count=r.view_count, created_at=r.created_at, )) return ReviewListResponse( reviews=items, total=total, page=page, page_size=page_size, total_pages=total_pages, ) @router.get("/my/reviewable-requests", response_model=List[ReviewableRequestItem]) def get_reviewable_requests( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Get user's completed requests that don't have a review yet""" requests = db.query(VehicleRequest).filter( VehicleRequest.user_id == current_user.id, VehicleRequest.status == "completed", ).all() # Filter out requests that already have a review existing_review_request_ids = set( r.request_id for r in db.query(Review.request_id).filter( Review.author_id == current_user.id ).all() ) items = [] for req in requests: if req.id in existing_review_request_ids: continue approved_vehicles = [v for v in req.recommended_vehicles if v.is_approved] main_image = None for v in approved_vehicles: if v.car_data and v.car_data.get("main_image"): main_image = v.car_data["main_image"] break items.append(ReviewableRequestItem( request_id=req.id, maker_name=req.maker_name, model_name=req.model_name, grade_name=req.grade_name, status=req.status, completed_at=req.admin_reviewed_at, vehicle_count=len(approved_vehicles), main_image=main_image, )) return items @router.get("/admin/list", response_model=List[AdminReviewListItem]) def admin_get_reviews( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Admin: Get all reviews including unpublished""" if not current_user.is_admin: raise HTTPException(status_code=403, detail="Admin access required") reviews = db.query(Review).order_by(desc(Review.created_at)).all() return [ AdminReviewListItem( id=r.id, rating=r.rating, title=r.title, author_name=r.author.name if r.author else None, author_email=r.author.email if r.author else None, is_published=r.is_published, cc_rewarded=r.cc_rewarded, view_count=r.view_count, created_at=r.created_at, ) for r in reviews ] @router.put("/admin/{review_id}/toggle-publish") def admin_toggle_publish( review_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Admin: Toggle review published status""" if not current_user.is_admin: raise HTTPException(status_code=403, detail="Admin access required") review = db.query(Review).filter(Review.id == review_id).first() if not review: raise HTTPException(status_code=404, detail="Review not found") review.is_published = not review.is_published db.commit() return {"message": f"Review {'published' if review.is_published else 'unpublished'}", "is_published": review.is_published} @router.delete("/admin/{review_id}") def admin_delete_review( review_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Admin: Delete a review""" if not current_user.is_admin: raise HTTPException(status_code=403, detail="Admin access required") review = db.query(Review).filter(Review.id == review_id).first() if not review: raise HTTPException(status_code=404, detail="Review not found") db.delete(review) db.commit() return {"message": "Review deleted"} @router.get("/{review_id}", response_model=ReviewDetail) def get_review( review_id: int, db: Session = Depends(get_db), ): """Get review detail (increments view count)""" review = db.query(Review).filter(Review.id == review_id).first() if not review: raise HTTPException(status_code=404, detail="Review not found") if not review.is_published: raise HTTPException(status_code=404, detail="Review not found") # Increment view count review.view_count = (review.view_count or 0) + 1 db.commit() vehicles = _extract_vehicles(review.request) if review.request else [] return ReviewDetail( id=review.id, request_id=review.request_id, rating=review.rating, title=review.title, content=review.content, author_id=review.author_id, author_name=review.author.name if review.author else None, author_email=review.author.email if review.author else None, cc_rewarded=review.cc_rewarded, is_published=review.is_published, view_count=review.view_count, vehicles=vehicles, created_at=review.created_at, updated_at=review.updated_at, ) # ===================== # User Endpoints (CRUD) # ===================== @router.post("/", response_model=ReviewDetail) def create_review( review_data: ReviewCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Create a review for a completed vehicle request (rewards 1 CC)""" # Verify the request exists and belongs to user request = db.query(VehicleRequest).filter( VehicleRequest.id == review_data.request_id ).first() if not request: raise HTTPException(status_code=404, detail="Vehicle request not found") if request.user_id != current_user.id: raise HTTPException(status_code=403, detail="You can only review your own requests") if request.status != "completed": raise HTTPException(status_code=400, detail="Can only review completed requests") # Check for existing review existing = db.query(Review).filter(Review.request_id == review_data.request_id).first() if existing: raise HTTPException(status_code=400, detail="You have already written a review for this request") # Create review review = Review( request_id=review_data.request_id, author_id=current_user.id, rating=review_data.rating, title=review_data.title, content=review_data.content, cc_rewarded=True, is_published=True, ) db.add(review) # Reward 1 CC current_user.cc_balance = (current_user.cc_balance or 0) + 1.0 db.commit() db.refresh(review) # Send notification about CC reward try: notify_system( db, current_user.id, "Review Reward", "You earned 1 CC for writing a review! Thank you for your feedback.", link="/reviews", ) except Exception: pass # Don't fail review creation if notification fails vehicles = _extract_vehicles(review.request) if review.request else [] return ReviewDetail( id=review.id, request_id=review.request_id, rating=review.rating, title=review.title, content=review.content, author_id=review.author_id, author_name=review.author.name if review.author else None, author_email=review.author.email if review.author else None, cc_rewarded=review.cc_rewarded, is_published=review.is_published, view_count=review.view_count, vehicles=vehicles, created_at=review.created_at, updated_at=review.updated_at, ) @router.put("/{review_id}", response_model=ReviewDetail) def update_review( review_id: int, review_data: ReviewUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Update own review""" review = db.query(Review).filter(Review.id == review_id).first() if not review: raise HTTPException(status_code=404, detail="Review not found") if review.author_id != current_user.id: raise HTTPException(status_code=403, detail="You can only edit your own reviews") if review_data.rating is not None: review.rating = review_data.rating if review_data.title is not None: review.title = review_data.title if review_data.content is not None: review.content = review_data.content review.updated_at = datetime.utcnow() db.commit() db.refresh(review) vehicles = _extract_vehicles(review.request) if review.request else [] return ReviewDetail( id=review.id, request_id=review.request_id, rating=review.rating, title=review.title, content=review.content, author_id=review.author_id, author_name=review.author.name if review.author else None, author_email=review.author.email if review.author else None, cc_rewarded=review.cc_rewarded, is_published=review.is_published, view_count=review.view_count, vehicles=vehicles, created_at=review.created_at, updated_at=review.updated_at, ) @router.delete("/{review_id}") def delete_review( review_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Delete own review""" review = db.query(Review).filter(Review.id == review_id).first() if not review: raise HTTPException(status_code=404, detail="Review not found") if review.author_id != current_user.id: raise HTTPException(status_code=403, detail="You can only delete your own reviews") db.delete(review) db.commit() return {"message": "Review deleted"}