Complete CarPass product flows

This commit is contained in:
VPN SaaS Dev
2026-05-14 21:19:37 +09:00
parent a83f55c646
commit c0014ab4ea
28 changed files with 3006 additions and 159 deletions

157
app/services/odometer.py Normal file
View 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,
)