Flusso valutazione
This commit is contained in:
@@ -0,0 +1,607 @@
|
|||||||
|
import math
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
|
|
||||||
|
from app.core.deps import get_db, get_current_user
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.models.valuation import Valuation, ValuationComment, ValuationHistory, ValuationPhoto, ValuationStatus, ValuationPriority
|
||||||
|
from app.models.vehicle import VehicleRegistry, VehicleVersion, PlateVersionCandidate, ApiUsageLog
|
||||||
|
from app.models.motornet_valuation import MotornetValuation
|
||||||
|
from app.schemas.valuation import (
|
||||||
|
CommentCreate,
|
||||||
|
CommentResponse,
|
||||||
|
HistoryResponse,
|
||||||
|
MotornetValuationResponse,
|
||||||
|
PaginatedValuations,
|
||||||
|
PhotoResponse,
|
||||||
|
ValuationCreate,
|
||||||
|
ValuationListItem,
|
||||||
|
ValuationResponse,
|
||||||
|
VehicleInfo,
|
||||||
|
)
|
||||||
|
from app.services import storage
|
||||||
|
from app.integrations.motornet import valuate_vehicle, MotornetUnavailableError
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
PRIORITY_RANK = {
|
||||||
|
ValuationPriority.contratto: 1,
|
||||||
|
ValuationPriority.preventivo: 2,
|
||||||
|
ValuationPriority.valutazione: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
SELLER_ROLES = {UserRole.venditore}
|
||||||
|
EVALUATOR_ROLES = {UserRole.valutatore, UserRole.admin}
|
||||||
|
ALL_ALLOWED = SELLER_ROLES | EVALUATOR_ROLES | {UserRole.backoffice}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_access(valuation: Valuation, user: User) -> None:
|
||||||
|
if user.role in EVALUATOR_ROLES:
|
||||||
|
return
|
||||||
|
if user.role in {UserRole.backoffice}:
|
||||||
|
return
|
||||||
|
if valuation.user_id == user.id:
|
||||||
|
return
|
||||||
|
if user.group_id and valuation.group_id == user.group_id:
|
||||||
|
return
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Accesso non consentito")
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_vehicle_info(plate: str, motornet_code: str | None, db: AsyncSession) -> VehicleInfo | None:
|
||||||
|
result = await db.execute(select(VehicleRegistry).where(VehicleRegistry.plate == plate))
|
||||||
|
reg = result.scalar_one_or_none()
|
||||||
|
if not reg:
|
||||||
|
return VehicleInfo(
|
||||||
|
plate=plate,
|
||||||
|
vin=None,
|
||||||
|
vehicle_type=None,
|
||||||
|
registration_date=None,
|
||||||
|
version_label=None,
|
||||||
|
brand_name=None,
|
||||||
|
model_description=None,
|
||||||
|
gamma_description=None,
|
||||||
|
list_price=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
version_label = None
|
||||||
|
brand_name = None
|
||||||
|
model_description = None
|
||||||
|
gamma_description = None
|
||||||
|
list_price = None
|
||||||
|
|
||||||
|
code = motornet_code or reg.selected_motornet_code
|
||||||
|
if code:
|
||||||
|
vr = await db.execute(select(VehicleVersion).where(VehicleVersion.motornet_code == code))
|
||||||
|
ver = vr.scalar_one_or_none()
|
||||||
|
if ver:
|
||||||
|
version_label = ver.version_label
|
||||||
|
brand_name = ver.brand_name
|
||||||
|
model_description = ver.model_description
|
||||||
|
gamma_description = ver.gamma_description
|
||||||
|
list_price = ver.list_price
|
||||||
|
|
||||||
|
return VehicleInfo(
|
||||||
|
plate=reg.plate,
|
||||||
|
vin=reg.vin,
|
||||||
|
vehicle_type=reg.vehicle_type,
|
||||||
|
registration_date=reg.registration_date,
|
||||||
|
version_label=version_label,
|
||||||
|
brand_name=brand_name,
|
||||||
|
model_description=model_description,
|
||||||
|
gamma_description=gamma_description,
|
||||||
|
list_price=list_price,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_list_item(valuation: Valuation, db: AsyncSession) -> ValuationListItem:
|
||||||
|
brand_name = None
|
||||||
|
model_description = None
|
||||||
|
version_label = None
|
||||||
|
|
||||||
|
code = valuation.motornet_code
|
||||||
|
vr = await db.execute(select(VehicleRegistry).where(VehicleRegistry.plate == valuation.plate))
|
||||||
|
reg = vr.scalar_one_or_none()
|
||||||
|
if not code and reg:
|
||||||
|
code = reg.selected_motornet_code
|
||||||
|
|
||||||
|
if code:
|
||||||
|
vres = await db.execute(select(VehicleVersion).where(VehicleVersion.motornet_code == code))
|
||||||
|
ver = vres.scalar_one_or_none()
|
||||||
|
if ver:
|
||||||
|
brand_name = ver.brand_name
|
||||||
|
model_description = ver.model_description
|
||||||
|
version_label = ver.version_label
|
||||||
|
|
||||||
|
return ValuationListItem(
|
||||||
|
id=valuation.id,
|
||||||
|
plate=valuation.plate,
|
||||||
|
status=valuation.status.value,
|
||||||
|
priority=valuation.priority.value,
|
||||||
|
priority_rank=valuation.priority_rank,
|
||||||
|
mileage=valuation.mileage,
|
||||||
|
is_frozen=valuation.is_frozen,
|
||||||
|
created_at=valuation.created_at,
|
||||||
|
updated_at=valuation.updated_at,
|
||||||
|
brand_name=brand_name,
|
||||||
|
model_description=model_description,
|
||||||
|
version_label=version_label,
|
||||||
|
final_value=valuation.final_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _try_motornet_valuation(
|
||||||
|
valuation: Valuation,
|
||||||
|
db: AsyncSession,
|
||||||
|
) -> MotornetValuation | None:
|
||||||
|
motornet_code = valuation.motornet_code
|
||||||
|
if not motornet_code:
|
||||||
|
logger.info("motornet_valuation_skip_no_code", valuation_id=valuation.id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
reg_result = await db.execute(
|
||||||
|
select(VehicleRegistry).where(VehicleRegistry.plate == valuation.plate)
|
||||||
|
)
|
||||||
|
reg = reg_result.scalar_one_or_none()
|
||||||
|
registration_date = reg.registration_date if reg else None
|
||||||
|
|
||||||
|
if not registration_date:
|
||||||
|
logger.info("motornet_valuation_skip_no_date", valuation_id=valuation.id, plate=valuation.plate)
|
||||||
|
return None
|
||||||
|
|
||||||
|
anno = registration_date.year
|
||||||
|
mese = registration_date.month
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await valuate_vehicle(
|
||||||
|
motornet_code=motornet_code,
|
||||||
|
anno=anno,
|
||||||
|
mese=mese,
|
||||||
|
km=valuation.mileage,
|
||||||
|
targa=valuation.plate,
|
||||||
|
)
|
||||||
|
except MotornetUnavailableError as exc:
|
||||||
|
logger.warning("motornet_valuation_failed", valuation_id=valuation.id, error=str(exc))
|
||||||
|
return None
|
||||||
|
|
||||||
|
val_data = data.get("valutazione") or {}
|
||||||
|
valutazioni_disponibili: int | None = data.get("valutazioniDisponibili")
|
||||||
|
|
||||||
|
usage_log = ApiUsageLog(
|
||||||
|
source="motornet_valuation",
|
||||||
|
plate=valuation.plate,
|
||||||
|
hit_cache=False,
|
||||||
|
remaining_queries=valutazioni_disponibili,
|
||||||
|
called_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(usage_log)
|
||||||
|
|
||||||
|
alimentazione_desc: str | None = None
|
||||||
|
alim = val_data.get("alimentazione")
|
||||||
|
if alim and isinstance(alim, dict):
|
||||||
|
alimentazione_desc = alim.get("descrizione")
|
||||||
|
|
||||||
|
brand_name: str | None = None
|
||||||
|
marca = val_data.get("marca")
|
||||||
|
if marca and isinstance(marca, dict):
|
||||||
|
brand_name = marca.get("nome")
|
||||||
|
|
||||||
|
def _dec(v) -> Decimal | None:
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return Decimal(str(v))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mn_val = MotornetValuation(
|
||||||
|
valuation_id=valuation.id,
|
||||||
|
plate=valuation.plate,
|
||||||
|
motornet_code=motornet_code,
|
||||||
|
fetched_at=datetime.now(timezone.utc),
|
||||||
|
registration_year=anno,
|
||||||
|
registration_month=mese,
|
||||||
|
mileage=valuation.mileage,
|
||||||
|
motornet_id=val_data.get("id"),
|
||||||
|
ediz_dati=val_data.get("edizDati"),
|
||||||
|
brand_name=brand_name,
|
||||||
|
model_description=val_data.get("modello"),
|
||||||
|
allestimento=val_data.get("allestimento"),
|
||||||
|
alimentazione=alimentazione_desc,
|
||||||
|
immagine_url=val_data.get("immagine"),
|
||||||
|
xml_url=val_data.get("xml"),
|
||||||
|
percorrenza_media_km=val_data.get("percorrenzaMediaKm"),
|
||||||
|
quotazione_blu=_dec(val_data.get("quotazioneEurotaxBlu")),
|
||||||
|
quotazione_blu_km=_dec(val_data.get("quotazioneEurotaxBluKm")),
|
||||||
|
quotazione_giallo=_dec(val_data.get("quotazioneEurotaxGiallo")),
|
||||||
|
quotazione_giallo_km=_dec(val_data.get("quotazioneEurotaxGialloKm")),
|
||||||
|
quotazione_blu_totale=_dec(val_data.get("quotazioneEurotaxBluTotale")),
|
||||||
|
quotazione_giallo_totale=_dec(val_data.get("quotazioneEurotaxGialloTotale")),
|
||||||
|
variazione_km=_dec(val_data.get("variazioneKm")),
|
||||||
|
prezzo_listino=_dec(val_data.get("prezzoListino")),
|
||||||
|
prezzo_accessori=_dec(val_data.get("prezzoAccessori")),
|
||||||
|
totale_riparazioni_carrozzeria=_dec(val_data.get("totaleRiparazioniCarrozzeria")),
|
||||||
|
totale_riparazioni_meccanica=_dec(val_data.get("totaleRiparazioniMeccanica")),
|
||||||
|
raw_response=data,
|
||||||
|
)
|
||||||
|
db.add(mn_val)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(mn_val)
|
||||||
|
return mn_val
|
||||||
|
|
||||||
|
|
||||||
|
def _build_motornet_response(mn: MotornetValuation) -> MotornetValuationResponse:
|
||||||
|
return MotornetValuationResponse(
|
||||||
|
id=mn.id,
|
||||||
|
valuation_id=mn.valuation_id,
|
||||||
|
plate=mn.plate,
|
||||||
|
motornet_code=mn.motornet_code,
|
||||||
|
fetched_at=mn.fetched_at,
|
||||||
|
registration_year=mn.registration_year,
|
||||||
|
registration_month=mn.registration_month,
|
||||||
|
mileage=mn.mileage,
|
||||||
|
motornet_id=mn.motornet_id,
|
||||||
|
ediz_dati=mn.ediz_dati,
|
||||||
|
brand_name=mn.brand_name,
|
||||||
|
model_description=mn.model_description,
|
||||||
|
allestimento=mn.allestimento,
|
||||||
|
alimentazione=mn.alimentazione,
|
||||||
|
immagine_url=mn.immagine_url,
|
||||||
|
xml_url=mn.xml_url,
|
||||||
|
percorrenza_media_km=mn.percorrenza_media_km,
|
||||||
|
quotazione_blu=mn.quotazione_blu,
|
||||||
|
quotazione_blu_km=mn.quotazione_blu_km,
|
||||||
|
quotazione_giallo=mn.quotazione_giallo,
|
||||||
|
quotazione_giallo_km=mn.quotazione_giallo_km,
|
||||||
|
quotazione_blu_totale=mn.quotazione_blu_totale,
|
||||||
|
quotazione_giallo_totale=mn.quotazione_giallo_totale,
|
||||||
|
variazione_km=mn.variazione_km,
|
||||||
|
prezzo_listino=mn.prezzo_listino,
|
||||||
|
prezzo_accessori=mn.prezzo_accessori,
|
||||||
|
totale_riparazioni_carrozzeria=mn.totale_riparazioni_carrozzeria,
|
||||||
|
totale_riparazioni_meccanica=mn.totale_riparazioni_meccanica,
|
||||||
|
raw_response=mn.raw_response,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ValuationResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_valuation(
|
||||||
|
body: ValuationCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
if current_user.role not in {UserRole.venditore, UserRole.valutatore, UserRole.admin}:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permessi insufficienti")
|
||||||
|
|
||||||
|
try:
|
||||||
|
priority_enum = ValuationPriority(body.priority)
|
||||||
|
except ValueError:
|
||||||
|
priority_enum = ValuationPriority.valutazione
|
||||||
|
|
||||||
|
priority_rank = PRIORITY_RANK[priority_enum]
|
||||||
|
|
||||||
|
normalized_plate = body.plate.upper().strip()
|
||||||
|
reg_stmt = pg_insert(VehicleRegistry).values(
|
||||||
|
plate=normalized_plate,
|
||||||
|
source="manual",
|
||||||
|
fetched_at=datetime.now(timezone.utc),
|
||||||
|
).on_conflict_do_nothing(index_elements=["plate"])
|
||||||
|
await db.execute(reg_stmt)
|
||||||
|
|
||||||
|
valuation = Valuation(
|
||||||
|
plate=body.plate.upper().strip(),
|
||||||
|
user_id=current_user.id,
|
||||||
|
group_id=current_user.group_id,
|
||||||
|
motornet_code=body.motornet_code,
|
||||||
|
mileage=body.mileage,
|
||||||
|
retake_regime=body.retake_regime,
|
||||||
|
expected_return_date=body.expected_return_date,
|
||||||
|
interest_fuel=body.interest_fuel,
|
||||||
|
priority=priority_enum,
|
||||||
|
priority_rank=priority_rank,
|
||||||
|
notes=body.notes,
|
||||||
|
status=ValuationStatus.inviata,
|
||||||
|
is_frozen=True,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(valuation)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
history_entry = ValuationHistory(
|
||||||
|
valuation_id=valuation.id,
|
||||||
|
changed_by=current_user.id,
|
||||||
|
field_name="status",
|
||||||
|
old_value=None,
|
||||||
|
new_value=ValuationStatus.inviata.value,
|
||||||
|
changed_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(history_entry)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(valuation)
|
||||||
|
|
||||||
|
mn_record = await _try_motornet_valuation(valuation, db)
|
||||||
|
|
||||||
|
vehicle = await _build_vehicle_info(valuation.plate, valuation.motornet_code, db)
|
||||||
|
|
||||||
|
motornet_valuations = []
|
||||||
|
if mn_record:
|
||||||
|
motornet_valuations = [_build_motornet_response(mn_record)]
|
||||||
|
|
||||||
|
return ValuationResponse(
|
||||||
|
id=valuation.id,
|
||||||
|
plate=valuation.plate,
|
||||||
|
user_id=valuation.user_id,
|
||||||
|
group_id=valuation.group_id,
|
||||||
|
motornet_code=valuation.motornet_code,
|
||||||
|
mileage=valuation.mileage,
|
||||||
|
retake_regime=valuation.retake_regime.value if valuation.retake_regime else None,
|
||||||
|
expected_return_date=valuation.expected_return_date,
|
||||||
|
interest_fuel=valuation.interest_fuel,
|
||||||
|
priority=valuation.priority.value,
|
||||||
|
notes=valuation.notes,
|
||||||
|
status=valuation.status.value,
|
||||||
|
final_value=valuation.final_value,
|
||||||
|
evaluator_notes=valuation.evaluator_notes,
|
||||||
|
is_frozen=valuation.is_frozen,
|
||||||
|
priority_rank=valuation.priority_rank,
|
||||||
|
created_at=valuation.created_at,
|
||||||
|
updated_at=valuation.updated_at,
|
||||||
|
vehicle=vehicle,
|
||||||
|
photos=[],
|
||||||
|
comments=[],
|
||||||
|
history=[
|
||||||
|
HistoryResponse(
|
||||||
|
id=history_entry.id,
|
||||||
|
field_name=history_entry.field_name,
|
||||||
|
old_value=history_entry.old_value,
|
||||||
|
new_value=history_entry.new_value,
|
||||||
|
changed_by=history_entry.changed_by,
|
||||||
|
changed_by_name=current_user.full_name,
|
||||||
|
changed_at=history_entry.changed_at,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
motornet_valuations=motornet_valuations,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=PaginatedValuations)
|
||||||
|
async def list_valuations(
|
||||||
|
plate: str | None = Query(None),
|
||||||
|
status_filter: str | None = Query(None, alias="status"),
|
||||||
|
priority: str | None = Query(None),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
query = select(Valuation)
|
||||||
|
|
||||||
|
if current_user.role == UserRole.venditore:
|
||||||
|
if current_user.group_id:
|
||||||
|
query = query.where(Valuation.group_id == current_user.group_id)
|
||||||
|
else:
|
||||||
|
query = query.where(Valuation.user_id == current_user.id)
|
||||||
|
|
||||||
|
if plate:
|
||||||
|
query = query.where(Valuation.plate.ilike(f"%{plate.upper()}%"))
|
||||||
|
if status_filter:
|
||||||
|
query = query.where(Valuation.status == status_filter)
|
||||||
|
if priority:
|
||||||
|
query = query.where(Valuation.priority == priority)
|
||||||
|
|
||||||
|
count_q = select(func.count()).select_from(query.subquery())
|
||||||
|
total_result = await db.execute(count_q)
|
||||||
|
total = total_result.scalar_one()
|
||||||
|
|
||||||
|
query = query.order_by(Valuation.created_at.desc())
|
||||||
|
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||||
|
|
||||||
|
result = await db.execute(query)
|
||||||
|
valuations = result.scalars().all()
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for v in valuations:
|
||||||
|
items.append(await _build_list_item(v, db))
|
||||||
|
|
||||||
|
return PaginatedValuations(
|
||||||
|
items=items,
|
||||||
|
total=total,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
pages=max(1, math.ceil(total / page_size)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{valuation_id}", response_model=ValuationResponse)
|
||||||
|
async def get_valuation(
|
||||||
|
valuation_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
result = await db.execute(
|
||||||
|
select(Valuation)
|
||||||
|
.options(
|
||||||
|
selectinload(Valuation.photos),
|
||||||
|
selectinload(Valuation.comments),
|
||||||
|
selectinload(Valuation.history),
|
||||||
|
selectinload(Valuation.motornet_valuations),
|
||||||
|
)
|
||||||
|
.where(Valuation.id == valuation_id)
|
||||||
|
)
|
||||||
|
valuation = result.scalar_one_or_none()
|
||||||
|
if not valuation:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Valutazione non trovata")
|
||||||
|
|
||||||
|
_check_access(valuation, current_user)
|
||||||
|
|
||||||
|
vehicle = await _build_vehicle_info(valuation.plate, valuation.motornet_code, db)
|
||||||
|
|
||||||
|
user_ids = {valuation.user_id}
|
||||||
|
user_ids.update(c.user_id for c in valuation.comments)
|
||||||
|
user_ids.update(h.changed_by for h in valuation.history)
|
||||||
|
user_ids.update(p.uploaded_by for p in valuation.photos)
|
||||||
|
|
||||||
|
users_result = await db.execute(select(User).where(User.id.in_(user_ids)))
|
||||||
|
users_map = {u.id: u.full_name for u in users_result.scalars().all()}
|
||||||
|
|
||||||
|
comments = []
|
||||||
|
for c in sorted(valuation.comments, key=lambda x: x.created_at):
|
||||||
|
if not c.visible_to_all and current_user.role not in EVALUATOR_ROLES:
|
||||||
|
continue
|
||||||
|
comments.append(
|
||||||
|
CommentResponse(
|
||||||
|
id=c.id,
|
||||||
|
valuation_id=c.valuation_id,
|
||||||
|
user_id=c.user_id,
|
||||||
|
user_full_name=users_map.get(c.user_id),
|
||||||
|
content=c.content,
|
||||||
|
visible_to_all=c.visible_to_all,
|
||||||
|
created_at=c.created_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
photos = []
|
||||||
|
for p in sorted(valuation.photos, key=lambda x: x.uploaded_at):
|
||||||
|
url = storage.get_presigned_url(p.storage_path)
|
||||||
|
photos.append(
|
||||||
|
PhotoResponse(
|
||||||
|
id=p.id,
|
||||||
|
valuation_id=p.valuation_id,
|
||||||
|
filename=p.filename,
|
||||||
|
storage_path=p.storage_path,
|
||||||
|
uploaded_by=p.uploaded_by,
|
||||||
|
uploaded_at=p.uploaded_at,
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
history = []
|
||||||
|
for h in sorted(valuation.history, key=lambda x: x.changed_at):
|
||||||
|
history.append(
|
||||||
|
HistoryResponse(
|
||||||
|
id=h.id,
|
||||||
|
field_name=h.field_name,
|
||||||
|
old_value=h.old_value,
|
||||||
|
new_value=h.new_value,
|
||||||
|
changed_by=h.changed_by,
|
||||||
|
changed_by_name=users_map.get(h.changed_by),
|
||||||
|
changed_at=h.changed_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
motornet_valuations = sorted(valuation.motornet_valuations, key=lambda x: x.fetched_at)
|
||||||
|
motornet_responses = [_build_motornet_response(mn) for mn in motornet_valuations]
|
||||||
|
|
||||||
|
return ValuationResponse(
|
||||||
|
id=valuation.id,
|
||||||
|
plate=valuation.plate,
|
||||||
|
user_id=valuation.user_id,
|
||||||
|
group_id=valuation.group_id,
|
||||||
|
motornet_code=valuation.motornet_code,
|
||||||
|
mileage=valuation.mileage,
|
||||||
|
retake_regime=valuation.retake_regime.value if valuation.retake_regime else None,
|
||||||
|
expected_return_date=valuation.expected_return_date,
|
||||||
|
interest_fuel=valuation.interest_fuel,
|
||||||
|
priority=valuation.priority.value,
|
||||||
|
notes=valuation.notes,
|
||||||
|
status=valuation.status.value,
|
||||||
|
final_value=valuation.final_value,
|
||||||
|
evaluator_notes=valuation.evaluator_notes,
|
||||||
|
is_frozen=valuation.is_frozen,
|
||||||
|
priority_rank=valuation.priority_rank,
|
||||||
|
created_at=valuation.created_at,
|
||||||
|
updated_at=valuation.updated_at,
|
||||||
|
vehicle=vehicle,
|
||||||
|
photos=photos,
|
||||||
|
comments=comments,
|
||||||
|
history=history,
|
||||||
|
motornet_valuations=motornet_responses,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{valuation_id}/photos", response_model=PhotoResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def upload_photo(
|
||||||
|
valuation_id: int,
|
||||||
|
file: Annotated[UploadFile, File(...)],
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
result = await db.execute(select(Valuation).where(Valuation.id == valuation_id))
|
||||||
|
valuation = result.scalar_one_or_none()
|
||||||
|
if not valuation:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Valutazione non trovata")
|
||||||
|
|
||||||
|
_check_access(valuation, current_user)
|
||||||
|
|
||||||
|
storage_path = await storage.upload_photo(file, valuation_id)
|
||||||
|
filename = file.filename or "foto"
|
||||||
|
|
||||||
|
photo = ValuationPhoto(
|
||||||
|
valuation_id=valuation_id,
|
||||||
|
filename=filename,
|
||||||
|
storage_path=storage_path,
|
||||||
|
uploaded_by=current_user.id,
|
||||||
|
uploaded_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(photo)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(photo)
|
||||||
|
|
||||||
|
url = storage.get_presigned_url(storage_path)
|
||||||
|
return PhotoResponse(
|
||||||
|
id=photo.id,
|
||||||
|
valuation_id=photo.valuation_id,
|
||||||
|
filename=photo.filename,
|
||||||
|
storage_path=photo.storage_path,
|
||||||
|
uploaded_by=photo.uploaded_by,
|
||||||
|
uploaded_at=photo.uploaded_at,
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{valuation_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def add_comment(
|
||||||
|
valuation_id: int,
|
||||||
|
body: CommentCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
result = await db.execute(select(Valuation).where(Valuation.id == valuation_id))
|
||||||
|
valuation = result.scalar_one_or_none()
|
||||||
|
if not valuation:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Valutazione non trovata")
|
||||||
|
|
||||||
|
_check_access(valuation, current_user)
|
||||||
|
|
||||||
|
comment = ValuationComment(
|
||||||
|
valuation_id=valuation_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
content=body.content,
|
||||||
|
visible_to_all=body.visible_to_all,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(comment)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(comment)
|
||||||
|
|
||||||
|
return CommentResponse(
|
||||||
|
id=comment.id,
|
||||||
|
valuation_id=comment.valuation_id,
|
||||||
|
user_id=comment.user_id,
|
||||||
|
user_full_name=current_user.full_name,
|
||||||
|
content=comment.content,
|
||||||
|
visible_to_all=comment.visible_to_all,
|
||||||
|
created_at=comment.created_at,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user