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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user