AIDA / app /services /review_service.py
destinyebuka's picture
fyp
bbcb4f3
# ============================================================
# 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()