"""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 == [] # --------------------------------------------------------------------------- # # US-002: reguli text (substring) dupa maparea exacta # # --------------------------------------------------------------------------- # def test_regula_text_contains_rezolva(): """O operatie nemapata al carei text CONTINE pattern-ul primeste codul regulii.""" text_rules = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}] resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP1", "denumire": "Verificare faruri"}], {}, valid_codes={"OE-2"}, text_rules=text_rules, ) assert resolved[0]["cod_prestatie"] == "OE-2" assert unmapped == [] def test_mapare_exacta_bate_regula_text(): """Maparea exacta cod_op_service->cod are precedenta peste regula text.""" text_rules = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}] resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP1", "denumire": "Verificare faruri"}], {"OP1": "OE-1"}, valid_codes={"OE-1", "OE-2"}, text_rules=text_rules, ) assert resolved[0]["cod_prestatie"] == "OE-1" # maparea exacta castiga assert unmapped == [] def test_regula_text_insensibila_diacritice_caz(): """Match-ul e insensibil la diacritice si majuscule (ambele parti normalizate).""" text_rules = [{"pattern": "Verificări", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}] resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP1", "denumire": "VERIFICARI complete auto"}], {}, valid_codes={"OE-2"}, text_rules=text_rules, ) assert resolved[0]["cod_prestatie"] == "OE-2" assert unmapped == [] def test_regula_text_cod_invalid_in_nomenclator_ramane_nemapat(): """Regula da match dar codul ei nu e in nomenclator -> operatia ramane nemapata.""" text_rules = [{"pattern": "verificare", "cod_prestatie": "ZZZ", "auto_send": 0, "priority": 0}] resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP1", "denumire": "Verificare faruri"}], {}, valid_codes={"OE-2"}, text_rules=text_rules, ) assert resolved[0]["cod_prestatie"] is None assert unmapped == [{"cod_op_service": "OP1", "denumire": "Verificare faruri"}] def test_prima_regula_dupa_priority_castiga(): """La match multiplu castiga prima regula in ordinea listei (priority, id).""" text_rules = [ {"pattern": "verificare faruri", "cod_prestatie": "OE-3", "auto_send": 0, "priority": 0}, {"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 1}, ] resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP1", "denumire": "Verificare faruri"}], {}, valid_codes={"OE-2", "OE-3"}, text_rules=text_rules, ) assert resolved[0]["cod_prestatie"] == "OE-3" # prima din lista 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" # PRD 5.7: raspuns onest si pe ramura respinsa — nemapate + motiv populate (aditiv). assert res["nemapate"] and res["nemapate"][0]["cod_op_service"] == _COD_INTERN assert res["motiv"] # 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_noul_comportament(client): """Mapare cu auto_send=0 -> queued (auto_send ignorat dupa US-001). Dupa US-001: classify_prezentare nu mai produce ramura AUTO_SEND_OPRIT. O operatie cu cod rezolvat (indiferent de auto_send) -> queued direct. """ 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"] == "queued", ( f"dupa US-001 auto_send=0 -> queued (nu needs_mapping), got {res['status']}" ) 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}" # =========================================================================== # # US-001: Scoate hold auto_send din mapare — teste RED (inainte de implementare) # =========================================================================== # @pytest.fixture() def db_conn(monkeypatch, tmp_path): """Conexiune directa la o DB temporara, fara client HTTP.""" db_path = str(tmp_path / "us001.db") monkeypatch.setenv("AUTOPASS_DB_PATH", db_path) from app.config import get_settings get_settings.cache_clear() from app.db import init_db init_db() from app.db import get_connection c = get_connection() yield c c.close() get_settings.cache_clear() def test_operatie_mapata_intra_in_queued_indiferent_de_autosend(): """classify_prezentare cu mapare auto_send=0 -> queued (nu needs_mapping). Dupa US-001: has_no_auto_send nu mai blocheaza; un cod rezolvat e direct queued. """ 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}} valid_codes = {"OE-1"} result = classify_prezentare(content, mapping, mapping_meta, valid_codes) assert result["status"] == "queued", ( f"asteptat queued (auto_send ignorat), got {result['status']}: {result.get('rar_error')}" ) def test_regula_text_rezolvata_nu_mai_tine_randul(): """Regula text cu auto_send=0 rezolva codul -> queued (nu needs_mapping held). Dupa US-001: regula_fara_autosend nu se mai seteaza; codul rezolvat = queued direct. """ from app.mapping import classify_prezentare content = { "vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST", "data_prestatie": "2026-06-15", "odometru_final": "123456", "prestatii": [{"cod_op_service": "X99", "denumire": "Verificare faruri"}], } text_rules = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}] valid_codes = {"OE-2"} result = classify_prezentare(content, {}, {}, valid_codes, text_rules) assert result["status"] == "queued", ( f"asteptat queued (regula text, auto_send ignorat), got {result['status']}: {result.get('rar_error')}" ) def test_fara_stare_needs_mapping_pe_auto_send_oprit(): """has_no_auto_send intotdeauna False dupa US-001; nu mai produce AUTO_SEND_OPRIT.""" from app.mapping import has_no_auto_send mapping_meta_false = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}} resolved = [{"cod_op_service": "OP_REVIEW", "cod_prestatie": "OE-1"}] assert has_no_auto_send(resolved, mapping_meta_false) is False, ( "has_no_auto_send trebuie sa intoarca mereu False dupa US-001" ) resolved_cu_flag = [{"cod_op_service": "X", "cod_prestatie": "OE-1", "regula_fara_autosend": True}] assert has_no_auto_send(resolved_cu_flag, {}) is False, ( "has_no_auto_send ignora regula_fara_autosend dupa US-001" ) def test_niciun_rand_existent_nu_se_dezgheata(db_conn): """Randuri legacy needs_mapping-din-auto_send: fara afordanta UI (cod prezent), dezghetabile via reresolve_account explicit (nu automat). _nemapate_pentru_submission -> [] (fara panou mapare in UI). reresolve_account cu mapare activa -> requeued=1 (dezghet via actiune explicita). """ import json as _json payload = { "vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1", "data_prestatie": "2026-06-15", "odometru_final": "123456", "prestatii": [{"cod_op_service": "X1", "cod_prestatie": "OE-1"}], } rar_error = _json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual"}) db_conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) " "VALUES (?, ?, ?, ?, ?)", ("k-legacy-us001", 1, "needs_mapping", _json.dumps(payload), rar_error), ) db_conn.execute( "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES ('OE-1', 'Test')" ) db_conn.execute( "INSERT OR REPLACE INTO operations_mapping " "(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1, 'X1', 'OE-1', 0)" ) db_conn.commit() row = db_conn.execute( "SELECT * FROM submissions WHERE idempotency_key='k-legacy-us001'" ).fetchone() nomenclator = [{"cod_prestatie": "OE-1", "nume_prestatie": "Test"}] # Nicio afordanta UI (cod deja prezent -> nu se arata panoul de mapare) from app.web.routes import _nemapate_pentru_submission assert _nemapate_pentru_submission(row, nomenclator) == [], ( "_nemapate_pentru_submission trebuie sa intoarca [] (cod deja prezent)" ) # Dezghetare via reresolve_account explicit (actiune admin la deploy) from app.mapping import reresolve_account stats = reresolve_account(db_conn, 1) assert stats["requeued"] == 1, ( f"reresolve_account trebuie sa requeueze randul legacy: {stats}" ) row2 = db_conn.execute( "SELECT status FROM submissions WHERE idempotency_key='k-legacy-us001'" ).fetchone() assert row2["status"] == "queued" def test_canal_api_auto_send_ignorat_intra_queued(): """classify_prezentare (canal API) cu mapping_meta auto_send=0 -> queued. Campul auto_send din mapping_meta nu mai afecteaza decizia de clasificare. """ from app.mapping import classify_prezentare content = { "vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST", "data_prestatie": "2026-06-15", "odometru_final": "123456", "prestatii": [{"cod_op_service": "ITP-CHECK", "denumire": "Inspectie"}], } mapping = {"ITP-CHECK": "OE-1"} mapping_meta = {"ITP-CHECK": {"cod_prestatie": "OE-1", "auto_send": False}} valid_codes = {"OE-1"} result = classify_prezentare(content, mapping, mapping_meta, valid_codes) assert result["status"] == "queued", ( f"canal API: auto_send ignorat -> asteptat queued, got {result['status']}" )