269 lines
9.7 KiB
Python
269 lines
9.7 KiB
Python
# /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.social import ServiceReview
|
||
from app.models.service import ServiceProfile
|
||
from app.models.identity import User
|
||
from app.models.audit 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 |