from __future__ import annotations from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.car import Car, OdometerHistory from app.models.expense import ExpenseEntry, FuelEntry, ServiceEntry def validate_odometer_change( car: Car, new_odometer: int | None, *, source_record_type: str, confirm_lower_odometer: bool = False, ) -> None: if new_odometer is None: return if new_odometer < 0: raise HTTPException(status_code=422, detail="Odometer must be non-negative") current = car.current_odometer if current is not None and new_odometer < current and not confirm_lower_odometer: raise HTTPException( status_code=409, detail={ "code": "odometer_lower_than_current", "message": "Новый пробег меньше текущего. Подтвердите ручную корректировку или проверьте запись.", "current_odometer": current, "new_odometer": new_odometer, "source": source_record_type, }, ) if current is not None and new_odometer > current + 100000 and not confirm_lower_odometer: raise HTTPException( status_code=409, detail={ "code": "odometer_jump_requires_confirmation", "message": "Пробег сильно отличается от текущего. Проверьте число перед сохранением.", "current_odometer": current, "new_odometer": new_odometer, "source": source_record_type, }, ) def add_odometer_history( session: AsyncSession, car: Car, *, new_odometer: int, source_record_type: str, source_record_id: int | None, changed_by: int | None, confirmation_required: bool = False, user_confirmed: bool = True, ) -> None: previous = car.current_odometer session.add( OdometerHistory( car_id=car.id, previous_odometer=previous, new_odometer=new_odometer, source_record_type=source_record_type, source_record_id=source_record_id, changed_by=changed_by, confirmation_required=confirmation_required, user_confirmed=user_confirmed, ) ) car.current_odometer = new_odometer async def apply_odometer_from_record( session: AsyncSession, car: Car, *, new_odometer: int | None, source_record_type: str, source_record_id: int | None, changed_by: int | None, confirm_lower_odometer: bool = False, ) -> None: if new_odometer is None: return validate_odometer_change( car, new_odometer, source_record_type=source_record_type, confirm_lower_odometer=confirm_lower_odometer, ) current = car.current_odometer if current is None or new_odometer > current or confirm_lower_odometer: add_odometer_history( session, car, new_odometer=new_odometer, source_record_type=source_record_type, source_record_id=source_record_id, changed_by=changed_by, confirmation_required=current is not None and new_odometer < current, user_confirmed=True, ) async def recalculate_current_odometer( session: AsyncSession, car_id: int, *, changed_by: int | None = None, source_record_type: str = "recalculate", ) -> 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 ] new_value = max(values) if values else None if new_value != car.current_odometer: if new_value is None: car.current_odometer = None return add_odometer_history( session, car, new_odometer=new_value, source_record_type=source_record_type, source_record_id=None, changed_by=changed_by, confirmation_required=False, user_confirmed=True, )