Cod_prestatie necunoscut in nomenclator nu se mai trimite raw la RAR (HTTP 500 ORA-12899 + record partial FINALIZATA pe care reconcilierea il marca fals sent): e promovat la cod_op_service si tratat ca operatie de mapat. Optiune top-level boolean on_unmapped_error pe POST /v1/prezentari + /valideaza: - false (default) -> submission needs_mapping (intra in editor) - true -> respinge fara enqueue (status error, submission_id=null, erori) - None -> default per-cont accounts.on_unmapped_error_default (implicit 0) Inlocuieste enum-ul anterior on_unmapped (needs_mapping/error) cu un boolean mai simplu; coloana de cont migrata aditiv la INTEGER on_unmapped_error_default. Izolare teste de .env-ul de dezvoltare: tests/conftest.py fixeaza default sigur pe AUTOPASS_REQUIRE_API_KEY / AUTOPASS_WORKER_USE_TEST_CREDS (precedenta peste .env in pydantic-settings) + fixturile env din test_creds_delivery/test_t1 pineaza explicit aceste flag-uri, ca fallback-ul creds pe cont sa fie atins. Teste: 752 passed (fara flag pe CLI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.2 KiB
Python
139 lines
5.2 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
|
|
# 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]
|