Files
drivers_bot/app/api/gamification.py
2026-05-12 20:06:25 +09:00

185 lines
6.7 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,
)
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 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)
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 session.commit()
await session.refresh(score)
return score