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

@@ -30,6 +30,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from ... import errors
from ...auth import resolve_account_id
from ...crypto import decrypt_creds, encrypt_creds
from ...db import get_connection
@@ -326,7 +327,15 @@ def apply_row_override(
dec = decrypt_creds(row["oj"])
if dec is None:
# Decrypt fail (cheie schimbata / token corupt): no-op defensiv, NICIODATA scriere goala.
raise HTTPException(status_code=422, detail="override curent ilizibil; editare anulata")
_oi_msg = "override curent ilizibil; editare anulata"
raise HTTPException(
status_code=422,
detail={
"error": "override_ilizibil",
"message": _oi_msg,
**errors.eroare("IMPORT_OVERRIDE_ILIZIBIL", cauza=_oi_msg),
},
)
current = dec
new_override = _merge_override(current, fields)
@@ -406,6 +415,7 @@ async def upload_import(
"error": "multiple_sheets",
"message": str(ms),
"sheets": ms.sheet_names,
**errors.eroare("IMPORT_MULTIPLE_SHEETS", cauza=str(ms)),
},
)
except FileTooLarge as e:
@@ -414,6 +424,7 @@ async def upload_import(
detail={
"error": "file_too_large",
"message": str(e),
**errors.eroare("IMPORT_FISIER_PREA_MARE", cauza=str(e)),
},
)
except HeaderError as e:
@@ -423,23 +434,28 @@ async def upload_import(
"error": "header_error",
"message": str(e),
"found": e.found,
**errors.eroare("IMPORT_ANTET_NECLAR", cauza=str(e)),
},
)
except UnicodeDecodeError as e:
_enc_msg = f"Encoding nesuportat: {e.reason}"
raise HTTPException(
status_code=422,
detail={
"error": "encoding_error",
"message": f"Encoding nesuportat: {e.reason}",
"message": _enc_msg,
**errors.eroare("IMPORT_ENCODING", cauza=_enc_msg),
},
)
except Exception as e:
# Fisier corupt (BadZipFile, InvalidFileException, etc.)
_inv_msg = f"Fisier nerecunoscut (xlsx/csv): {type(e).__name__}"
raise HTTPException(
status_code=422,
detail={
"error": "invalid_file",
"message": f"Fisier nerecunoscut (xlsx/csv): {type(e).__name__}",
"message": _inv_msg,
**errors.eroare("IMPORT_FISIER_NERECUNOSCUT", cauza=_inv_msg),
},
)
@@ -713,11 +729,13 @@ def preview_import(
).fetchone()
if not mapping_row:
_ncm_msg = "Maparea coloanelor nu a fost configurata. Configureaza mai intai maparea."
raise HTTPException(
status_code=422,
detail={
"error": "no_column_mapping",
"message": "Maparea coloanelor nu a fost configurata. Configureaza mai intai maparea.",
"message": _ncm_msg,
**errors.eroare("IMPORT_FARA_MAPARE_COLOANE", cauza=_ncm_msg),
},
)
@@ -971,12 +989,14 @@ def commit_import(
# Gate HARD: n_confirmat trebuie sa fie EXACT egal cu numarul de randuri ok
n_total_ok = len(ok_rows)
if req.n_confirmat != n_total_ok:
_cg_msg = f"Ai confirmat {req.n_confirmat} dar sunt {n_total_ok} randuri gata de trimis. Verifica preview-ul."
raise HTTPException(
status_code=422,
detail={
"error": "confirmare_gresita",
"message": f"Ai confirmat {req.n_confirmat} dar sunt {n_total_ok} randuri gata de trimis. Verifica preview-ul.",
"message": _cg_msg,
"n_ok": n_total_ok,
**errors.eroare("IMPORT_CONFIRMARE_GRESITA", cauza=_cg_msg),
},
)

View File

@@ -22,6 +22,7 @@ from pydantic import BaseModel, Field
from ...auth import resolve_account_id
from ...crypto import encrypt_creds
from ...db import get_connection
from ...errors import eroare as err_eroare
from ...idempotency import build_key, canonicalize_row
from ...mapping import (
account_or_default,
@@ -133,12 +134,17 @@ def valideaza_prezentari(
for i, prez in enumerate(req.prezentari):
content = prez.model_dump()
res = classify_prezentare(content, mapping, mapping_meta)
# US-003: imbogatim fiecare element nemapat cu 3 niveluri COD_NEMAPAT
nemapate = [
{**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} fara mapare RAR")}
for u in res["unmapped"]
]
results.append(ValidareResult(
index=i,
valid=(res["status"] == "queued"),
status_estimat=res["status"],
erori=res["errors"],
nemapate=res["unmapped"],
nemapate=nemapate,
prestatii_rezolvate=res["resolved"],
))
finally: