from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import ( ensure_service_employee, ensure_vehicle_owner_or_access, get_current_telegram_user, ) from app.db.session import get_session from app.models.car import ServiceCenter, ServiceVisit from app.models.expense import FuelEntry, ServiceEntry from app.models.gamification import Achievement, EngagementEvent, UserAchievement from app.models.user import User from app.schemas.gamification import ( AchievementRead, ServiceCenterScoreRead, TimelineItem, VehicleScoreRead, ) from app.services.scoring import ( compute_service_center_score, compute_vehicle_score, evaluate_garage_achievements, record_engagement_event, ) router = APIRouter(tags=["gamification"]) @router.get("/me/achievements", response_model=list[AchievementRead]) async def my_achievements( session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> list[AchievementRead]: await evaluate_garage_achievements(session, current_user.id) result = await session.execute( select(UserAchievement, Achievement) .join(Achievement, Achievement.id == UserAchievement.achievement_id) .where(UserAchievement.user_id == current_user.id) .order_by(UserAchievement.unlocked_at.desc(), UserAchievement.id.desc()) ) await session.commit() return [ AchievementRead( code=achievement.code, scope=achievement.scope, title=achievement.title, description=achievement.description, icon=achievement.icon, category=achievement.category, unlocked_at=user_achievement.unlocked_at, vehicle_id=user_achievement.vehicle_id, service_center_id=user_achievement.service_center_id, ) for user_achievement, achievement in result.all() ] @router.get("/my/vehicles/{vehicle_id}/score", response_model=VehicleScoreRead) async def vehicle_score( vehicle_id: int, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ): car = await ensure_vehicle_owner_or_access(session, vehicle_id, current_user) score = await compute_vehicle_score(session, car) await record_engagement_event( session, event_type="vehicle_score_viewed", user_id=current_user.id, vehicle_id=vehicle_id, metadata={"completeness_score": score.completeness_score}, ) await session.commit() await session.refresh(score) return score @router.get("/my/vehicles/{vehicle_id}/timeline", response_model=list[TimelineItem]) async def vehicle_timeline( vehicle_id: int, limit: int = 80, offset: int = 0, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> list[TimelineItem]: if limit < 1: limit = 1 limit = min(limit, 200) offset = max(offset, 0) car = await ensure_vehicle_owner_or_access(session, vehicle_id, current_user) await record_engagement_event( session, event_type="vehicle_timeline_viewed", user_id=current_user.id, vehicle_id=vehicle_id, ) fuel_entries = list((await session.execute(select(FuelEntry).where(FuelEntry.car_id == car.id))).scalars()) service_entries = list((await session.execute(select(ServiceEntry).where(ServiceEntry.car_id == car.id))).scalars()) visits = list((await session.execute(select(ServiceVisit).where(ServiceVisit.vehicle_id == car.id))).scalars()) achievements = list( ( await session.execute( select(UserAchievement, Achievement) .join(Achievement, Achievement.id == UserAchievement.achievement_id) .where(UserAchievement.user_id == current_user.id, UserAchievement.vehicle_id == car.id) ) ).all() ) events = list( ( await session.execute( select(EngagementEvent).where(EngagementEvent.vehicle_id == car.id).order_by(EngagementEvent.created_at.desc()) ) ).scalars() ) items: list[TimelineItem] = [] for entry in fuel_entries: items.append( TimelineItem( id=f"fuel:{entry.id}", date=entry.entry_date.isoformat(), type="fuel", title=f"Fuel refill {float(entry.liters):.1f} L", description=entry.station, amount=entry.total_cost, metadata={"odometer": entry.odometer, "full_tank": entry.is_full_tank}, ) ) for entry in service_entries: items.append( TimelineItem( id=f"service:{entry.id}", date=entry.entry_date.isoformat(), type="service", title=entry.title, status="self_reported", description=entry.vendor, amount=entry.total_cost, metadata={"odometer": entry.odometer, "service_type": entry.service_type.value}, ) ) for visit in visits: items.append( TimelineItem( id=f"visit:{visit.id}", date=visit.visit_date.isoformat(), type="service_visit", title="Service-center visit", status=visit.status, description=visit.notes, amount=visit.total_cost, metadata={"odometer": visit.odometer, "service_center_id": visit.service_center_id}, ) ) for user_achievement, achievement in achievements: items.append( TimelineItem( id=f"achievement:{user_achievement.id}", date=user_achievement.unlocked_at, type="achievement", title=achievement.title, status="unlocked", description=achievement.description, metadata={"code": achievement.code, "category": achievement.category}, ) ) for event in events: items.append( TimelineItem( id=f"event:{event.id}", date=event.created_at, type="event", title=event.event_type.replace("_", " ").title(), metadata=event.metadata_json, ) ) items.sort(key=lambda item: str(item.date), reverse=True) return items[offset : offset + limit] @router.get("/service-centers/{service_center_id}/trust-score", response_model=ServiceCenterScoreRead) async def service_center_trust_score( service_center_id: int, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ): center = await session.get(ServiceCenter, service_center_id) if center is None: raise HTTPException(status_code=404, detail="Service center not found") await ensure_service_employee(session, service_center_id, current_user) score = await compute_service_center_score(session, center) await record_engagement_event( session, event_type="service_trust_score_viewed", user_id=current_user.id, service_center_id=service_center_id, metadata={"trust_score": score.trust_score, "trust_level": score.trust_level}, ) await session.commit() await session.refresh(score) return score