Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes + seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap + telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil (a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR in loc sa tina randul pentru review. 814 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
268 lines
9.7 KiB
Python
268 lines
9.7 KiB
Python
"""US-003: Reguli text active la ingestie (API + import + corectie web) si la
|
|
re-rezolvarea blocajelor (`reresolve_account`).
|
|
|
|
Verifica ca o regula text salvata in prealabil rezolva o operatie fara mapare
|
|
exacta pe TOATE caile de ingestie, in loc sa o lase `needs_mapping`, si ca la
|
|
re-rezolvare un rand `needs_mapping` care acum da match pe o regula se deblocheaza.
|
|
|
|
Codul rezolvat din regula respecta validarea fata de nomenclator (US-002): folosim
|
|
`OE-2`, cod valid din seed-ul nomenclatorului.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
|
|
import openpyxl
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Fixtures #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@pytest.fixture()
|
|
def api_env(monkeypatch):
|
|
"""Client API + get_connection, DB temporara izolata (fara web-auth)."""
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "rr.db"))
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.db import get_connection
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c, get_connection
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def web_env(monkeypatch):
|
|
"""Client web (auth pornit) + get_connection, DB temporara izolata."""
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "rrw.db"))
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.web import ratelimit
|
|
ratelimit._hits.clear()
|
|
from app.db import get_connection
|
|
from app.main import app
|
|
with TestClient(app, follow_redirects=False) as c:
|
|
yield c, get_connection
|
|
ratelimit._hits.clear()
|
|
get_settings.cache_clear()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Helpere #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def _seed_text_rule(get_connection, account_id, pattern, cod, auto_send=True):
|
|
from app.mapping import save_text_rule
|
|
conn = get_connection()
|
|
try:
|
|
save_text_rule(conn, account_id, pattern, cod, auto_send=auto_send)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _make_xlsx(rows):
|
|
wb = openpyxl.Workbook()
|
|
ws = wb.active
|
|
ws.title = "Sheet1"
|
|
for row in rows:
|
|
ws.append(row)
|
|
buf = io.BytesIO()
|
|
wb.save(buf)
|
|
return buf.getvalue()
|
|
|
|
|
|
def _row_status_cod(get_connection, sub_id):
|
|
conn = get_connection()
|
|
try:
|
|
r = conn.execute(
|
|
"SELECT status, payload_json FROM submissions WHERE id=?", (sub_id,)
|
|
).fetchone()
|
|
payload = json.loads(r["payload_json"]) if r["payload_json"] else {}
|
|
cod = (payload.get("prestatii") or [{}])[0].get("cod_prestatie")
|
|
return r["status"], cod
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 1. Ingestie API (router.py -> classify_prezentare) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_ingestie_api_aplica_regula_text(api_env):
|
|
"""POST /v1/prezentari cu operatie fara mapare exacta dar match pe regula text
|
|
-> queued (cod din regula), nu needs_mapping."""
|
|
client, get_connection = api_env
|
|
_seed_text_rule(get_connection, 1, "verificare", "OE-2")
|
|
|
|
body = {
|
|
"rar_credentials": {"email": "x@y.ro", "password": "s"},
|
|
"prezentari": [{
|
|
"vin": "WVWZZZ1KZAW000123",
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"prestatii": [{"cod_op_service": "Verificare frane", "denumire": "Verificare frane"}],
|
|
}],
|
|
}
|
|
r = client.post("/v1/prezentari", json=body)
|
|
assert r.status_code == 200, r.text
|
|
res = r.json()["results"][0]
|
|
assert res["status"] == "queued", res
|
|
assert not res.get("nemapate")
|
|
|
|
status, cod = _row_status_cod(get_connection, res["submission_id"])
|
|
assert status == "queued"
|
|
assert cod == "OE-2"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 2. Ingestie import (import_router.py preview + commit) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_ingestie_import_aplica_regula_text(api_env):
|
|
"""Import xlsx cu operatie fara mapare exacta dar match pe regula text:
|
|
preview o marcheaza 'ok' si commit o pune 'queued' (cod din regula)."""
|
|
client, get_connection = api_env
|
|
_seed_text_rule(get_connection, 1, "verificare", "OE-2")
|
|
|
|
header = ["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Operatie"]
|
|
row = ["WVWZZZ1KZAW001111", "B100TST", "2026-06-15", "123456", "Verificare frane"]
|
|
data = _make_xlsx([header, row])
|
|
|
|
r = client.post(
|
|
"/v1/import",
|
|
files={"file": ("t.xlsx", io.BytesIO(data), "application/octet-stream")},
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
import_id = r.json()["import_id"]
|
|
|
|
rc = client.post(f"/v1/import/{import_id}/column-mapping", json={"json_mapare": {
|
|
"VIN": "vin",
|
|
"Nr inmatriculare": "nr_inmatriculare",
|
|
"Data prestatie": "data_prestatie",
|
|
"Odometru final": "odometru_final",
|
|
"Operatie": "operatie",
|
|
}})
|
|
assert rc.status_code == 200, rc.text
|
|
|
|
rp = client.get(f"/v1/import/{import_id}/preview")
|
|
assert rp.status_code == 200, rp.text
|
|
assert rp.json()["summary"].get("ok", 0) == 1, rp.json()["summary"]
|
|
|
|
rcommit = client.post(f"/v1/import/{import_id}/commit", json={
|
|
"n_confirmat": 1, "reviewed_rows": [],
|
|
})
|
|
assert rcommit.status_code == 200, rcommit.text
|
|
assert rcommit.json()["enqueued"] == 1
|
|
sub_id = rcommit.json()["submissions"][0]["submission_id"]
|
|
|
|
status, cod = _row_status_cod(get_connection, sub_id)
|
|
assert status == "queued"
|
|
assert cod == "OE-2"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 3. Corectie web (routes.py -> post_corectie_trimitere) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def _create_account_user(get_connection, email, name="Service", password="parolasecreta10"):
|
|
from app.accounts import create_account
|
|
from app.users import create_user
|
|
conn = get_connection()
|
|
try:
|
|
acct_id = create_account(conn, name, active=True)
|
|
create_user(conn, acct_id, email, password)
|
|
return acct_id
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _login(client, email, password="parolasecreta10"):
|
|
resp = client.get("/login")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
|
assert m
|
|
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
|
|
assert resp.status_code == 303
|
|
|
|
|
|
def _csrf(client):
|
|
resp = client.get("/?tab=acasa")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
|
assert m
|
|
return m.group(1)
|
|
|
|
|
|
def _insert_needs_mapping(get_connection, acct, op, denumire=None, batch_id=None):
|
|
conn = get_connection()
|
|
try:
|
|
payload = {
|
|
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B123ABC",
|
|
"data_prestatie": "2026-06-10", "odometru_final": "159004",
|
|
"prestatii": [{"cod_prestatie": None, "cod_op_service": op, "denumire": denumire or op}],
|
|
}
|
|
k = f"k-{os.urandom(6).hex()}"
|
|
cur = conn.execute(
|
|
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, batch_id) "
|
|
"VALUES (?, ?, 'needs_mapping', ?, ?, ?)",
|
|
(k, acct, json.dumps(payload), json.dumps({"unmapped": [{"cod_op_service": op}]}), batch_id),
|
|
)
|
|
conn.commit()
|
|
return int(cur.lastrowid)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_corectie_web_aplica_regula_text(web_env):
|
|
"""POST /trimitere/{id}/corecteaza pe un needs_mapping a carui operatie acum da
|
|
match pe o regula text -> randul intra 'queued' (cod din regula)."""
|
|
client, get_connection = web_env
|
|
acct = _create_account_user(get_connection, "cor@test.com")
|
|
sid = _insert_needs_mapping(get_connection, acct, op="Verificare frane")
|
|
_seed_text_rule(get_connection, acct, "verificare", "OE-2")
|
|
|
|
_login(client, "cor@test.com")
|
|
csrf = _csrf(client)
|
|
resp = client.post(f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf})
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
status, cod = _row_status_cod(get_connection, sid)
|
|
assert status == "queued"
|
|
assert cod == "OE-2"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 4. Re-rezolvare blocaje (mapping.reresolve_account) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_salvare_regula_rerezolva_blocate(api_env):
|
|
"""Dupa salvarea unei reguli noi, reresolve_account deblocheaza randurile
|
|
needs_mapping care acum dau match (acelasi mecanism ca la save_mapping)."""
|
|
client, get_connection = api_env
|
|
sid = _insert_needs_mapping(get_connection, 1, op="Verificare frane")
|
|
_seed_text_rule(get_connection, 1, "verificare", "OE-2")
|
|
|
|
from app.mapping import reresolve_account
|
|
conn = get_connection()
|
|
try:
|
|
stats = reresolve_account(conn, 1)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
assert stats["requeued"] == 1, stats
|
|
status, cod = _row_status_cod(get_connection, sid)
|
|
assert status == "queued"
|
|
assert cod == "OE-2"
|