feat(api): endpoint dry-run POST /v1/prezentari/valideaza (PRD 5.2)

Valideaza payload + mapare si intoarce verdictul real (status_estimat
queued/needs_data/needs_mapping + erori [{field,message}] + coduri nemapate
+ prestatii rezolvate) FARA enqueue, fara creds, zero scriere DB. "Magical
moment" pentru integratori (ROAAUTO / soft propriu / punte VFP).

Cheia de design: helper pur partajat classify_prezentare (mapping.py) folosit
de AMBELE rute, ca dry-run-ul sa nu poata diverge de trimiterea reala
(invariant de corectitudine). create_prezentari refactorizat pe el cu
comportament identic (test_api.py verde).

Scope minim (decizie user): doar validare+mapare, fara idempotency/duplicat
(idempotency.py neatins); descoperibilitate in hub /integrare amanata.

VERIFY context curat PASS (577 teste; E2E API cu cele 3 verdicte + COUNT(*)=0
dupa dry-run). /code-review high: 0 findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-22 18:54:50 +00:00
parent f0786051f5
commit ae7960294f
6 changed files with 445 additions and 35 deletions

View File

@@ -217,6 +217,61 @@ def load_mapping_meta(conn, account_id: int | None) -> dict[str, dict]:
}
def classify_prezentare(
content: dict,
mapping: dict[str, str],
mapping_meta: dict[str, dict],
) -> dict:
"""Helper pur de clasificare: reproduce EXACT logica create_prezentari fara DB/efecte.
Apelat de AMBELE rute (POST /v1/prezentari si POST /v1/prezentari/valideaza) pentru
a garanta acelasi verdict — invariantul de corectitudine dry-run (PRD 5.2).
Intoarce {"status", "rar_error", "resolved", "unmapped", "errors", "content"}.
"content" = copia actualizata (VIN/nr canonicalizat + prestatii rezolvate).
"""
from .idempotency import canonicalize_row # import local: evita circular (mapping <- idempotency)
c = dict(content)
canon = canonicalize_row(c)
c.update({
"vin": canon["vin"],
"nr_inmatriculare": canon["nr_inmatriculare"],
"odometru_final": canon["odometru_final"],
})
resolved, unmapped = resolve_prestatii(c.get("prestatii"), mapping)
c["prestatii"] = resolved
if unmapped:
status = "needs_mapping"
rar_error = json.dumps({"unmapped": unmapped}, ensure_ascii=False)
errors: list[dict] = []
else:
errors = validate_prezentare(c)
if errors:
status = "needs_data"
rar_error = json.dumps(errors, ensure_ascii=False)
elif has_no_auto_send(resolved, mapping_meta):
status = "needs_mapping"
rar_error = json.dumps(
{"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"},
ensure_ascii=False,
)
else:
status = "queued"
rar_error = None
return {
"status": status,
"rar_error": rar_error,
"resolved": resolved,
"unmapped": unmapped,
"errors": errors,
"content": c,
}
def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool:
"""Verifica daca vreun item rezolvat via mapping are auto_send=0.