feat(api): raspuns minim implicit + corelare cerere-raspuns + sugestie cod in raspuns; coloana # in lista trimiteri

- POST /v1/prezentari: camp optional 'raspuns' (minim implicit / complet);
  forma minima = 6 chei fixe (index, submission_id, vehicul, status, motiv,
  id_prezentare), formatul detaliat ramane la cerere
- corelare rezultat<->cerere: index 0-based + ecou vin/nr_inmatriculare
- sugestia principala de cod RAR pe nemapate (GOLD partajat > SILVER >
  embeddings > fuzzy peste prag), in motiv si in nemapate[].sugestie
  (si pe dry-run /valideaza); suggestion-only, nu se aplica automat
- lista Trimiteri: coloana # cu id-ul trimiterii (desktop + mobil)
- README: sectiune 'Mapari si sugestii de cod RAR'; contract actualizat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-09 08:32:55 +00:00
parent c2beede084
commit 3ba35abed2
16 changed files with 239 additions and 20 deletions

View File

@@ -31,7 +31,7 @@ def _body(**over):
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez], "raspuns": "complet"}
def test_prezentare_valida_queued(client):
@@ -117,6 +117,66 @@ def test_raspuns_nu_reflecta_parola(client):
assert "password" not in r.text
def test_rezultate_poarta_index_si_ecou_vehicul(client):
"""Fiecare rezultat e corelabil cu cererea: index 0-based + ecou vin/nr,
inclusiv pe randuri blocate si pe dedup."""
body = _body()
prez2 = dict(body["prezentari"][0])
prez2.update({"vin": "WAUZZZ8K9BA123456", "nr_inmatriculare": "cj45xyz",
"data_prestatie": "2099-01-01"})
body["prezentari"].append(prez2)
r = client.post("/v1/prezentari", json=body)
res = r.json()["results"]
assert [x["index"] for x in res] == [0, 1]
assert res[0]["vin"] == "WVWZZZ1KZAW000123"
assert res[1]["nr_inmatriculare"] == "CJ45XYZ" # ecou normalizat (upper)
assert res[1]["status"] == "needs_data"
# Dedup: acelasi continut -> rezultatul pastreaza corelarea.
r2 = client.post("/v1/prezentari", json=body)
res2 = r2.json()["results"]
assert res2[0]["deduped"] is True
assert res2[0]["index"] == 0
assert res2[0]["vin"] == "WVWZZZ1KZAW000123"
def test_nemapat_primeste_sugestie(client):
"""Cod nemapat cu denumire apropiata de nomenclator -> sugestie fuzzy in
nemapate[].sugestie (complet) si in motiv (ambele forme)."""
prestatii = [{"cod_op_service": "REV-PER", "denumire": "Revizie periodica"}]
r = client.post("/v1/prezentari", json=_body(prestatii=prestatii))
res = r.json()["results"][0]
assert res["status"] == "needs_mapping"
sug = res["nemapate"][0]["sugestie"]
assert sug["cod_prestatie"] == "OE-3" # seed: REVIZIE PERIODICA
assert sug["sursa"] == "fuzzy"
assert "sugestie: OE-3" in res["motiv"]
# Forma minima: sugestia ramane vizibila in motiv.
body = _body(prestatii=prestatii, vin="WAUZZZ8K9BA123456")
del body["raspuns"]
r2 = client.post("/v1/prezentari", json=body)
assert "sugestie: OE-3" in r2.json()["results"][0]["motiv"]
def test_raspuns_minim_implicit(client):
"""Fara `raspuns` in cerere -> forma compacta cu chei fixe, fara campurile detaliate."""
body = _body(prestatii=[{"cod_op_service": "REP-MOT", "denumire": "Reparatie motor"}])
del body["raspuns"]
r = client.post("/v1/prezentari", json=body)
res = r.json()["results"][0]
assert set(res) == {"index", "submission_id", "vehicul", "status", "motiv", "id_prezentare"}
assert res["index"] == 0
assert res["vehicul"] == "B999TST"
assert res["status"] == "needs_mapping"
assert "REP-MOT" in res["motiv"]
# 'minim' explicit = acelasi lucru cu default-ul.
body["raspuns"] = "minim"
r2 = client.post("/v1/prezentari", json=body)
assert set(r2.json()["results"][0]) == set(res)
def test_json_malformat_422(client):
# Lipseste vin -> validare de shape Pydantic -> 422 (NU needs_data).
bad = {"rar_credentials": {"email": "x", "password": "y"},

View File

@@ -38,7 +38,7 @@ def _body(rar_env=None, **over):
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
body = {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
body = {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez], "raspuns": "complet"}
if rar_env is not None:
body["rar_env"] = rar_env
return body

View File

@@ -479,7 +479,7 @@ def test_retry_idempotent_nu_consuma_cota(client):
assert usage_1 == 1, f"Dupa prima trimitere: asteptat usage=1, primit {usage_1}"
# Retry (acelasi payload -> acelasi idempotency_key -> dedup, fara INSERT nou)
r2 = client.post("/v1/prezentari", json={"prezentari": [prez_unic]})
r2 = client.post("/v1/prezentari", json={"prezentari": [prez_unic], "raspuns": "complet"})
assert r2.status_code == 200, f"Retry trebuie sa treaca (dedup): {r2.text}"
assert r2.json()["results"][0].get("deduped") is True, "Retry trebuia marcat deduped"

View File

@@ -161,7 +161,7 @@ def _body(**over):
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez], "raspuns": "complet"}
def test_api_dedup_dupa_t9(client):

View File

@@ -37,7 +37,7 @@ def _body(password="corecta", **over):
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
return {"rar_credentials": {"email": "x@y.ro", "password": password}, "prezentari": [prez]}
return {"rar_credentials": {"email": "x@y.ro", "password": password}, "prezentari": [prez], "raspuns": "complet"}
def _force_status(sid, status):

View File

@@ -75,7 +75,7 @@ def _body(**over) -> dict:
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
return {"prezentari": [prez]}
return {"prezentari": [prez], "raspuns": "complet"}
# --------------------------------------------------------------------------- #

View File

@@ -205,7 +205,7 @@ def _body(prestatii, **over):
"prestatii": prestatii,
}
prez.update(over)
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez], "raspuns": "complet"}
def test_nomenclator_seed_la_boot(client):

View File

@@ -99,7 +99,7 @@ def _body(**over):
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
return {"rar_credentials": {"email": "x@y.ro", "password": "HUNTER2SECRET"}, "prezentari": [prez]}
return {"rar_credentials": {"email": "x@y.ro", "password": "HUNTER2SECRET"}, "prezentari": [prez], "raspuns": "complet"}
def test_422_does_not_echo_password(env):

View File

@@ -259,5 +259,5 @@ def test_mobil_pastreaza_doua_linii_cu_bulina_si_litera(client):
html = client.get("/?tab=coada").text
assert re.search(r'\.trimiteri-head\s*\{\s*display\s*:\s*none', html), \
"Headerul de coloane nu e ascuns pe mobil"
m = re.search(r'grid-template-areas\s*:\s*"check veh\s+act env status"\s*"check meta act env status"', html)
assert m, "Grid-areas pe 2 linii (veh/meta) lipseste din CSS-ul mobil"
m = re.search(r'grid-template-areas\s*:\s*"check id\s+veh\s+act env status"\s*"check meta meta act env status"', html)
assert m, "Grid-areas pe 2 linii (id+veh/meta) lipseste din CSS-ul mobil"