Complete CarPass product flows
This commit is contained in:
157
app/services/odometer.py
Normal file
157
app/services/odometer.py
Normal file
@@ -0,0 +1,157 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user