206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
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
|