# /opt/docker/dev/service_finder/backend/app/services/marketplace_service.py """ Marketplace Service – Verifikált Szerviz Értékelések (Social 3) logikája. """ import logging import uuid import asyncio from datetime import datetime, timedelta from typing import Optional, Dict, Any, List, Tuple from sqlalchemy import select, and_, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import IntegrityError from app.models import ServiceReview from app.models.marketplace.service import ServiceProfile from app.models.identity import User from app.models import FinancialLedger from app.models.system import SystemParameter from app.schemas.social import ServiceReviewCreate, ServiceReviewResponse from app.services.system_service import get_system_parameter logger = logging.getLogger(__name__) async def create_verified_review( db: AsyncSession, service_id: int, user_id: int, transaction_id: uuid.UUID, review_data: ServiceReviewCreate, ) -> ServiceReviewResponse: """ Verifikált szerviz értékelés létrehozása. Csak igazolt pénzügyi tranzakció után, időablakon belül, egy tranzakcióra egyszer. Args: db: AsyncSession service_id: A szerviz ID (service_profiles.id) user_id: A felhasználó ID (users.id) transaction_id: A pénzügyi tranzakció UUID (financial_ledger.transaction_id) review_data: Értékelési adatok (ratings, comment) Returns: ServiceReviewResponse Raises: ValueError: Ha a validáció sikertelen. IntegrityError: Ha a tranzakció már értékelve van. """ # 1. Ellenőrzés: Létezik‑e a szerviz? service = await db.get(ServiceProfile, service_id) if not service: raise ValueError(f"Service {service_id} not found") # 2. Ellenőrzés: Létezik‑e a felhasználó? user = await db.get(User, user_id) if not user: raise ValueError(f"User {user_id} not found") # 3. Ellenőrzés: A tranzakció létezik és a felhasználóhoz tartozik? stmt = select(FinancialLedger).where( FinancialLedger.transaction_id == transaction_id, FinancialLedger.user_id == user_id ) result = await db.execute(stmt) transaction = result.scalar_one_or_none() if not transaction: raise ValueError(f"Transaction {transaction_id} not found or does not belong to user {user_id}") # 4. Ellenőrzés: A tranzakció időpontja a REVIEW_WINDOW_DAYS‑on belül van? window_days = await get_system_parameter(db, "REVIEW_WINDOW_DAYS", default=30) window_limit = datetime.now() - timedelta(days=window_days) if transaction.created_at < window_limit: raise ValueError(f"Transaction is older than {window_days} days, review window expired") # 5. Ellenőrzés: Már létezik‑e értékelés ehhez a tranzakcióhoz? existing_review = await db.execute( select(ServiceReview).where(ServiceReview.transaction_id == transaction_id) ) if existing_review.scalar_one_or_none(): raise IntegrityError(f"Transaction {transaction_id} already has a review") # 6. Értékelési dimenziók validálása (1‑10) ratings = [ review_data.price_rating, review_data.quality_rating, review_data.time_rating, review_data.communication_rating ] for rating in ratings: if not (1 <= rating <= 10): raise ValueError("All ratings must be between 1 and 10") # 7. ServiceReview létrehozása review = ServiceReview( service_id=service_id, user_id=user_id, transaction_id=transaction_id, price_rating=review_data.price_rating, quality_rating=review_data.quality_rating, time_rating=review_data.time_rating, communication_rating=review_data.communication_rating, comment=review_data.comment, is_verified=True ) db.add(review) await db.commit() await db.refresh(review) # 8. Háttér‑aggregátor indítása (aszinkron) asyncio.create_task(update_service_rating_aggregates(db, service_id)) logger.info(f"Verified review created: id={review.id}, service={service_id}, user={user_id}") return ServiceReviewResponse.from_orm(review) async def update_service_rating_aggregates(db: AsyncSession, service_id: int) -> None: """ Frissíti a szerviz aggregált értékelési adatait (service_profiles táblában). Ez a függvény háttérben futhat (pl. Celery vagy asyncio task). """ # Összes verifikált értékelés lekérdezése a szervizhez stmt = select( func.count(ServiceReview.id).label("count"), func.avg(ServiceReview.price_rating).label("price_avg"), func.avg(ServiceReview.quality_rating).label("quality_avg"), func.avg(ServiceReview.time_rating).label("time_avg"), func.avg(ServiceReview.communication_rating).label("communication_avg"), func.max(ServiceReview.created_at).label("last_review_at") ).where( and_( ServiceReview.service_id == service_id, ServiceReview.is_verified == True ) ) result = await db.execute(stmt) row = result.fetchone() if not row or row.count == 0: # Nincs értékelés, alapértékek price_avg = quality_avg = time_avg = communication_avg = None count = 0 last_review_at = None else: count = row.count price_avg = float(row.price_avg) if row.price_avg else None quality_avg = float(row.quality_avg) if row.quality_avg else None time_avg = float(row.time_avg) if row.time_avg else None communication_avg = float(row.communication_avg) if row.communication_avg else None last_review_at = row.last_review_at # Trust‑score súlyozás: a felhasználók trust‑score‑jának átlaga trust_stmt = select(func.avg(User.trust_score)).join( ServiceReview, ServiceReview.user_id == User.id ).where( and_( ServiceReview.service_id == service_id, ServiceReview.is_verified == True ) ) trust_result = await db.execute(trust_stmt) avg_trust = trust_result.scalar() or 50.0 # alapérték 50 # Trust‑score befolyási tényező trust_factor = await get_system_parameter(db, "TRUST_SCORE_INFLUENCE_FACTOR", default=1.0) trust_weight = 1.0 + (avg_trust / 100.0) * trust_factor # Súlyozott összpontszám számítása weights = await get_system_parameter(db, "REVIEW_RATING_WEIGHTS", default={ "price": 0.25, "quality": 0.35, "time": 0.20, "communication": 0.20 }) weighted_score = 0.0 if price_avg: weighted_score += price_avg * weights.get("price", 0.25) if quality_avg: weighted_score += quality_avg * weights.get("quality", 0.35) if time_avg: weighted_score += time_avg * weights.get("time", 0.20) if communication_avg: weighted_score += communication_avg * weights.get("communication", 0.20) weighted_score *= trust_weight # ServiceProfile frissítése service = await db.get(ServiceProfile, service_id) if service: service.rating_verified_count = count service.rating_price_avg = price_avg service.rating_quality_avg = quality_avg service.rating_time_avg = time_avg service.rating_communication_avg = communication_avg service.rating_overall = weighted_score service.last_review_at = last_review_at await db.commit() logger.debug(f"Updated rating aggregates for service {service_id}: count={count}, overall={weighted_score:.2f}") async def get_service_reviews( db: AsyncSession, service_id: int, skip: int = 0, limit: int = 20, verified_only: bool = True ) -> Tuple[List[ServiceReviewResponse], int]: """ Szerviz értékeléseinek lapozható listázása. Args: db: AsyncSession service_id: A szerviz ID skip: Lapozási offset limit: Maximális darabszám verified_only: Csak verifikált értékelések Returns: (reviews, total_count) """ conditions = [ServiceReview.service_id == service_id] if verified_only: conditions.append(ServiceReview.is_verified == True) # Összes darabszám count_stmt = select(func.count(ServiceReview.id)).where(*conditions) total_result = await db.execute(count_stmt) total = total_result.scalar() # Lapozott lekérdezés stmt = select(ServiceReview).where(*conditions).order_by( ServiceReview.created_at.desc() ).offset(skip).limit(limit) result = await db.execute(stmt) reviews = result.scalars().all() return [ServiceReviewResponse.from_orm(r) for r in reviews], total async def can_user_review_service( db: AsyncSession, user_id: int, service_id: int ) -> Tuple[bool, Optional[str]]: """ Ellenőrzi, hogy a felhasználó értékelheti‑e a szervizt. Returns: (can_review, reason) """ # 1. Van‑e már értékelése a szervizre? existing_stmt = select(ServiceReview).where( ServiceReview.user_id == user_id, ServiceReview.service_id == service_id ) existing = await db.execute(existing_stmt) if existing.scalar_one_or_none(): return False, "User already reviewed this service" # 2. Van‑e a felhasználónak tranzakciója a szervizzel? # Megjegyzés: A tranzakció‑szerviz kapcsolat jelenleg nincs tárolva. # Ehhez a FinancialLedger‑ben kellene egy service_id mező, vagy # egy kapcsolótábla. Most csak annyit ellenőrzünk, hogy van‑e bármilyen # tranzakció a felhasználónak, ami még nem értékelt. # TODO: Később pontosítani a tranzakció‑szerviz kapcsolatot. return True, None