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

@@ -29,6 +29,7 @@ from ...mapping import (
account_scope_clause,
classify_prezentare,
load_mapping_meta,
load_nomenclator_codes,
pending_unmapped,
reresolve_account,
save_mapping,
@@ -53,6 +54,36 @@ from ...submissions_admin import (
router = APIRouter(prefix="/v1", tags=["v1"])
def _effective_on_unmapped_error(conn, acct: int, req_value: bool | None) -> bool:
"""Modul efectiv la cod necunoscut/nemapat (True => respinge cererea, False => needs_mapping).
Precedenta: override per-cerere > default cont (on_unmapped_error_default) > False.
"""
if req_value is not None:
return req_value
row = conn.execute("SELECT on_unmapped_error_default FROM accounts WHERE id=?", (acct,)).fetchone()
return bool(row["on_unmapped_error_default"]) if row else False
def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode) -> dict:
"""classify_prezentare + aplicarea modului on_unmapped_error.
Cand exista coduri nemapate si error_mode=True, marcheaza outcome-ul ca respingere
(blocked_error=True): rutele NU mai fac enqueue, ci intorc o eroare per-element.
"""
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes)
cl["blocked_error"] = bool(cl["unmapped"]) and error_mode
return cl
def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
"""Erori 3 niveluri (COD_NEMAPAT) pentru raspunsul on_unmapped_error=True."""
return [
{**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} necunoscut/fara mapare RAR")}
for u in unmapped
]
@router.post("/prezentari", response_model=PrezentariResponse)
def create_prezentari(
req: PrezentareRequest,
@@ -82,6 +113,10 @@ def create_prezentari(
# T6/OV-1: load_mapping_meta include auto_send per op (gate pentru coduri noi).
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
# valid_codes gol (nomenclator nepopulat) -> None (nu validam, ca sa nu blocam tot).
valid_codes = load_nomenclator_codes(conn) or None
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
for prez in req.prezentari:
content = prez.model_dump()
# T9/OV-2: canonicalize_row inaintea build_key (odometru strip ".0", VIN upper).
@@ -104,7 +139,14 @@ def create_prezentari(
# retrimiterea aceluiasi continut. Il RE-ACTIVAM (re-clasificam + actualizam
# creds + reset), printr-un UPDATE compare-and-swap pe status='error'.
if existing["status"] == "error":
cl = classify_prezentare(content, mapping, mapping_meta)
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
if cl["blocked_error"]:
# on_unmapped_error=True: nu reactivam; randul ramane 'error'.
results.append(SubmissionResult(
submission_id=existing["id"], status="error",
erori=_erori_nemapate(cl["unmapped"]),
))
continue
cur = conn.execute(
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
"rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, "
@@ -143,7 +185,13 @@ def create_prezentari(
# Helper pur partajat cu dry-run (PRD 5.2): reproduce EXACT clasificarea
# (canonicalize + mapare op->cod + validare + auto_send gate).
cl = classify_prezentare(content, mapping, mapping_meta)
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
if cl["blocked_error"]:
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
results.append(SubmissionResult(
submission_id=None, status="error", erori=_erori_nemapate(cl["unmapped"]),
))
continue
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc) "
"VALUES (?, ?, ?, ?, ?, ?)",
@@ -191,9 +239,13 @@ def valideaza_prezentari(
try:
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
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
for i, prez in enumerate(req.prezentari):
content = prez.model_dump()
res = classify_prezentare(content, mapping, mapping_meta)
res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
if res["blocked_error"]:
res = {**res, "status": "error"}
# 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")}