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>
359 lines
14 KiB
Python
359 lines
14 KiB
Python
"""Teste mapare op ROAAUTO -> cod RAR: fuzzy, rezolvare pura, flux on-demand.
|
|
|
|
Contract hibrid (decis 2026-06-15): item de prestatie cu cod_prestatie (RAR direct)
|
|
SAU cod_op_service+denumire (mapat de gateway). Op nemapata -> needs_mapping, apare
|
|
in editor; la salvarea maparii submission-ul se re-rezolva automat.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.mapping import normalize_for_match, resolve_prestatii, suggest_codes
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Pur #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_normalize_scoate_diacritice_si_colapseaza():
|
|
assert normalize_for_match("Reparație motor") == "REPARATIE MOTOR"
|
|
assert normalize_for_match(" întreținere ") == "INTRETINERE"
|
|
assert normalize_for_match(None) == ""
|
|
|
|
|
|
_NOM = [
|
|
{"cod_prestatie": "OE-1", "nume_prestatie": "REPARATIE"},
|
|
{"cod_prestatie": "OE-2", "nume_prestatie": "INTRETINERE"},
|
|
{"cod_prestatie": "OE-3", "nume_prestatie": "REVIZIE PERIODICA"},
|
|
{"cod_prestatie": "R-ODO", "nume_prestatie": "REPARATIE ODOMETRU"},
|
|
]
|
|
|
|
|
|
def test_suggest_pune_potrivirea_evidenta_prima():
|
|
s = suggest_codes("Reparatie odometru electronic", _NOM, limit=4)
|
|
assert s[0]["cod_prestatie"] == "R-ODO"
|
|
assert s[0]["score"] >= 60
|
|
|
|
|
|
def test_suggest_denumire_goala_intoarce_nomenclator_scor_zero():
|
|
s = suggest_codes("", _NOM, limit=2)
|
|
assert len(s) == 2
|
|
assert all(x["score"] == 0 for x in s)
|
|
|
|
|
|
def test_resolve_cod_direct_trece_neatins():
|
|
resolved, unmapped = resolve_prestatii([{"cod_prestatie": "oe-1"}], {})
|
|
assert resolved[0]["cod_prestatie"] == "OE-1" # normalizat upper
|
|
assert unmapped == []
|
|
|
|
|
|
def test_resolve_op_mapata():
|
|
resolved, unmapped = resolve_prestatii(
|
|
[{"cod_op_service": "1234", "denumire": "Schimb ulei"}], {"1234": "OE-2"}
|
|
)
|
|
assert resolved[0]["cod_prestatie"] == "OE-2"
|
|
assert unmapped == []
|
|
|
|
|
|
def test_resolve_op_nemapata_iese_in_unmapped():
|
|
resolved, unmapped = resolve_prestatii(
|
|
[{"cod_op_service": "9999", "denumire": "Operatie noua"}], {}
|
|
)
|
|
assert resolved[0]["cod_prestatie"] is None
|
|
assert unmapped == [{"cod_op_service": "9999", "denumire": "Operatie noua"}]
|
|
|
|
|
|
def test_resolve_cod_valid_cu_nomenclator_trece():
|
|
"""cod_prestatie in nomenclator -> pastrat (validare activa)."""
|
|
resolved, unmapped = resolve_prestatii([{"cod_prestatie": "oe-1"}], {}, valid_codes={"OE-1"})
|
|
assert resolved[0]["cod_prestatie"] == "OE-1"
|
|
assert unmapped == []
|
|
|
|
|
|
def test_resolve_cod_necunoscut_devine_unmapped():
|
|
"""cod_prestatie NECUNOSCUT in nomenclator -> promovat la cod_op_service + needs_mapping.
|
|
|
|
Regresie pentru bug-ul real: un cod intern in cod_prestatie (ex. 'DIVERSE
|
|
VERIFICARI 159002') NU trebuie trimis raw la RAR (HTTP 500 + record partial).
|
|
"""
|
|
resolved, unmapped = resolve_prestatii(
|
|
[{"cod_prestatie": "DIVERSE VERIFICARI 159002"}], {}, valid_codes={"OE-1", "R-ODO"}
|
|
)
|
|
assert resolved[0]["cod_prestatie"] is None
|
|
assert resolved[0]["cod_op_service"] == "DIVERSE VERIFICARI 159002" # promovat
|
|
assert unmapped == [{"cod_op_service": "DIVERSE VERIFICARI 159002",
|
|
"denumire": "DIVERSE VERIFICARI 159002"}]
|
|
|
|
|
|
def test_resolve_cod_necunoscut_cu_mapare_se_rezolva():
|
|
"""Dupa ce codul necunoscut a fost mapat, se rezolva la codul RAR (re-rezolvare)."""
|
|
resolved, unmapped = resolve_prestatii(
|
|
[{"cod_prestatie": "DIVERSE VERIFICARI 159002"}],
|
|
{"DIVERSE VERIFICARI 159002": "OE-1"},
|
|
valid_codes={"OE-1"},
|
|
)
|
|
assert resolved[0]["cod_prestatie"] == "OE-1"
|
|
assert unmapped == []
|
|
|
|
|
|
def test_resolve_fara_valid_codes_e_backcompat():
|
|
"""valid_codes=None -> validarea dezactivata: cod direct trece neatins (compat)."""
|
|
resolved, unmapped = resolve_prestatii([{"cod_prestatie": "ORICE-COD"}], {})
|
|
assert resolved[0]["cod_prestatie"] == "ORICE-COD"
|
|
assert unmapped == []
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Flux complet (API) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _body(prestatii, **over):
|
|
prez = {
|
|
"vin": "WVWZZZ1KZAW000123",
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"prestatii": prestatii,
|
|
}
|
|
prez.update(over)
|
|
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
|
|
|
|
|
|
def test_nomenclator_seed_la_boot(client):
|
|
r = client.get("/v1/nomenclator")
|
|
coduri = {n["cod_prestatie"] for n in r.json()["nomenclator"]}
|
|
assert {"OE-1", "R-ODO", "I-ODO"} <= coduri
|
|
|
|
|
|
def test_cod_op_nemapat_da_needs_mapping(client):
|
|
r = client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "Reparatie generala"}]))
|
|
assert r.status_code == 200
|
|
assert r.json()["results"][0]["status"] == "needs_mapping"
|
|
|
|
|
|
def test_pending_arata_op_cu_sugestii(client):
|
|
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "Reparatie generala"}]))
|
|
pend = client.get("/v1/mapari/pending").json()["pending"]
|
|
assert len(pend) == 1
|
|
e = pend[0]
|
|
assert e["cod_op_service"] == "OP100"
|
|
assert e["blocked"] == 1
|
|
assert e["suggestions"] and e["suggestions"][0]["cod_prestatie"]
|
|
|
|
|
|
def test_salvare_mapare_deblocheaza_submission(client):
|
|
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "Reparatie"}]))
|
|
r = client.post("/v1/mapari", json={"cod_op_service": "OP100", "cod_prestatie": "OE-1", "auto_send": True})
|
|
assert r.status_code == 200
|
|
assert r.json()["reresolve"]["requeued"] == 1
|
|
# submission-ul e acum queued
|
|
subs = client.get("/v1/prezentari", params={"status": "queued"}).json()["submissions"]
|
|
assert len(subs) == 1
|
|
# nu mai e nimic in pending
|
|
assert client.get("/v1/mapari/pending").json()["pending"] == []
|
|
|
|
|
|
def test_mapare_cod_inexistent_respinsa(client):
|
|
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "x"}]))
|
|
r = client.post("/v1/mapari", json={"cod_op_service": "OP100", "cod_prestatie": "ZZZ", "auto_send": True})
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_mapare_apoi_re_ingestie_e_directa(client):
|
|
"""Dupa ce maparea exista, o noua comanda cu acelasi op intra direct queued."""
|
|
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "x"}]))
|
|
client.post("/v1/mapari", json={"cod_op_service": "OP100", "cod_prestatie": "OE-1", "auto_send": True})
|
|
r = client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "x"}], vin="WVWZZZ1KZAW000999"))
|
|
assert r.json()["results"][0]["status"] == "queued"
|
|
|
|
|
|
def test_cod_prestatie_direct_inca_merge(client):
|
|
"""Back-compat: trimiterea codului RAR direct se comporta ca inainte (queued)."""
|
|
r = client.post("/v1/prezentari", json=_body([{"cod_prestatie": "OE-1"}]))
|
|
assert r.json()["results"][0]["status"] == "queued"
|
|
|
|
|
|
def test_op_mapat_declanseaza_regula_odometru(client):
|
|
"""Dupa mapare la R-ODO, validarea cere odometruInitial -> needs_data (nu queued)."""
|
|
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OPODO", "denumire": "Reparatie odometru"}]))
|
|
r = client.post("/v1/mapari", json={"cod_op_service": "OPODO", "cod_prestatie": "R-ODO", "auto_send": True})
|
|
stats = r.json()["reresolve"]
|
|
assert stats["needs_data"] == 1 and stats["requeued"] == 0
|
|
|
|
|
|
def test_item_fara_cod_si_fara_op_e_422(client):
|
|
r = client.post("/v1/prezentari", json=_body([{"denumire": "doar text"}]))
|
|
assert r.status_code == 422
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Cod_prestatie necunoscut in nomenclator + optiunea on_unmapped #
|
|
# (RAR accepta NUMAI coduri din nomenclator; cod necunoscut -> 500 + record #
|
|
# partial. Gateway-ul nu-l mai trimite raw.) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
_COD_INTERN = "DIVERSE VERIFICARI 159002" # >5 car., nu e in nomenclator
|
|
|
|
|
|
def test_cod_prestatie_necunoscut_da_needs_mapping(client):
|
|
"""Default: cod_prestatie necunoscut -> needs_mapping, apare in pending pentru mapare."""
|
|
r = client.post("/v1/prezentari", json=_body([{"cod_prestatie": _COD_INTERN}]))
|
|
assert r.status_code == 200
|
|
assert r.json()["results"][0]["status"] == "needs_mapping"
|
|
pend = client.get("/v1/mapari/pending").json()["pending"]
|
|
assert len(pend) == 1
|
|
assert pend[0]["cod_op_service"] == _COD_INTERN # promovat din cod_prestatie
|
|
|
|
|
|
def test_cod_necunoscut_mapat_se_trimite(client):
|
|
"""Flux complet: cod necunoscut -> needs_mapping -> mapezi -> queued."""
|
|
client.post("/v1/prezentari", json=_body([{"cod_prestatie": _COD_INTERN}]))
|
|
r = client.post("/v1/mapari", json={"cod_op_service": _COD_INTERN, "cod_prestatie": "OE-1", "auto_send": True})
|
|
assert r.json()["reresolve"]["requeued"] == 1
|
|
subs = client.get("/v1/prezentari", params={"status": "queued"}).json()["submissions"]
|
|
assert len(subs) == 1
|
|
|
|
|
|
def test_on_unmapped_error_respinge_fara_enqueue(client):
|
|
"""on_unmapped_error=True per-cerere: cod necunoscut -> status error, fara submission."""
|
|
body = _body([{"cod_prestatie": _COD_INTERN}])
|
|
body["on_unmapped_error"] = True
|
|
r = client.post("/v1/prezentari", json=body)
|
|
assert r.status_code == 200
|
|
res = r.json()["results"][0]
|
|
assert res["status"] == "error"
|
|
assert res["submission_id"] is None
|
|
assert res["erori"] and res["erori"][0]["cod"] == "COD_NEMAPAT"
|
|
# Nu s-a creat nimic in coada.
|
|
assert client.get("/v1/prezentari").json()["submissions"] == []
|
|
|
|
|
|
def test_on_unmapped_default_cont_error(client):
|
|
"""Default per-cont (on_unmapped_error_default=1) se aplica cand cererea nu specifica optiunea."""
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
conn.execute("UPDATE accounts SET on_unmapped_error_default=1 WHERE id=1")
|
|
conn.commit()
|
|
conn.close()
|
|
r = client.post("/v1/prezentari", json=_body([{"cod_prestatie": _COD_INTERN}]))
|
|
res = r.json()["results"][0]
|
|
assert res["status"] == "error" and res["submission_id"] is None
|
|
# Override per-cerere bate default-ul de cont:
|
|
body = _body([{"cod_prestatie": _COD_INTERN}], vin="WVWZZZ1KZAW000999")
|
|
body["on_unmapped_error"] = False
|
|
r2 = client.post("/v1/prezentari", json=body)
|
|
assert r2.json()["results"][0]["status"] == "needs_mapping"
|
|
|
|
|
|
def test_valideaza_error_mode(client):
|
|
"""Dry-run reflecta modul error: status_estimat='error' pentru cod necunoscut."""
|
|
body = _body([{"cod_prestatie": _COD_INTERN}])
|
|
body["on_unmapped_error"] = True
|
|
r = client.post("/v1/prezentari/valideaza", json=body)
|
|
assert r.status_code == 200
|
|
res = r.json()["results"][0]
|
|
assert res["status_estimat"] == "error" and res["valid"] is False
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# US-003: 3 niveluri in classify_prezentare (needs_mapping) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_unmapped_are_3niveluri(client):
|
|
"""cod_op_service necunoscut -> needs_mapping; rar_error are cheie 'unmapped'
|
|
PASTRATA + campurile COD_NEMAPAT (cod/problema/cauza/fix)."""
|
|
import json
|
|
from app.mapping import classify_prezentare
|
|
|
|
content = {
|
|
"vin": "WVWZZZ1KZAW000123",
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"prestatii": [{"cod_op_service": "OP_NECUNOSCUT", "denumire": "Reparatie necunoscuta"}],
|
|
}
|
|
mapping = {}
|
|
mapping_meta = {}
|
|
res = classify_prezentare(content, mapping, mapping_meta)
|
|
assert res["status"] == "needs_mapping"
|
|
err = json.loads(res["rar_error"])
|
|
# Cheia originala pastrata
|
|
assert "unmapped" in err
|
|
assert len(err["unmapped"]) == 1
|
|
assert err["unmapped"][0]["cod_op_service"] == "OP_NECUNOSCUT"
|
|
# 3 niveluri prezente
|
|
assert err["cod"] == "COD_NEMAPAT"
|
|
assert err["problema"]
|
|
assert err["cauza"]
|
|
assert err["fix"]
|
|
|
|
|
|
def test_auto_send_oprit_3niveluri(client):
|
|
"""Mapare cu auto_send=0 -> needs_mapping; rar_error are cheie 'auto_send'
|
|
PASTRATA + campurile AUTO_SEND_OPRIT (cod/problema/cauza/fix)."""
|
|
import json
|
|
from app.mapping import classify_prezentare
|
|
|
|
content = {
|
|
"vin": "WVWZZZ1KZAW000123",
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"prestatii": [{"cod_op_service": "OP_REVIEW", "denumire": "Operatie cu review"}],
|
|
}
|
|
mapping = {"OP_REVIEW": "OE-1"}
|
|
mapping_meta = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
|
|
res = classify_prezentare(content, mapping, mapping_meta)
|
|
assert res["status"] == "needs_mapping"
|
|
err = json.loads(res["rar_error"])
|
|
# Cheia originala pastrata
|
|
assert "auto_send" in err
|
|
# 3 niveluri prezente
|
|
assert err["cod"] == "AUTO_SEND_OPRIT"
|
|
assert err["problema"]
|
|
assert err["cauza"]
|
|
assert err["fix"]
|
|
|
|
|
|
def test_needs_data_pass_through(client):
|
|
"""VIN invalid -> needs_data; rar_error = array cu erori care au cod/problema/fix (US-002)."""
|
|
import json
|
|
from app.mapping import classify_prezentare
|
|
|
|
content = {
|
|
"vin": "VIN_INVALID_XXXXXXXXX", # nu trece regex
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"prestatii": [{"cod_prestatie": "OE-1"}],
|
|
}
|
|
mapping = {}
|
|
mapping_meta = {}
|
|
res = classify_prezentare(content, mapping, mapping_meta)
|
|
assert res["status"] == "needs_data"
|
|
erori = json.loads(res["rar_error"])
|
|
assert isinstance(erori, list)
|
|
assert len(erori) >= 1
|
|
# Fiecare eroare are cele 3 niveluri (pass-through US-002)
|
|
for e in erori:
|
|
assert "cod" in e, f"lipseste 'cod' in {e}"
|
|
assert "problema" in e, f"lipseste 'problema' in {e}"
|
|
assert "fix" in e, f"lipseste 'fix' in {e}"
|