Files
rar-autopass/tests/test_exclude_declarare.py
Claude Agent 0bc618763e feat(mapari): regula 'exclude de la declarare' per operatie + stare preview Nedeclarat
Operatiile care nu se declara la RAR (ex. ITP facturat in service) primesc o
regula exclus=1 in operations_mapping, setabila din panoul de mapare al
preview-ului de import si din tab-ul Mapari (optiunea 'Nu se declara la RAR').

- resolve_prestatii(excluded_ops): item nemapat cu op exclusa -> adnotat exclus,
  nu mai e needs_mapping; precedenta: cod explicit > exclus > mapare > reguli text
- split_prestatii_excluse: itemii exclusi nu intra niciodata in payload/cheie
- preview import: rand cu toate operatiile excluse -> stare 'excluded'
  (eticheta Nedeclarat), necomis; operatia dispare din panoul de mapat
- reresolve/corectie/API: submission cu toate operatiile excluse -> needs_data
  cu motiv explicit; ingestia API trateaza excluderea la clasificare
- migrare: coloana operations_mapping.exclus + rebuild import_rows pentru
  CHECK-ul resolved_status cu 'excluded' (o singura data, gardat pe sqlite_master)
- fix flake: clamp similaritate embeddings la [-1,1] (float32 dadea 1.0000001)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:49:47 +00:00

305 lines
10 KiB
Python

"""Teste regula "exclude de la declarare" (operations_mapping.exclus).
O operatie exclusa nu se declara la RAR: randurile de import cu toate operatiile
excluse devin 'excluded' (Nedeclarat) si nu se comit; operatiile excluse dispar
din panoul de mapare; submission-urile API blocate trec pe needs_data cu motiv.
Un cod ales explicit pe rand bate regula de excludere.
"""
from __future__ import annotations
import json
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
from tests.test_web_preview_edit import ( # helpers reutilizate
_csv_bytes,
_get_csrf,
_upload_and_preview,
)
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "excl.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.crypto import reset_cache
reset_cache()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
reset_cache()
_ROWS_ITP = [
{
"VIN": "WVWZZZ3CZ9E123456",
"Nr": "TM789BC",
"Data": "2026-05-15",
"KM": "82500",
"Operatie": "OP-ITP",
},
{
"VIN": "WVWZZZ1KZAW000123",
"Nr": "B001TST",
"Data": "2026-06-10",
"KM": "123456",
"Operatie": "OP-1",
},
]
def _seed(account_id: int = 1) -> None:
"""Nomenclator + mapare OP-1 -> R-FRANE (OP-ITP ramane nemapat)."""
from app.db import get_connection
conn = get_connection()
try:
for cod, nume in (("R-FRANE", "Reparatie frane"), ("OE-2", "Verificare")):
conn.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
(cod, nume),
)
conn.execute(
"INSERT OR IGNORE INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) "
"VALUES (?, 'OP-1', 'R-FRANE', 1)",
(account_id,),
)
conn.commit()
finally:
conn.close()
def _row_status(iid: int, row_index: int) -> str:
from app.db import get_connection
from app.web.routes import _preview_one_row
conn = get_connection()
try:
result, row = _preview_one_row(conn, iid, 1, row_index)
assert row is not None and not isinstance(result, str)
return row["resolved_status"]
finally:
conn.close()
def test_panoul_de_mapare_ofera_optiunea_nedeclarat(client):
_seed()
iid = _upload_and_preview(client, rows=_ROWS_ITP)
r = client.get(f"/_import/{iid}/preview")
assert r.status_code == 200
assert '__NEDECLARAT__' in r.text, "Selectul de mapare trebuie sa ofere 'Nu se declara la RAR'"
def test_exclude_din_preview_marcheaza_randul_nedeclarat(client):
_seed()
iid = _upload_and_preview(client, rows=_ROWS_ITP)
assert _row_status(iid, 0) == "needs_mapping"
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/mapare-operatii", data={
"cod_op_service": "OP-ITP",
"cod_prestatie": "__NEDECLARAT__",
"csrf_token": csrf,
})
assert r.status_code == 200, r.text
assert "Excluse de la declarare: OP-ITP" in r.text
assert "Nedeclarat" in r.text
assert _row_status(iid, 0) == "excluded"
assert _row_status(iid, 1) == "ok"
# Operatia exclusa nu mai apare in panoul de mapat
assert "Operatii de mapat" not in r.text or "OP-ITP" not in r.text.split("Operatii de mapat")[1][:2000]
# Regula persistata
from app.db import get_connection
conn = get_connection()
try:
rule = conn.execute(
"SELECT cod_prestatie, exclus FROM operations_mapping WHERE account_id=1 AND cod_op_service='OP-ITP'"
).fetchone()
finally:
conn.close()
assert rule is not None and rule["exclus"] == 1 and rule["cod_prestatie"] == ""
def test_commit_sare_randurile_excluse(client):
_seed()
iid = _upload_and_preview(client, rows=_ROWS_ITP)
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/mapare-operatii", data={
"cod_op_service": "OP-ITP",
"cod_prestatie": "__NEDECLARAT__",
"csrf_token": csrf,
})
assert r.status_code == 200
# Doar randul OP-1 e "gata de trimis" -> n_confirmat=1
csrf = _get_csrf(client)
rc = client.post(f"/_import/{iid}/confirma", data={"n_confirmat": "1", "csrf_token": csrf})
assert rc.status_code == 200, rc.text
from app.db import get_connection
conn = get_connection()
try:
subs = conn.execute("SELECT payload_json FROM submissions WHERE batch_id=?", (iid,)).fetchall()
finally:
conn.close()
assert len(subs) == 1, "Doar randul declarabil se enqueue-uieste"
payload = json.loads(subs[0]["payload_json"])
assert payload["prestatii"][0]["cod_prestatie"] == "R-FRANE"
def test_rand_mixt_exclude_doar_operatia_exclusa(client):
"""Rand cu 2 operatii (una exclusa, una mapata) -> ok; payload fara cea exclusa."""
_seed()
rows = [{
"VIN": "WVWZZZ1KZAW000789",
"Nr": "B003TST",
"Data": "2026-06-12",
"KM": "90000",
"Operatie": "OP-ITP",
}]
iid = _upload_and_preview(client, rows=rows)
csrf = _get_csrf(client)
client.post(f"/_import/{iid}/mapare-operatii", data={
"cod_op_service": "OP-ITP", "cod_prestatie": "__NEDECLARAT__", "csrf_token": csrf,
})
assert _row_status(iid, 0) == "excluded"
# Editorul de rand: adauga explicit un cod suplimentar -> randul redevine declarabil
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={
"cod_prestatie": ["", "OE-2"],
"chip_op_service": ["OP-ITP", ""],
"chip_denumire": ["", ""],
})
assert r.status_code == 200, r.text
assert _row_status(iid, 0) == "ok"
csrf = _get_csrf(client)
rc = client.post(f"/_import/{iid}/confirma", data={"n_confirmat": "1", "csrf_token": csrf})
assert rc.status_code == 200, rc.text
from app.db import get_connection
conn = get_connection()
try:
sub = conn.execute("SELECT payload_json FROM submissions WHERE batch_id=?", (iid,)).fetchone()
finally:
conn.close()
payload = json.loads(sub["payload_json"])
coduri = [p.get("cod_prestatie") for p in payload["prestatii"]]
assert coduri == ["OE-2"], f"Operatia exclusa nu trebuie sa plece la RAR: {coduri}"
def test_cod_explicit_pe_operatie_bate_regula_de_excludere(client):
"""Chips cu cod explicit PE operatia exclusa -> codul ales de user castiga."""
_seed()
rows = [_ROWS_ITP[0]]
iid = _upload_and_preview(client, rows=rows)
csrf = _get_csrf(client)
client.post(f"/_import/{iid}/mapare-operatii", data={
"cod_op_service": "OP-ITP", "cod_prestatie": "__NEDECLARAT__", "csrf_token": csrf,
})
assert _row_status(iid, 0) == "excluded"
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={
"cod_prestatie": "OE-2",
"chip_op_service": "OP-ITP",
"chip_denumire": "Inspectie",
})
assert r.status_code == 200, r.text
assert _row_status(iid, 0) == "ok"
def test_mapari_tab_exclude_si_rerezolva_submissions_api(client):
"""Excluderea din tab-ul Mapari trece submission-urile blocate (canal API) pe needs_data."""
_seed()
# Submission blocat pe OP-XYZ (canal API, batch_id NULL)
r = client.post("/v1/prezentari", json={
"rar_credentials": {"email": "x@y.ro", "password": "s"},
"prezentari": [{
"vin": "WVWZZZ1KZAW000555",
"nr_inmatriculare": "B055TST",
"data_prestatie": "2026-06-20",
"odometru_final": "50000",
"prestatii": [{"cod_op_service": "OP-XYZ", "denumire": "Operatie interna"}],
}],
})
assert r.status_code == 200, r.text
sid = r.json()["results"][0]["submission_id"]
assert r.json()["results"][0]["status"] == "needs_mapping"
# Operatia apare la mapat
frag = client.get("/_fragments/mapari")
assert "OP-XYZ" in frag.text
csrf = _get_csrf(client)
resp = client.post("/mapari", data={
"cod_op_service": "OP-XYZ",
"cod_prestatie": "__NEDECLARAT__",
"csrf_token": csrf,
})
assert resp.status_code == 200, resp.text
assert "exclus de la declarare" in resp.text
from app.db import get_connection
conn = get_connection()
try:
row = conn.execute("SELECT status, rar_error FROM submissions WHERE id=?", (sid,)).fetchone()
# Operatia exclusa nu mai apare in pending
from app.mapping import pending_unmapped
pend = pending_unmapped(conn, 1)
finally:
conn.close()
assert row["status"] == "needs_data"
assert "excluse de la declarare" in (row["rar_error"] or "")
assert all(e["cod_op_service"] != "OP-XYZ" for e in pend)
def test_maparea_unei_operatii_excluse_reactiveaza_declararea(client):
"""save_mapping peste o regula de excludere reseteaza exclus=0."""
_seed()
from app.db import get_connection
from app.mapping import save_exclusion, load_excluded_ops, load_mapping
conn = get_connection()
try:
save_exclusion(conn, 1, "OP-ITP")
assert "OP-ITP" in load_excluded_ops(conn, 1)
from app.mapping import save_mapping
save_mapping(conn, 1, "OP-ITP", "OE-2", auto_send=False)
assert "OP-ITP" not in load_excluded_ops(conn, 1)
assert load_mapping(conn, 1)["OP-ITP"] == "OE-2"
finally:
conn.close()
def test_ingestie_api_cu_operatie_exclusa_nu_blocheaza(client):
"""POST /v1/prezentari cu operatie deja exclusa -> needs_data cu motiv, nu needs_mapping."""
_seed()
from app.db import get_connection
from app.mapping import save_exclusion
conn = get_connection()
try:
save_exclusion(conn, 1, "OP-ITP")
conn.commit()
finally:
conn.close()
r = client.post("/v1/prezentari", json={
"rar_credentials": {"email": "x@y.ro", "password": "s"},
"prezentari": [{
"vin": "WVWZZZ1KZAW000777",
"nr_inmatriculare": "B077TST",
"data_prestatie": "2026-06-21",
"odometru_final": "60000",
"prestatii": [{"cod_op_service": "OP-ITP", "denumire": "ITP"}],
}],
})
assert r.status_code == 200, r.text
rez = r.json()["results"][0]
assert rez["status"] == "needs_data", rez