Files
rar-autopass/app/models.py
Claude Agent ac57b9250a feat(5.7): raspuns API onest la blocaje + mapare inline din detaliu
Raportat din client VFP: POST /v1/prezentari raspundea submission_id+status
fara motiv pe randuri blocate (erori se popula doar pe on_unmapped_error=True),
deci un needs_data/needs_mapping parea succes.

API (aditiv): SubmissionResult += nemapate + motiv. create_prezentari
populeaza erori (validare continut, 3 niveluri) / nemapate (coduri fara
mapare, COD_NEMAPAT) / motiv (rezumat uman) pe TOATE caile non-queued —
enqueue, respins (on_unmapped_error=True) si reactivare dedup peste error,
prin helperele _rezultat_enqueue / _rezultat_respins / _motiv_clasificare.
on_unmapped_error=True pastreaza erori=COD_NEMAPAT (compat clienti vechi).

Web: mapare inline in panoul de detaliu trimitere — ruta
POST /trimitere/{id}/mapeaza (reuse save_mapping + reresolve_account, scoped
sesiune + CSRF, re-rezolva pe batch_id-ul randului), helper
_nemapate_pentru_submission + context in _detaliu_ctx, sectiune in
_trimitere_detaliu.html (selector cod RAR cu sugestie fuzzy preselectata).
Apare doar pe operatii nemapate reale (nu pe auto_send=0).

/code-review high: reparat raspuns neonest la reactivare + dublu
load_nomenclator in _detaliu_ctx.

Teste: pytest -q 765 passed. Backend trimitere (worker/masina stari/
idempotenta) si schema NEATINSE. PRD: docs/prd/prd-5.7-*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:51:16 +00:00

145 lines
5.6 KiB
Python

"""Modele Pydantic pentru suprafata API.
ATENTIE: validarea completa (regex VIN ^[A-HJ-NPR-Z0-9]{17}$, nrInmatriculare,
dataPrestatie ∈ [2024-12-01, azi] TZ Bucuresti, R-ODO/I-ODO -> odometruInitial
obligatoriu, odometruInitial <= odometruFinal, normalizare strip/upper) este
**T3** — aici sunt doar formele de baza. Vezi plan.md sect. 2 + roadmap T3.
"""
from __future__ import annotations
from pydantic import BaseModel, Field, field_validator, model_validator
class RarCredentials(BaseModel):
"""Credentiale RAR per-cerere (vin de la ROAAUTO din Oracle). NU se stocheaza."""
email: str
# repr=False: str(creds) / loguri care fac repr pe model NU expun parola.
password: str = Field(..., repr=False)
class PrestatieItem(BaseModel):
"""O operatie de declarat. Contract hibrid (decis 2026-06-15):
ROAAUTO poate trimite FIE `cod_prestatie` (cod RAR direct, ex. OE-1), FIE
`cod_op_service` (cod intern ROAAUTO) + `denumire` — pe care gateway-ul le
mapeaza in cod RAR prin operations_mapping. Cel putin unul dintre
cod_prestatie / cod_op_service e obligatoriu (shape -> 422 daca lipsesc ambele).
"""
cod_prestatie: str | None = Field(None, description="cod din nomenclator RAR, ex. OE-1")
cod_op_service: str | None = Field(None, description="cod intern operatie ROAAUTO (mapat -> cod RAR)")
denumire: str | None = Field(None, description="denumirea operatiei ROAAUTO (pentru fuzzy lookup la mapare)")
@field_validator("cod_prestatie")
@classmethod
def _norm_cod(cls, v: str | None) -> str | None:
return v.strip().upper() if v else None
@field_validator("cod_op_service", "denumire")
@classmethod
def _norm_op(cls, v: str | None) -> str | None:
return v.strip() if v else None
@model_validator(mode="after")
def _require_one(self) -> "PrestatieItem":
if not self.cod_prestatie and not self.cod_op_service:
raise ValueError("fiecare prestatie are nevoie de cod_prestatie sau cod_op_service")
return self
class PrezentareIn(BaseModel):
"""O prezentare de declarat la RAR.
Pydantic doar NORMALIZEAZA aici (strip/upper pe vin/nrInm). Validarea de
continut (regex VIN, interval data, R-ODO/I-ODO, odometru) e in
app.validation.validate_prezentare si NU resping cererea — marcheaza
`needs_data` (plan.md sect. 3).
"""
vin: str
nr_inmatriculare: str
data_prestatie: str # YYYY-MM-DD
odometru_final: str # string per contract
odometru_initial: str | None = None
prestatii: list[PrestatieItem]
sistem_reparat: str = "null"
obs: str | None = None
b64_image: str | None = None
@field_validator("vin", "nr_inmatriculare")
@classmethod
def _norm_upper(cls, v: str) -> str:
return v.strip().upper()
@field_validator("data_prestatie", "odometru_final")
@classmethod
def _norm_strip(cls, v: str) -> str:
return v.strip()
class PrezentareRequest(BaseModel):
"""Body pentru POST /v1/prezentari — una sau mai multe prezentari + creds RAR.
`rar_credentials` e OPTIONAL: daca lipseste, worker-ul foloseste creds-urile RAR
durabile salvate pe cont (`accounts.rar_creds_enc`, via POST /v1/conturi/rar-creds).
Trimite-le explicit doar cand vrei sa suprascrii creds-urile contului pe acea cerere.
"""
rar_credentials: RarCredentials | None = None
prezentari: list[PrezentareIn] = Field(..., min_length=1)
# Optional: override per-cerere al comportamentului la cod necunoscut/nemapat.
# True -> respinge cererea fara enqueue (status 'error');
# False -> submission 'needs_mapping' (intra in editorul de mapare);
# None -> se foloseste accounts.on_unmapped_error_default (implicit False).
on_unmapped_error: bool | None = None
class SubmissionResult(BaseModel):
# submission_id e None cand cererea a fost RESPINSA fara enqueue (on_unmapped_error=True).
submission_id: int | None = None
status: str
id_prezentare: int | None = None
deduped: bool = False # True daca idempotency a intors un submission existent
# US-012 (decizie /autoplan #19): camp ADITIV. True cand un rand `error` cu aceeasi
# cheie de continut a fost RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit.
# `deduped` pastreaza semantica actuala (clientii vechi care testeaza `deduped` nu se sparg).
reactivated: bool = False
# Raspuns ONEST pentru randuri blocate (PRD 5.7): orice status != 'queued' isi
# expune motivul, ca integratorul sa nu trateze un needs_data/needs_mapping drept succes.
# erori = validare de continut (needs_data), 3 niveluri [{field, cod, problema, cauza, fix, message}].
# Pe ramura on_unmapped_error='error' pastreaza COD_NEMAPAT (compat).
# nemapate = coduri fara mapare RAR (needs_mapping / respins), 3 niveluri + cod_op_service/denumire.
# motiv = rezumat uman pe o linie (None cand status='queued').
erori: list[dict] = []
nemapate: list[dict] = []
motiv: str | None = None
class PrezentariResponse(BaseModel):
results: list[SubmissionResult]
class ValidarePrezentariRequest(BaseModel):
"""Body pentru POST /v1/prezentari/valideaza — dry-run fara enqueue (PRD 5.2)."""
rar_credentials: RarCredentials | None = None
prezentari: list[PrezentareIn] = Field(..., min_length=1)
on_unmapped_error: bool | None = None
class ValidareResult(BaseModel):
"""Verdictul dry-run per prezentare."""
index: int
valid: bool
status_estimat: str # "queued" | "needs_data" | "needs_mapping"
erori: list[dict] = []
nemapate: list[dict] = []
prestatii_rezolvate: list[dict] = []
class ValidareResponse(BaseModel):
results: list[ValidareResult]