feat(api): validare cod_prestatie la nomenclator + optiune on_unmapped_error

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>
This commit is contained in:
Claude Agent
2026-06-23 19:35:47 +00:00
parent c842e3352a
commit 6bad6bc01e
17 changed files with 376 additions and 23 deletions

View File

@@ -90,13 +90,23 @@ def suggest_codes(
def resolve_prestatii(
prestatii: list[dict] | None,
mapping: dict[str, str],
valid_codes: set[str] | None = None,
) -> tuple[list[dict], list[dict]]:
"""Rezolva fiecare item: umple `cod_prestatie` din maparea op->cod unde lipseste.
Reguli (hibrid):
- item cu `cod_prestatie` -> pastrat ca atare (cod RAR direct).
- item cu `cod_prestatie` valid (in nomenclator) -> pastrat ca atare.
- item fara cod, cu `cod_op_service` in `mapping` -> umplem cod_prestatie.
- item fara cod si fara mapare -> ramane nemapat.
- item cu `cod_prestatie` NECUNOSCUT in nomenclator -> tratat ca operatie de
mapat: il promovam la `cod_op_service` (daca nu exista deja) ca sa intre in
fluxul needs_mapping. Confirmat live (2026-06-23): RAR accepta NUMAI coduri
din nomenclator (coloana COD_PRESTATIE max 5 car.); un cod necunoscut da
HTTP 500 si RECORD PARTIAL la RAR (terminal) -> nu-l trimitem niciodata raw.
`valid_codes` = setul de coduri RAR valide (uppercase) din nomenclator. Cand e
None, validarea e dezactivata (compat: comportamentul vechi „cod_prestatie trece
neatins"); rutele API il paseaza intotdeauna.
Intoarce (prestatii_rezolvate, nemapate). `prestatii_rezolvate` pastreaza
si campurile originale (cod_op_service/denumire) ca re-rezolvarea sa aiba
@@ -109,15 +119,25 @@ def resolve_prestatii(
it = dict(item)
cod = (it.get("cod_prestatie") or "").strip().upper()
op = (it.get("cod_op_service") or "").strip()
if cod:
cod_valid = bool(cod) and (valid_codes is None or cod in valid_codes)
if cod_valid:
it["cod_prestatie"] = cod
elif op and op in mapping:
it["cod_prestatie"] = mapping[op]
elif op:
it["cod_prestatie"] = None
unmapped.append({"cod_op_service": op, "denumire": it.get("denumire")})
# item fara cod si fara op: il lasam asa; validarea de continut prinde
# "prestatii goale"/cod lipsa.
else:
# cod lipsa SAU necunoscut in nomenclator -> ruta de mapare.
if cod and not op:
# Promovam codul direct necunoscut la cod_op_service ca sa-l poti mapa
# in editor (cu denumire = codul, pentru sugestia fuzzy) si sa se retina.
op = cod
it["cod_op_service"] = op
if not it.get("denumire"):
it["denumire"] = cod
if op and op in mapping:
it["cod_prestatie"] = mapping[op]
elif op:
it["cod_prestatie"] = None
unmapped.append({"cod_op_service": op, "denumire": it.get("denumire")})
# item fara cod si fara op: il lasam asa; validarea de continut prinde
# "prestatii goale"/cod lipsa.
resolved.append(it)
return resolved, unmapped
@@ -192,6 +212,17 @@ def load_nomenclator(conn) -> list[dict]:
return [dict(r) for r in rows]
def load_nomenclator_codes(conn) -> set[str]:
"""Setul de coduri RAR valide (uppercase) pentru validarea cod_prestatie la ingestie.
Intoarce set() daca nomenclatorul e gol -> apelantul trebuie sa NU valideze in
acel caz (altfel ar bloca totul). In practica nomenclatorul e mereu populat:
seed fallback (18 coduri) la boot + upsert live de la worker la fiecare login.
"""
rows = conn.execute("SELECT cod_prestatie FROM nomenclator_rar").fetchall()
return {(r["cod_prestatie"] or "").strip().upper() for r in rows if (r["cod_prestatie"] or "").strip()}
def load_mapping(conn, account_id: int | None) -> dict[str, str]:
"""{cod_op_service -> cod_prestatie} pentru un cont."""
acct = account_or_default(account_id)
@@ -222,6 +253,7 @@ def classify_prezentare(
content: dict,
mapping: dict[str, str],
mapping_meta: dict[str, dict],
valid_codes: set[str] | None = None,
) -> dict:
"""Helper pur de clasificare: reproduce EXACT logica create_prezentari fara DB/efecte.
@@ -241,7 +273,7 @@ def classify_prezentare(
"odometru_final": canon["odometru_final"],
})
resolved, unmapped = resolve_prestatii(c.get("prestatii"), mapping)
resolved, unmapped = resolve_prestatii(c.get("prestatii"), mapping, valid_codes)
c["prestatii"] = resolved
if unmapped:
@@ -380,6 +412,7 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
acct = account_or_default(account_id)
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
valid_codes = load_nomenclator_codes(conn) or None
if batch_id is not None:
# T7: scope la batch-ul specificat (import commit explicit).
@@ -405,7 +438,7 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
content = json.loads(r["payload_json"])
except (ValueError, TypeError):
continue
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping)
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes)
content["prestatii"] = resolved
payload_json = json.dumps(content, ensure_ascii=False)