Spaces:
Running
Running
| # ============================================================ | |
| # app/services/review_service.py - Review Business Logic | |
| # ============================================================ | |
| import logging | |
| from datetime import datetime | |
| from typing import Optional | |
| from bson import ObjectId | |
| from fastapi import HTTPException, status | |
| from app.database import get_db | |
| from app.schemas.review import SubmitReviewDto, ReviewResponseDto, ReviewsListResponseDto | |
| logger = logging.getLogger(__name__) | |
| class ReviewService: | |
| """Service for handling review operations""" | |
| async def submit_review( | |
| self, | |
| dto: SubmitReviewDto, | |
| reviewer_id: str, | |
| reviewer_name: str, | |
| ) -> dict: | |
| """ | |
| Submit a new review for a listing or user. | |
| Updates the target's rating and reviews_count. | |
| """ | |
| db = await get_db() | |
| # 1. Validate target exists | |
| target_exists = await self._validate_target_exists( | |
| db, dto.target_type, dto.target_id | |
| ) | |
| if not target_exists: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"{dto.target_type.capitalize()} not found" | |
| ) | |
| # 2. Check for duplicate review | |
| existing_review = await db.reviews.find_one({ | |
| "reviewer_id": reviewer_id, | |
| "target_type": dto.target_type, | |
| "target_id": dto.target_id, | |
| }) | |
| if existing_review: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail=f"You have already reviewed this {dto.target_type}" | |
| ) | |
| # 3. Create review document | |
| review_doc = { | |
| "target_type": dto.target_type, | |
| "target_id": dto.target_id, | |
| "reviewer_id": reviewer_id, | |
| "reviewer_name": reviewer_name, | |
| "rating": dto.rating, | |
| "review_text": dto.review_text, | |
| "created_at": datetime.utcnow(), | |
| } | |
| # 4. Insert review | |
| result = await db.reviews.insert_one(review_doc) | |
| review_id = str(result.inserted_id) | |
| logger.info(f"Review {review_id} created for {dto.target_type} {dto.target_id}") | |
| # 5. Update target's rating and reviews_count | |
| await self._update_target_rating( | |
| db, dto.target_type, dto.target_id, dto.rating | |
| ) | |
| return { | |
| "success": True, | |
| "message": "Review submitted successfully", | |
| "data": { | |
| "review_id": review_id, | |
| "target_type": dto.target_type, | |
| "target_id": dto.target_id, | |
| "rating": dto.rating, | |
| } | |
| } | |
| async def get_reviews_for_target( | |
| self, | |
| target_type: str, | |
| target_id: str, | |
| ) -> ReviewsListResponseDto: | |
| """ | |
| Get all reviews for a specific listing or user. | |
| Returns reviews with total count and average rating. | |
| """ | |
| db = await get_db() | |
| # Fetch all reviews for the target | |
| cursor = db.reviews.find({ | |
| "target_type": target_type, | |
| "target_id": target_id, | |
| }).sort("created_at", -1) | |
| reviews = [] | |
| total_rating = 0 | |
| async for doc in cursor: | |
| reviews.append(ReviewResponseDto( | |
| id=str(doc["_id"]), | |
| reviewer_name=doc["reviewer_name"], | |
| rating=doc["rating"], | |
| review_text=doc["review_text"], | |
| created_at=doc["created_at"], | |
| )) | |
| total_rating += doc["rating"] | |
| total_reviews = len(reviews) | |
| average_rating = round(total_rating / total_reviews, 2) if total_reviews > 0 else 0.0 | |
| return ReviewsListResponseDto( | |
| reviews=reviews, | |
| total_reviews=total_reviews, | |
| average_rating=average_rating, | |
| ) | |
| async def _validate_target_exists( | |
| self, | |
| db, | |
| target_type: str, | |
| target_id: str, | |
| ) -> bool: | |
| """Check if the target (listing or user) exists""" | |
| try: | |
| if not ObjectId.is_valid(target_id): | |
| return False | |
| if target_type == "listing": | |
| doc = await db.listings.find_one({"_id": ObjectId(target_id)}) | |
| else: # user | |
| doc = await db.users.find_one({"_id": ObjectId(target_id)}) | |
| return doc is not None | |
| except Exception as e: | |
| logger.error(f"Error validating target: {e}") | |
| return False | |
| async def _update_target_rating( | |
| self, | |
| db, | |
| target_type: str, | |
| target_id: str, | |
| new_rating: int, | |
| ): | |
| """ | |
| Update the target's aggregate rating and reviews_count. | |
| Formula: new_avg = (old_avg * old_count + new_stars) / (old_count + 1) | |
| """ | |
| try: | |
| collection = db.listings if target_type == "listing" else db.users | |
| # Get current values | |
| doc = await collection.find_one({"_id": ObjectId(target_id)}) | |
| old_rating = doc.get("rating", 0.0) if doc else 0.0 | |
| old_count = doc.get("reviews_count", 0) if doc else 0 | |
| # Calculate new rating | |
| new_count = old_count + 1 | |
| new_avg_rating = round( | |
| (old_rating * old_count + new_rating) / new_count, | |
| 2 | |
| ) | |
| # Update document | |
| await collection.update_one( | |
| {"_id": ObjectId(target_id)}, | |
| { | |
| "$set": { | |
| "rating": new_avg_rating, | |
| "reviews_count": new_count, | |
| } | |
| } | |
| ) | |
| logger.info( | |
| f"Updated {target_type} {target_id} rating: " | |
| f"{old_rating} -> {new_avg_rating} ({new_count} reviews)" | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error updating target rating: {e}") | |
| # Don't raise - review was already saved, rating update is secondary | |
| # Singleton instance | |
| review_service = ReviewService() | |