feat(T5): editor web mapare operatii (hibrid + fuzzy + on-demand needs_mapping)
T5 reinterpretat: nu import DBF, ci editor web al maparii operatie ROAAUTO -> cod RAR, cu fuzzy lookup si validare de catre utilizator. - Contract hibrid: item prestatie accepta cod_prestatie (RAR direct, back-compat) SAU cod_op_service+denumire (mapat de gateway prin operations_mapping). - Ingestie: op intern necunoscut -> submission needs_mapping (nu pleaca la RAR); codul rezolvat se scrie inapoi in payload_json -> payload builder + worker neatinse. - Editor HTMX (_mapari.html + GET /_fragments/mapari, POST /mapari): listeaza op-urile nemapate, fuzzy preselecteaza codul RAR, save -> re-rezolvare automata (queued / needs_data). - Fuzzy: rapidfuzz.token_sort_ratio pe denumire normalizata (fara diacritice). - Nomenclator: seed fallback 18 coduri la boot (offline) + refresh live din worker. - Cont default id=1 cat timp auth API-key (CORE) nu exista (account_id NULL). - Endpointuri API: GET /v1/mapari/pending, POST /v1/mapari (respinge cod inexistent). - 15 teste noi (tests/test_mapping.py); 69 pass total. - Contract actualizat (docs/api-rar-contract.md), rapidfuzz==3.14.5 in requirements. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
tests/test_mapping.py
Normal file
163
tests/test_mapping.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""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"}]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 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
|
||||
Reference in New Issue
Block a user