Add CarPass gamification scoring foundation
This commit is contained in:
184
app/api/gamification.py
Normal file
184
app/api/gamification.py
Normal file
@@ -0,0 +1,184 @@
|
||||
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
|
||||
Reference in New Issue
Block a user