"""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 # Populat cand status='error' din cauza on_unmapped='error': erori 3 niveluri # (COD_NEMAPAT) pentru fiecare cod necunoscut/nemapat. Gol altfel. erori: list[dict] = [] 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]