Improve CarPass product UX and service flows

This commit is contained in:
VPN SaaS Dev
2026-05-14 19:33:25 +09:00
parent b85db333d8
commit caa5f6d3db
36 changed files with 1836 additions and 366 deletions

View File

@@ -9,9 +9,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_telegram_user
from app.db.session import get_session
from app.models.car import Car
from app.models.expense import FuelEntry, ServiceEntry
from app.models.expense import ExpenseEntry, FuelEntry, ServiceEntry
from app.models.user import User
from app.schemas.expense import (
ExpenseEntryCreate,
ExpenseEntryRead,
ExpenseEntryUpdate,
FuelEntryCreate,
FuelEntryRead,
FuelEntryUpdate,
@@ -36,8 +39,8 @@ async def ensure_owned_car(session: AsyncSession, car_id: int, user: User) -> Ca
async def ensure_entry_owner(
session: AsyncSession, entry: FuelEntry | ServiceEntry | None, user: User
) -> FuelEntry | ServiceEntry:
session: AsyncSession, entry: FuelEntry | ServiceEntry | ExpenseEntry | None, user: User
) -> FuelEntry | ServiceEntry | ExpenseEntry:
if entry is None:
raise HTTPException(status_code=404, detail="Entry not found")
await ensure_owned_car(session, entry.car_id, user)
@@ -60,9 +63,19 @@ async def refresh_current_odometer(session: AsyncSession, car_id: int) -> None:
.order_by(ServiceEntry.odometer.desc())
.limit(1)
)
expense_result = await session.execute(
select(ExpenseEntry.odometer)
.where(ExpenseEntry.car_id == car_id, ExpenseEntry.odometer.is_not(None))
.order_by(ExpenseEntry.odometer.desc())
.limit(1)
)
values = [
value
for value in (fuel_result.scalar_one_or_none(), service_result.scalar_one_or_none())
for value in (
fuel_result.scalar_one_or_none(),
service_result.scalar_one_or_none(),
expense_result.scalar_one_or_none(),
)
if value is not None
]
car.current_odometer = max(values) if values else None
@@ -212,6 +225,79 @@ async def delete_service_entry(
await session.commit()
@router.post("/expenses", response_model=ExpenseEntryRead, status_code=status.HTTP_201_CREATED)
async def create_expense_entry(
payload: ExpenseEntryCreate,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ExpenseEntry:
car = await ensure_owned_car(session, payload.car_id, current_user)
entry = ExpenseEntry(**payload.model_dump())
session.add(entry)
if 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}/expenses", response_model=list[ExpenseEntryRead])
async def list_expense_entries(
car_id: int,
date_from: date | None = None,
date_to: date | None = None,
category: str | None = None,
limit: int = 50,
offset: int = 0,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> list[ExpenseEntry]:
await ensure_owned_car(session, car_id, current_user)
limit = min(max(limit, 1), 200)
offset = max(offset, 0)
stmt = select(ExpenseEntry).where(ExpenseEntry.car_id == car_id)
if date_from:
stmt = stmt.where(ExpenseEntry.entry_date >= date_from)
if date_to:
stmt = stmt.where(ExpenseEntry.entry_date <= date_to)
if category:
stmt = stmt.where(ExpenseEntry.category == category)
result = await session.execute(
stmt.order_by(ExpenseEntry.entry_date.desc(), ExpenseEntry.id.desc()).limit(limit).offset(offset)
)
return list(result.scalars())
@router.patch("/expenses/{entry_id}", response_model=ExpenseEntryRead)
async def update_expense_entry(
entry_id: int,
payload: ExpenseEntryUpdate,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ExpenseEntry:
entry = await ensure_entry_owner(session, await session.get(ExpenseEntry, entry_id), current_user)
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(entry, field, value)
await refresh_current_odometer(session, entry.car_id)
await session.commit()
await session.refresh(entry)
return entry
@router.delete("/expenses/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_expense_entry(
entry_id: int,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> None:
entry = await ensure_entry_owner(session, await session.get(ExpenseEntry, entry_id), current_user)
car_id = entry.car_id
await session.delete(entry)
await session.flush()
await refresh_current_odometer(session, car_id)
await session.commit()
@router.get("/cars/{car_id}/stats", response_model=OwnershipStats)
async def car_stats(
car_id: int,