Complete CarPass product flows
This commit is contained in:
@@ -8,7 +8,7 @@ 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.car import Car, OdometerHistory
|
||||
from app.models.expense import ExpenseEntry, FuelEntry, ServiceEntry
|
||||
from app.models.user import User
|
||||
from app.schemas.expense import (
|
||||
@@ -18,6 +18,7 @@ from app.schemas.expense import (
|
||||
FuelEntryCreate,
|
||||
FuelEntryRead,
|
||||
FuelEntryUpdate,
|
||||
OdometerHistoryRead,
|
||||
OdometerPrediction,
|
||||
OwnershipStats,
|
||||
ServiceEntryCreate,
|
||||
@@ -25,6 +26,11 @@ from app.schemas.expense import (
|
||||
ServiceEntryUpdate,
|
||||
)
|
||||
from app.services.calculations import dataframe_from_query, get_ownership_stats, predict_odometer
|
||||
from app.services.odometer import (
|
||||
apply_odometer_from_record,
|
||||
recalculate_current_odometer,
|
||||
validate_odometer_change,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["entries"])
|
||||
|
||||
@@ -47,40 +53,6 @@ async def ensure_entry_owner(
|
||||
return entry
|
||||
|
||||
|
||||
async def refresh_current_odometer(session: AsyncSession, car_id: int) -> None:
|
||||
car = await session.get(Car, car_id)
|
||||
if car is None:
|
||||
return
|
||||
fuel_result = await session.execute(
|
||||
select(FuelEntry.odometer)
|
||||
.where(FuelEntry.car_id == car_id)
|
||||
.order_by(FuelEntry.odometer.desc())
|
||||
.limit(1)
|
||||
)
|
||||
service_result = await session.execute(
|
||||
select(ServiceEntry.odometer)
|
||||
.where(ServiceEntry.car_id == car_id, ServiceEntry.odometer.is_not(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(),
|
||||
expense_result.scalar_one_or_none(),
|
||||
)
|
||||
if value is not None
|
||||
]
|
||||
car.current_odometer = max(values) if values else None
|
||||
|
||||
|
||||
@router.post("/fuel", response_model=FuelEntryRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_fuel_entry(
|
||||
payload: FuelEntryCreate,
|
||||
@@ -88,10 +60,24 @@ async def create_fuel_entry(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> FuelEntry:
|
||||
car = await ensure_owned_car(session, payload.car_id, current_user)
|
||||
entry = FuelEntry(**payload.model_dump())
|
||||
validate_odometer_change(
|
||||
car,
|
||||
payload.odometer,
|
||||
source_record_type="fuel",
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
entry = FuelEntry(**payload.model_dump(exclude={"confirm_lower_odometer"}))
|
||||
session.add(entry)
|
||||
if car.current_odometer is None or payload.odometer > car.current_odometer:
|
||||
car.current_odometer = payload.odometer
|
||||
await session.flush()
|
||||
await apply_odometer_from_record(
|
||||
session,
|
||||
car,
|
||||
new_odometer=payload.odometer,
|
||||
source_record_type="fuel",
|
||||
source_record_id=entry.id,
|
||||
changed_by=current_user.id,
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(entry)
|
||||
return entry
|
||||
@@ -129,13 +115,21 @@ async def update_fuel_entry(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> FuelEntry:
|
||||
entry = await ensure_entry_owner(session, await session.get(FuelEntry, entry_id), current_user)
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
car = await session.get(Car, entry.car_id)
|
||||
if car is not None and payload.odometer is not None:
|
||||
validate_odometer_change(
|
||||
car,
|
||||
payload.odometer,
|
||||
source_record_type="fuel",
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
for field, value in payload.model_dump(exclude_unset=True, exclude={"confirm_lower_odometer"}).items():
|
||||
setattr(entry, field, value)
|
||||
if payload.total_cost is None and (
|
||||
payload.liters is not None or payload.price_per_liter is not None
|
||||
):
|
||||
entry.total_cost = entry.liters * entry.price_per_liter
|
||||
await refresh_current_odometer(session, entry.car_id)
|
||||
await recalculate_current_odometer(session, entry.car_id, changed_by=current_user.id, source_record_type="fuel_update")
|
||||
await session.commit()
|
||||
await session.refresh(entry)
|
||||
return entry
|
||||
@@ -151,7 +145,7 @@ async def delete_fuel_entry(
|
||||
car_id = entry.car_id
|
||||
await session.delete(entry)
|
||||
await session.flush()
|
||||
await refresh_current_odometer(session, car_id)
|
||||
await recalculate_current_odometer(session, car_id, changed_by=current_user.id, source_record_type="fuel_delete")
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -162,10 +156,24 @@ async def create_service_entry(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceEntry:
|
||||
car = await ensure_owned_car(session, payload.car_id, current_user)
|
||||
entry = ServiceEntry(**payload.model_dump())
|
||||
validate_odometer_change(
|
||||
car,
|
||||
payload.odometer,
|
||||
source_record_type="service",
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
entry = ServiceEntry(**payload.model_dump(exclude={"confirm_lower_odometer"}))
|
||||
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.flush()
|
||||
await apply_odometer_from_record(
|
||||
session,
|
||||
car,
|
||||
new_odometer=payload.odometer,
|
||||
source_record_type="service",
|
||||
source_record_id=entry.id,
|
||||
changed_by=current_user.id,
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(entry)
|
||||
return entry
|
||||
@@ -203,9 +211,17 @@ async def update_service_entry(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceEntry:
|
||||
entry = await ensure_entry_owner(session, await session.get(ServiceEntry, entry_id), current_user)
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
car = await session.get(Car, entry.car_id)
|
||||
if car is not None and payload.odometer is not None:
|
||||
validate_odometer_change(
|
||||
car,
|
||||
payload.odometer,
|
||||
source_record_type="service",
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
for field, value in payload.model_dump(exclude_unset=True, exclude={"confirm_lower_odometer"}).items():
|
||||
setattr(entry, field, value)
|
||||
await refresh_current_odometer(session, entry.car_id)
|
||||
await recalculate_current_odometer(session, entry.car_id, changed_by=current_user.id, source_record_type="service_update")
|
||||
await session.commit()
|
||||
await session.refresh(entry)
|
||||
return entry
|
||||
@@ -221,7 +237,7 @@ async def delete_service_entry(
|
||||
car_id = entry.car_id
|
||||
await session.delete(entry)
|
||||
await session.flush()
|
||||
await refresh_current_odometer(session, car_id)
|
||||
await recalculate_current_odometer(session, car_id, changed_by=current_user.id, source_record_type="service_delete")
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -232,10 +248,24 @@ async def create_expense_entry(
|
||||
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())
|
||||
validate_odometer_change(
|
||||
car,
|
||||
payload.odometer,
|
||||
source_record_type="expense",
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
entry = ExpenseEntry(**payload.model_dump(exclude={"confirm_lower_odometer"}))
|
||||
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.flush()
|
||||
await apply_odometer_from_record(
|
||||
session,
|
||||
car,
|
||||
new_odometer=payload.odometer,
|
||||
source_record_type="expense",
|
||||
source_record_id=entry.id,
|
||||
changed_by=current_user.id,
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(entry)
|
||||
return entry
|
||||
@@ -276,9 +306,17 @@ async def update_expense_entry(
|
||||
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():
|
||||
car = await session.get(Car, entry.car_id)
|
||||
if car is not None and payload.odometer is not None:
|
||||
validate_odometer_change(
|
||||
car,
|
||||
payload.odometer,
|
||||
source_record_type="expense",
|
||||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||||
)
|
||||
for field, value in payload.model_dump(exclude_unset=True, exclude={"confirm_lower_odometer"}).items():
|
||||
setattr(entry, field, value)
|
||||
await refresh_current_odometer(session, entry.car_id)
|
||||
await recalculate_current_odometer(session, entry.car_id, changed_by=current_user.id, source_record_type="expense_update")
|
||||
await session.commit()
|
||||
await session.refresh(entry)
|
||||
return entry
|
||||
@@ -294,10 +332,30 @@ async def delete_expense_entry(
|
||||
car_id = entry.car_id
|
||||
await session.delete(entry)
|
||||
await session.flush()
|
||||
await refresh_current_odometer(session, car_id)
|
||||
await recalculate_current_odometer(session, car_id, changed_by=current_user.id, source_record_type="expense_delete")
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.get("/cars/{car_id}/odometer-history", response_model=list[OdometerHistoryRead])
|
||||
async def odometer_history(
|
||||
car_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> list[OdometerHistory]:
|
||||
await ensure_owned_car(session, car_id, current_user)
|
||||
limit = min(max(limit, 1), 200)
|
||||
result = await session.execute(
|
||||
select(OdometerHistory)
|
||||
.where(OdometerHistory.car_id == car_id)
|
||||
.order_by(OdometerHistory.changed_at.desc(), OdometerHistory.id.desc())
|
||||
.limit(limit)
|
||||
.offset(max(offset, 0))
|
||||
)
|
||||
return list(result.scalars())
|
||||
|
||||
|
||||
@router.get("/cars/{car_id}/stats", response_model=OwnershipStats)
|
||||
async def car_stats(
|
||||
car_id: int,
|
||||
|
||||
Reference in New Issue
Block a user