Files
drivers_bot/app/api/entries.py
VPN SaaS Dev d93c88c751 first commit
2026-05-12 03:52:13 +09:00

161 lines
5.4 KiB
Python

from io import BytesIO
from datetime import date
import matplotlib.pyplot as plt
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_session
from app.models.car import Car
from app.models.expense import FuelEntry, ServiceEntry
from app.schemas.expense import (
FuelEntryCreate,
FuelEntryRead,
OdometerPrediction,
OwnershipStats,
ServiceEntryCreate,
ServiceEntryRead,
)
from app.services.calculations import dataframe_from_query, get_ownership_stats, predict_odometer
router = APIRouter(tags=["entries"])
async def ensure_car(session: AsyncSession, car_id: int) -> None:
if await session.get(Car, car_id) is None:
raise HTTPException(status_code=404, detail="Car not found")
@router.post("/fuel", response_model=FuelEntryRead, status_code=status.HTTP_201_CREATED)
async def create_fuel_entry(
payload: FuelEntryCreate, session: AsyncSession = Depends(get_session)
) -> FuelEntry:
await ensure_car(session, payload.car_id)
entry = FuelEntry(**payload.model_dump())
session.add(entry)
car = await session.get(Car, payload.car_id)
if car and (car.current_odometer is None or payload.odometer > car.current_odometer):
car.current_odometer = payload.odometer
await session.commit()
await session.refresh(entry)
return entry
@router.get("/cars/{car_id}/fuel", response_model=list[FuelEntryRead])
async def list_fuel_entries(
car_id: int,
date_from: date | None = None,
date_to: date | None = None,
session: AsyncSession = Depends(get_session),
) -> list[FuelEntry]:
stmt = select(FuelEntry).where(FuelEntry.car_id == car_id)
if date_from:
stmt = stmt.where(FuelEntry.entry_date >= date_from)
if date_to:
stmt = stmt.where(FuelEntry.entry_date <= date_to)
result = await session.execute(
stmt.order_by(FuelEntry.entry_date.desc())
)
return list(result.scalars())
@router.post("/service", response_model=ServiceEntryRead, status_code=status.HTTP_201_CREATED)
async def create_service_entry(
payload: ServiceEntryCreate, session: AsyncSession = Depends(get_session)
) -> ServiceEntry:
await ensure_car(session, payload.car_id)
entry = ServiceEntry(**payload.model_dump())
session.add(entry)
car = await session.get(Car, payload.car_id)
if car and payload.odometer and (
car.current_odometer is None or payload.odometer > car.current_odometer
):
car.current_odometer = payload.odometer
await session.commit()
await session.refresh(entry)
return entry
@router.get("/cars/{car_id}/service", response_model=list[ServiceEntryRead])
async def list_service_entries(
car_id: int,
date_from: date | None = None,
date_to: date | None = None,
session: AsyncSession = Depends(get_session),
) -> list[ServiceEntry]:
stmt = select(ServiceEntry).where(ServiceEntry.car_id == car_id)
if date_from:
stmt = stmt.where(ServiceEntry.entry_date >= date_from)
if date_to:
stmt = stmt.where(ServiceEntry.entry_date <= date_to)
result = await session.execute(
stmt.order_by(ServiceEntry.entry_date.desc())
)
return list(result.scalars())
@router.get("/cars/{car_id}/stats", response_model=OwnershipStats)
async def car_stats(
car_id: int,
date_from: date | None = None,
date_to: date | None = None,
session: AsyncSession = Depends(get_session),
) -> OwnershipStats:
await ensure_car(session, car_id)
today = date.today()
period_from = date_from or today.replace(day=1)
period_to = date_to or today
return await get_ownership_stats(session, car_id, period_from, period_to)
@router.get("/cars/{car_id}/analytics", response_model=OdometerPrediction)
async def car_analytics(car_id: int, session: AsyncSession = Depends(get_session)) -> OdometerPrediction:
await ensure_car(session, car_id)
return await predict_odometer(session, car_id)
@router.get("/cars/{car_id}/charts/expenses.png")
async def expenses_chart(car_id: int, session: AsyncSession = Depends(get_session)) -> Response:
await ensure_car(session, car_id)
fuel_df = await dataframe_from_query(
session,
select(FuelEntry.entry_date.label("date"), FuelEntry.total_cost.label("cost")).where(
FuelEntry.car_id == car_id
),
)
service_df = await dataframe_from_query(
session,
select(ServiceEntry.entry_date.label("date"), ServiceEntry.total_cost.label("cost")).where(
ServiceEntry.car_id == car_id
),
)
if fuel_df.empty and service_df.empty:
raise HTTPException(status_code=404, detail="No data for chart")
frames = []
if not fuel_df.empty:
fuel_df["type"] = "fuel"
frames.append(fuel_df)
if not service_df.empty:
service_df["type"] = "service"
frames.append(service_df)
import pandas as pd
df = pd.concat(frames)
df["date"] = pd.to_datetime(df["date"])
pivot = df.pivot_table(index="date", columns="type", values="cost", aggfunc="sum").sort_index()
fig, ax = plt.subplots(figsize=(8, 4.5))
pivot.plot(kind="bar", stacked=True, ax=ax)
ax.set_title("Car expenses")
ax.set_xlabel("Date")
ax.set_ylabel("Cost")
fig.tight_layout()
buffer = BytesIO()
fig.savefig(buffer, format="png")
plt.close(fig)
return Response(buffer.getvalue(), media_type="image/png")