feat(errors): erori pe 3 niveluri (problema+cauza+fix) pe API si UI (PRD 5.4)

Catalog central pur app/errors.py ca sursa unica cod->{problema,fix},
consumat de API+UI+worker. Aditiv (field/message pastrate la octet) +
rar_error stocat superset. Scope: fluxul de declarare; login/signup/CSRF
neatinse. labels.parse_erori degradeaza gratios; UI progresiv AA light+dark.
631 teste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-23 10:28:09 +00:00
parent b48501d8e4
commit 14e1c463f0
25 changed files with 2440 additions and 44 deletions

View File

@@ -17,6 +17,8 @@ import re
from datetime import date
from zoneinfo import ZoneInfo
from app.errors import eroare as _eroare
# VIN: 17 caractere, majuscule, fara O/I/Q (plan §2 + contract).
VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
# Numar inmatriculare: max 10, litere + cifre majuscule.
@@ -64,36 +66,54 @@ def validate_prezentare(content: dict) -> list[dict]:
# --- VIN ---
vin = _norm(content.get("vin"))
if not VIN_RE.match(vin):
errors.append({
"field": "vin",
"message": "VIN trebuie sa aiba exact 17 caractere majuscule, fara spatii/caractere speciale si fara O, I, Q.",
})
errors.append(_eroare(
"VIN_FORMAT",
field="vin",
cauza="VIN trebuie sa aiba exact 17 caractere majuscule, fara spatii/caractere speciale si fara O, I, Q.",
))
# --- nrInmatriculare ---
nrinm = _norm(content.get("nr_inmatriculare"))
if not NRINM_RE.match(nrinm):
errors.append({
"field": "nr_inmatriculare",
"message": "Numarul de inmatriculare trebuie sa aiba max 10 caractere, doar litere si cifre majuscule.",
})
errors.append(_eroare(
"NR_INMATRICULARE_FORMAT",
field="nr_inmatriculare",
cauza="Numarul de inmatriculare trebuie sa aiba max 10 caractere, doar litere si cifre majuscule.",
))
# --- dataPrestatie ∈ [2024-12-01, azi] TZ Bucuresti ---
raw_data = str(content.get("data_prestatie") or "").strip()
try:
d = date.fromisoformat(raw_data)
except ValueError:
errors.append({"field": "data_prestatie", "message": "Format data invalid; foloseste YYYY-MM-DD."})
errors.append(_eroare(
"DATA_FORMAT",
field="data_prestatie",
cauza="Format data invalid; foloseste YYYY-MM-DD.",
))
d = None
if d is not None:
if d < MIN_DATA_PRESTATIE:
errors.append({"field": "data_prestatie", "message": "Data prestatiei nu poate fi anterioara datei de 01.12.2024."})
errors.append(_eroare(
"DATA_PREA_VECHE",
field="data_prestatie",
cauza="Data prestatiei nu poate fi anterioara datei de 01.12.2024.",
))
elif d > today_bucuresti():
errors.append({"field": "data_prestatie", "message": "Data prestatiei nu poate fi in viitor."})
errors.append(_eroare(
"DATA_VIITOR",
field="data_prestatie",
cauza="Data prestatiei nu poate fi in viitor.",
))
# --- odometruFinal (string numeric) ---
odo_final = _parse_int(content.get("odometru_final"))
if odo_final is None:
errors.append({"field": "odometru_final", "message": "odometruFinal trebuie sa fie un numar intreg (ca string)."})
errors.append(_eroare(
"ODOMETRU_FINAL_FORMAT",
field="odometru_final",
cauza="odometruFinal trebuie sa fie un numar intreg (ca string).",
))
# --- odometruInitial: obligatoriu daca prestatii ∋ R-ODO/I-ODO; <= odometruFinal ---
codes = _codes(content.get("prestatii"))
@@ -101,26 +121,43 @@ def validate_prezentare(content: dict) -> list[dict]:
raw_initial = content.get("odometru_initial")
has_initial = str(raw_initial or "").strip() != ""
if needs_initial and not has_initial:
errors.append({
"field": "odometru_initial",
"message": "odometruInitial este obligatoriu cand prestatiile contin R-ODO sau I-ODO.",
})
errors.append(_eroare(
"ODOMETRU_INITIAL_LIPSA",
field="odometru_initial",
cauza="odometruInitial este obligatoriu cand prestatiile contin R-ODO sau I-ODO.",
))
if has_initial:
odo_initial = _parse_int(raw_initial)
if odo_initial is None:
errors.append({"field": "odometru_initial", "message": "odometruInitial trebuie sa fie un numar intreg."})
errors.append(_eroare(
"ODOMETRU_INITIAL_FORMAT",
field="odometru_initial",
cauza="odometruInitial trebuie sa fie un numar intreg.",
))
elif odo_final is not None and odo_initial > odo_final:
errors.append({"field": "odometru_initial", "message": "odometruInitial trebuie sa fie <= odometruFinal."})
errors.append(_eroare(
"ODOMETRU_INITIAL_ORDINE",
field="odometru_initial",
cauza="odometruInitial trebuie sa fie <= odometruFinal.",
))
# --- prestatii nevide ---
if not codes:
errors.append({"field": "prestatii", "message": "Lista de prestatii nu poate fi goala."})
errors.append(_eroare(
"PRESTATII_GOALE",
field="prestatii",
cauza="Lista de prestatii nu poate fi goala.",
))
# --- b64Image: optional, dar daca e prezent trebuie base64 valid ---
b64 = content.get("b64_image")
if b64:
if not _is_valid_base64(str(b64)):
errors.append({"field": "b64_image", "message": "b64Image nu este base64 valid."})
errors.append(_eroare(
"B64_INVALID",
field="b64_image",
cauza="b64Image nu este base64 valid.",
))
return errors