feat(editor): editor prestatii unificat in modal - un select, un rand de chips (PRD 5.23)
Inlocuieste cele 3 controale de asociere cod RAR cu un singur rand de chips + un singur select care adauga instant la change. Stare = un hidden chips_state JSON versionat; post_form_chips redus la 2 actiuni (add/remove). Optgroup Sugestii (fuzzy/k-NN) + optiune "Nu se declara la RAR" in select, cu tinta implicita evidentiata si placeholder care o numeste. exclus persistat prin payload_json (treapta noua de precedenta in resolve_prestatii, round-trip complet). Siguranta: itemii exclusi sunt scosi din payload la momentul trimiterii (worker split_prestatii_excluse inainte de build_rar_payload + filtru defensiv), ca sa nu ajunga NICIODATA la RAR ca codPrestatie:null. Suita: 1680 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
tests/test_chips_target_robustete.py
Normal file
78
tests/test_chips_target_robustete.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Robustete selectie tinta chips: doar operatii nemapate IDENTIFICABILE (cu
|
||||
cod_op_service) pot deveni tinta. Un item fara cod_op_service (chip liber, fara
|
||||
nicio operatie asociata) nu are cum sa fie afisat ca tinta in template (chip-ul
|
||||
warning se randeaza doar cand cod_op_service e truthy) — daca ar fi ales tinta,
|
||||
ar deveni invizibil: userul nu vede niciun highlight, dar selectul tot arata
|
||||
sugestii/NEDECLARAT pentru un "cui" pe care nu-l poate vedea.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def conn(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.db import get_connection, init_db
|
||||
init_db()
|
||||
c = get_connection()
|
||||
yield c
|
||||
c.close()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def _seed_cod(conn, cod: str, nume: str) -> None:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
||||
(cod, nume),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_item_fara_cod_op_service_nu_devine_tinta(conn):
|
||||
"""Chip liber (fara cod_op_service) nemapat nu poate fi tinta — e invizibil in template."""
|
||||
from app.web.routes import _chips_target_si_sugestii
|
||||
_seed_cod(conn, "OE-1", "Schimb ulei")
|
||||
from app.mapping import load_nomenclator
|
||||
nomenclator = load_nomenclator(conn)
|
||||
|
||||
chips = [{"cod_prestatie": "", "cod_op_service": "", "denumire": "", "exclus": 0}]
|
||||
target_index, sugestii = _chips_target_si_sugestii(conn, chips, nomenclator)
|
||||
|
||||
assert target_index is None
|
||||
assert sugestii == []
|
||||
|
||||
|
||||
def test_operatie_identificabila_ramane_tinta(conn):
|
||||
"""Regresie: o operatie nemapata cu cod_op_service ramane tinta normal."""
|
||||
from app.web.routes import _chips_target_si_sugestii
|
||||
_seed_cod(conn, "OE-1", "Schimb ulei motor")
|
||||
from app.mapping import load_nomenclator
|
||||
nomenclator = load_nomenclator(conn)
|
||||
|
||||
chips = [{"cod_prestatie": "", "cod_op_service": "Op-A", "denumire": "Schimb ulei motor", "exclus": 0}]
|
||||
target_index, sugestii = _chips_target_si_sugestii(conn, chips, nomenclator)
|
||||
|
||||
assert target_index == 0
|
||||
assert any(s.get("cod_prestatie") == "OE-1" for s in sugestii)
|
||||
|
||||
|
||||
def test_sugestii_pentru_tinta_fara_denumire_intoarce_gol():
|
||||
"""Denumire (si cod_op_service) goala pe tinta -> lista goala, fara sugestii irelevante."""
|
||||
from app.web.routes import _sugestii_select_pentru_tinta
|
||||
|
||||
class _FakeConn:
|
||||
pass
|
||||
|
||||
nomenclator = [{"cod_prestatie": "OE-1", "nume_prestatie": "Schimb ulei"}]
|
||||
out = _sugestii_select_pentru_tinta(_FakeConn(), "", nomenclator)
|
||||
assert out == []
|
||||
out_none = _sugestii_select_pentru_tinta(_FakeConn(), None, nomenclator)
|
||||
assert out_none == []
|
||||
@@ -8,8 +8,10 @@ Un cod ales explicit pe rand bate regula de excludere.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
@@ -191,8 +193,72 @@ def test_rand_mixt_exclude_doar_operatia_exclusa(client):
|
||||
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}"
|
||||
# Round-trip: itemul exclus RAMANE in payload_json (persistat), adnotat exclus,
|
||||
# dar fara cod_prestatie — nu pleaca niciodata la RAR (vezi build_rar_payload).
|
||||
by_op = {p.get("cod_op_service"): p for p in payload["prestatii"] if p.get("cod_op_service")}
|
||||
assert by_op["OP-ITP"]["exclus"] is True
|
||||
assert not by_op["OP-ITP"].get("cod_prestatie")
|
||||
coduri_declarabile = [p.get("cod_prestatie") for p in payload["prestatii"] if not p.get("exclus")]
|
||||
assert coduri_declarabile == ["OE-2"], f"Operatia exclusa nu trebuie sa plece la RAR: {coduri_declarabile}"
|
||||
|
||||
|
||||
def test_reupload_rand_cu_exclus_deja_comis_e_already_sent(client):
|
||||
"""Cheia de idempotenta la commit (doar declarabile) e IDENTICA cu preview.
|
||||
|
||||
Re-incarcarea aceluiasi rand (dupa ce a fost deja comis cu un item exclus
|
||||
adnotat in payload) trebuie recunoscuta drept duplicat — nu un rand nou.
|
||||
"""
|
||||
_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,
|
||||
})
|
||||
client.post(f"/_import/{iid}/rand/0/editeaza", data={
|
||||
"cod_prestatie": ["", "OE-2"],
|
||||
"chip_op_service": ["OP-ITP", ""],
|
||||
"chip_denumire": ["", ""],
|
||||
})
|
||||
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
|
||||
|
||||
# Re-incarca acelasi rand + acelasi cod ales pe editorul de rand. Maparea de
|
||||
# coloane e deja memorata din primul upload -> raspunsul sare direct la preview
|
||||
# (fara link mapare-coloane); extragem import_id din hx-get pe #import-section.
|
||||
csv_data = _csv_bytes(rows)
|
||||
csrf = _get_csrf(client)
|
||||
r2 = client.post(
|
||||
"/_import/upload",
|
||||
files={"file": ("test.csv", io.BytesIO(csv_data), "text/csv")},
|
||||
data={"csrf_token": csrf},
|
||||
)
|
||||
assert r2.status_code == 200, r2.text
|
||||
m2 = re.search(r"/_import/(\d+)/preview", r2.text)
|
||||
assert m2, f"import_id negasit in raspunsul de reupload: {r2.text[:300]}"
|
||||
iid2 = int(m2.group(1))
|
||||
csrf = _get_csrf(client)
|
||||
client.post(f"/_import/{iid2}/mapare-operatii", data={
|
||||
"cod_op_service": "OP-ITP", "cod_prestatie": "__NEDECLARAT__", "csrf_token": csrf,
|
||||
})
|
||||
client.post(f"/_import/{iid2}/rand/0/editeaza", data={
|
||||
"cod_prestatie": ["", "OE-2"],
|
||||
"chip_op_service": ["OP-ITP", ""],
|
||||
"chip_denumire": ["", ""],
|
||||
})
|
||||
# already_sent se calculeaza doar la refresh-ul complet de preview (lookup pe
|
||||
# tot batch-ul), nu la editarea unui singur rand — reincarcam preview-ul.
|
||||
rp = client.get(f"/_import/{iid2}/preview")
|
||||
assert rp.status_code == 200
|
||||
assert "Deja trimis" in rp.text, \
|
||||
"Randul re-incarcat trebuie recunoscut deja trimis (cheia de idempotenta neschimbata)"
|
||||
|
||||
|
||||
def test_cod_explicit_pe_operatie_bate_regula_de_excludere(client):
|
||||
|
||||
@@ -100,3 +100,35 @@ def test_normalizare_vin_nrinm_in_payload():
|
||||
|
||||
def test_sistem_reparat_default_null():
|
||||
assert build_rar_payload(_internal())["sistemReparat"] == "null"
|
||||
|
||||
|
||||
def test_item_exclus_nu_apare_in_payload():
|
||||
"""Un item adnotat exclus=True nu produce niciodata {codPrestatie: null} la RAR."""
|
||||
internal = {
|
||||
"vin": "WVWZZZ1KZAW000123",
|
||||
"nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15",
|
||||
"odometru_final": "123456",
|
||||
"prestatii": [
|
||||
{"cod_prestatie": "OE-1"},
|
||||
{"cod_prestatie": None, "cod_op_service": "200", "denumire": "Spalare", "exclus": True},
|
||||
],
|
||||
}
|
||||
p = build_rar_payload(internal)
|
||||
assert p["prestatii"] == [{"codPrestatie": "OE-1", "idPrezentare": None}]
|
||||
|
||||
|
||||
def test_item_fara_cod_prestatie_nu_apare_in_payload():
|
||||
"""Defense-in-depth: orice item fara cod (chiar neadnotat exclus) e sarit, niciodata null."""
|
||||
internal = {
|
||||
"vin": "WVWZZZ1KZAW000123",
|
||||
"nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15",
|
||||
"odometru_final": "123456",
|
||||
"prestatii": [
|
||||
{"cod_prestatie": "OE-1"},
|
||||
{"cod_prestatie": None, "cod_op_service": "200"},
|
||||
],
|
||||
}
|
||||
p = build_rar_payload(internal)
|
||||
assert p["prestatii"] == [{"codPrestatie": "OE-1", "idPrezentare": None}]
|
||||
|
||||
745
tests/test_web_chips.py
Normal file
745
tests/test_web_chips.py
Normal file
@@ -0,0 +1,745 @@
|
||||
"""US-001: stare chips extinsa (JSON versionat, D4) + actiuni add/remove unificate.
|
||||
|
||||
Contract nou /form-chips:
|
||||
- Stare curenta: UN SINGUR hidden `chips_state` = JSON {"v":1,"items":[{"cod","op","den","exclus"}]}.
|
||||
- chips_action=add + chips_pick (codul ales) + chips_target_index optional:
|
||||
cu tinta -> leaga codul de operatia de la acel index; fara tinta -> chip liber.
|
||||
chips_pick=__NEDECLARAT__ + tinta valida -> item exclus=1, fara cod_prestatie.
|
||||
- chips_action=remove + chips_remove_index: chip legat de operatie -> revine warning
|
||||
(cod_prestatie="", exclus=0); chip liber -> iese din lista.
|
||||
- Actiunile vechi (add_flat/add_extra/remove_flat/chips_add_cod_{i}/chips_add_cod_flat
|
||||
ca actiuni) sunt ELIMINATE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fixtures #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "chips.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.main import app
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c
|
||||
ratelimit._hits.clear()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def _create_account_user(email: str, password: str = "parolasecreta10"):
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
acct_id = create_account(conn, "Service", active=True)
|
||||
create_user(conn, acct_id, email, password)
|
||||
return acct_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
||||
resp = client.get("/login")
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
||||
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
||||
assert m, "csrf_token nu gasit in login"
|
||||
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
|
||||
assert resp.status_code == 303
|
||||
|
||||
|
||||
def _csrf(client) -> str:
|
||||
resp = client.get("/?tab=coada")
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
||||
assert m, "csrf_token nu gasit in dashboard"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def _seed_cod(cod: str, denumire: str = "Prestatie test") -> None:
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
||||
(cod, denumire),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _state(items: list[dict]) -> str:
|
||||
"""Construieste JSON-ul hidden chips_state din itemi scurti {cod,op,den,exclus}."""
|
||||
return json.dumps({"v": 1, "items": items}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _login_seed(client, email: str) -> None:
|
||||
_create_account_user(email)
|
||||
_login(client, email)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# add cu tinta -> leaga codul de operatia respectiva #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_add_cu_tinta_leaga_de_operatie(client):
|
||||
_login_seed(client, "add.tinta@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "add",
|
||||
"chips_target_index": "0",
|
||||
"chips_pick": "OE-1",
|
||||
})
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
assert "OE-1" in html
|
||||
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m, f"hidden chips_state lipsa din raspuns: {html[:400]}"
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["v"] == 1
|
||||
items = payload["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["cod"] == "OE-1"
|
||||
assert items[0]["op"] == "Op-A"
|
||||
assert items[0]["exclus"] == 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# add fara tinta -> chip liber #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_add_fara_tinta_adauga_cod_liber(client):
|
||||
_login_seed(client, "add.liber@test.com")
|
||||
_seed_cod("FRN1", "Sistem de franare")
|
||||
csrf = _csrf(client)
|
||||
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": _state([]),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "FRN1",
|
||||
})
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
items = payload["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["cod"] == "FRN1"
|
||||
assert items[0]["op"] == ""
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# add __NEDECLARAT__ cu tinta -> marcheaza exclus #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_add_nedeclarat_marcheaza_exclus(client):
|
||||
_login_seed(client, "add.ned@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "", "op": "OP-ITP", "den": "Inspectie tehnica", "exclus": 0},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "add",
|
||||
"chips_target_index": "0",
|
||||
"chips_pick": "__NEDECLARAT__",
|
||||
})
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
items = payload["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["exclus"] == 1
|
||||
assert items[0]["cod"] == "", f"item exclus nu trebuie sa aiba cod_prestatie: {items[0]}"
|
||||
assert items[0]["op"] == "OP-ITP"
|
||||
assert "nu se va declara" in resp.text.lower()
|
||||
|
||||
|
||||
def test_add_nedeclarat_fara_tinta_da_eroare(client):
|
||||
"""Sentinelul __NEDECLARAT__ fara tinta valida -> chips_error explicit, fara modificare."""
|
||||
_login_seed(client, "add.ned.notinta@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": _state([]),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "__NEDECLARAT__",
|
||||
# fara chips_target_index
|
||||
})
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
assert "chips-extra-error" in resp.text, (
|
||||
f"Eroare vizibila lipsa pentru NEDECLARAT fara tinta: {resp.text[:400]}"
|
||||
)
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["items"] == [], "NEDECLARAT fara tinta nu trebuie sa modifice starea"
|
||||
|
||||
|
||||
def test_add_nedeclarat_tinta_deja_mapata_da_eroare(client):
|
||||
"""Tinta trebuie sa fie o operatie NEMAPATA — index spre operatie deja rezolvata -> eroare."""
|
||||
_login_seed(client, "add.ned.mapata@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "add",
|
||||
"chips_target_index": "0",
|
||||
"chips_pick": "__NEDECLARAT__",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "chips-extra-error" in resp.text
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["items"][0]["cod"] == "OE-1", "Tinta invalida nu trebuie sa strice starea existenta"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# remove -> readuce operatia nemapata (warning) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_remove_cod_readuce_operatia_nemapata(client):
|
||||
_login_seed(client, "remove.readuce@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "remove",
|
||||
"chips_remove_index": "0",
|
||||
})
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
items = payload["items"]
|
||||
assert len(items) == 1, "Operatia trebuie sa RAMANA (doar codul dispare)"
|
||||
assert items[0]["cod"] == ""
|
||||
assert items[0]["op"] == "Op-A"
|
||||
assert items[0]["exclus"] == 0
|
||||
|
||||
|
||||
def test_remove_pe_nedeclarat_readuce_warning(client):
|
||||
"""x pe un chip Nedeclarat readuce operatia la warning (nu se sterge operatia)."""
|
||||
_login_seed(client, "remove.nedeclarat@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "", "op": "OP-ITP", "den": "Inspectie", "exclus": 1},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "remove",
|
||||
"chips_remove_index": "0",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
items = payload["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["exclus"] == 0
|
||||
assert items[0]["cod"] == ""
|
||||
assert items[0]["op"] == "OP-ITP"
|
||||
|
||||
|
||||
def test_remove_cod_liber_il_sterge_complet(client):
|
||||
"""Chip liber (fara operatie sursa) e eliminat integral din lista la remove."""
|
||||
_login_seed(client, "remove.liber@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "FRN1", "op": "", "den": "", "exclus": 0},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "remove",
|
||||
"chips_remove_index": "0",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["items"] == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# dedup (operatie, cod) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_dedup_operatie_cod(client):
|
||||
"""Acelasi cod liber nu se adauga de doua ori (pereche op='',cod deja existenta)."""
|
||||
_login_seed(client, "dedup.op.cod@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([
|
||||
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
||||
])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": "add",
|
||||
"chips_pick": "OE-1",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "chips-extra-error" in resp.text or "deja in lista" in resp.text
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert len(payload["items"]) == 1, f"Dedup a esuat, itemi: {payload['items']}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# actiuni vechi eliminate #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@pytest.mark.parametrize("actiune", ["add_flat", "add_extra", "remove_flat"])
|
||||
def test_actiuni_vechi_eliminate_sunt_no_op(client, actiune):
|
||||
"""Actiunile eliminate nu mai au ramura dedicata — cad prin fara sa schimbe starea."""
|
||||
_login_seed(client, f"vechi.{actiune}@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
state = _state([{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0}])
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": state,
|
||||
"chips_action": actiune,
|
||||
"chips_add_cod_flat": "FRN1",
|
||||
"chips_remove_cod": "OE-1",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["items"] == [{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0}]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# chips_state prezent mereu, inclusiv gol #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_chips_state_gol_ramane_prezent_in_raspuns(client):
|
||||
_login_seed(client, "state.gol@test.com")
|
||||
csrf = _csrf(client)
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": _state([]),
|
||||
"chips_action": "",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m, "hidden chips_state trebuie sa fie mereu prezent, chiar cu lista goala"
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload == {"v": 1, "items": []}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# JSON invalid -> eroare vizibila, nu stare goala silentioasa #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_chips_state_json_invalid_da_eroare_vizibila(client):
|
||||
_login_seed(client, "state.corupt@test.com")
|
||||
csrf = _csrf(client)
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
"chips_state": "{not valid json",
|
||||
"chips_action": "",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "chips-extra-error" in resp.text or "invalid" in resp.text.lower(), (
|
||||
f"JSON corupt trebuie sa dea eroare vizibila: {resp.text[:400]}"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# compat tranzitorie: chips_state lipsa -> fallback pe listele vechi #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_compat_fallback_pe_liste_vechi_cand_chips_state_lipseste(client):
|
||||
_login_seed(client, "compat.fallback@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
resp = client.post("/form-chips", data={
|
||||
"csrf_token": csrf,
|
||||
# FARA chips_state -> fallback pe listele vechi
|
||||
"cod_prestatie": [""],
|
||||
"chip_op_service": ["Op-A"],
|
||||
"chip_denumire": ["Schimb ulei motor"],
|
||||
"chips_action": "add",
|
||||
"chips_target_index": "0",
|
||||
"chips_pick": "OE-1",
|
||||
})
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["items"][0]["cod"] == "OE-1"
|
||||
assert payload["items"][0]["exclus"] == 0
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-002: template unificat -- un container .chips, un select, fara op-row #
|
||||
# =========================================================================== #
|
||||
|
||||
|
||||
def _post_chips(client, csrf, items, **extra):
|
||||
data = {
|
||||
"csrf_token": csrf,
|
||||
"chips_state": _state(items),
|
||||
"chips_action": "",
|
||||
}
|
||||
data.update(extra)
|
||||
return client.post("/form-chips", data=data)
|
||||
|
||||
|
||||
def test_render_un_singur_container_chips(client):
|
||||
"""UN singur container `.chips` -- indiferent de mixul de tipuri de chip."""
|
||||
_login_seed(client, "un.container@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
||||
{"cod": "", "op": "Op-B", "den": "Vulcanizare", "exclus": 0},
|
||||
{"cod": "FRN1", "op": "", "den": "", "exclus": 0},
|
||||
{"cod": "", "op": "Op-C", "den": "ITP anual", "exclus": 1},
|
||||
]
|
||||
_seed_cod("FRN1", "Sistem de franare")
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert html.count('class="chips"') == 1, (
|
||||
f"Trebuie sa existe UN SINGUR container .chips, gasit {html.count('class=\"chips\"')}: {html[:800]}"
|
||||
)
|
||||
|
||||
|
||||
def test_render_fara_op_row_si_fara_butoane_adauga(client):
|
||||
"""Fara .op-row, fara butoane Adauga/+, fara actiunile vechi in markup."""
|
||||
_login_seed(client, "fara.oprow@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
||||
{"cod": "", "op": "Op-B", "den": "Vulcanizare", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert "op-row" not in html
|
||||
assert "Adauga" not in html
|
||||
assert "add_flat" not in html
|
||||
assert "chips_add_cod_" not in html
|
||||
|
||||
|
||||
def test_chip_warning_pentru_operatie_nemapata(client):
|
||||
"""Operatie nemapata -> chip warning `warn OP-X . fara cod` cu title complet."""
|
||||
_login_seed(client, "chip.warning@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "", "op": "OP-ITP", "den": "Inspectie tehnica periodica", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert "OP-ITP" in html
|
||||
assert "fara cod" in html
|
||||
assert 'title="OP-ITP' in html
|
||||
|
||||
|
||||
def test_select_are_hx_disabled_elt_this(client):
|
||||
"""Selectul unic are hx-disabled-elt='this' EXPLICIT (capcana 5.22 -- nu mostenit de la form)."""
|
||||
_login_seed(client, "select.disabled@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
resp = _post_chips(client, csrf, [])
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
m = re.search(r'<select[^>]*name="chips_pick"[^>]*>', html)
|
||||
assert m, f"selectul chips_pick lipseste: {html[:600]}"
|
||||
select_tag = m.group(0)
|
||||
assert 'id="chips-picker"' in select_tag
|
||||
assert 'hx-trigger="change"' in select_tag
|
||||
assert 'hx-post="/form-chips"' in select_tag
|
||||
assert 'hx-target="#chips-section"' in select_tag
|
||||
assert 'hx-swap="outerHTML"' in select_tag
|
||||
assert 'hx-include="closest form"' in select_tag
|
||||
assert 'hx-disabled-elt="this"' in select_tag, (
|
||||
f"hx-disabled-elt='this' lipseste de pe select (capcana 5.22): {select_tag}"
|
||||
)
|
||||
|
||||
|
||||
def test_mod_plat_fara_operatii_select_doar_coduri_libere(client):
|
||||
"""Mod plat (zero operatii din import): doar chips libere + select placeholder generic."""
|
||||
_login_seed(client, "mod.plat@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert "⚠" not in html, "Mod plat nu trebuie sa aiba niciun chip warning"
|
||||
m = re.search(r'<select[^>]*name="chips_pick"[^>]*>\s*<option value="">([^<]*)</option>', html)
|
||||
assert m, f"select cu optiune placeholder lipseste: {html[:600]}"
|
||||
assert "adauga cod RAR" in m.group(1)
|
||||
|
||||
|
||||
def test_has_r_odo_pastrat_dupa_actiuni(client):
|
||||
"""has_r_odo / data-has-r-odo + chip-warn pe R-ODO/I-ODO raman neschimbate."""
|
||||
_login_seed(client, "r.odo@test.com")
|
||||
_seed_cod("R-ODO", "Odometru initial")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "R-ODO", "op": "", "den": "", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert 'data-has-r-odo="true"' in html
|
||||
assert "chip-warn" in html
|
||||
assert "R-ODO" in html
|
||||
|
||||
|
||||
def test_chip_nedeclarat_are_x_functional(client):
|
||||
"""Item exclus=1 -> chip Nedeclarat (line-through) cu buton de sters functional (remove -> warning)."""
|
||||
_login_seed(client, "nedeclarat.x@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "", "op": "OP-ITP", "den": "Inspectie", "exclus": 1},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert "Nedeclarat" in html
|
||||
assert 'hx-vals=\'{"chips_action":"remove","chips_remove_index":0}\'' in html
|
||||
|
||||
|
||||
def test_chips_submission_id_ecouat(client):
|
||||
"""chips_submission_id trimis in form e ecouat ca hidden in raspunsul /form-chips."""
|
||||
_login_seed(client, "sub.id.ecou@test.com")
|
||||
csrf = _csrf(client)
|
||||
|
||||
resp = _post_chips(client, csrf, [], chips_submission_id="4242")
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
assert re.search(r'name="chips_submission_id"\s+value="4242"', resp.text), (
|
||||
f"chips_submission_id nu a fost ecouat: {resp.text[:600]}"
|
||||
)
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-003: sugestii fuzzy/k-NN + optiunea "Nu se declara la RAR" in select #
|
||||
# =========================================================================== #
|
||||
|
||||
|
||||
def _extrage_select(html: str) -> str:
|
||||
"""Extrage markup-ul complet al <select name="chips_pick">...</select>."""
|
||||
m = re.search(r'<select[^>]*name="chips_pick".*?</select>', html, re.DOTALL)
|
||||
assert m, f"select chips_pick lipseste din raspuns: {html[:600]}"
|
||||
return m.group(0)
|
||||
|
||||
|
||||
def test_select_are_optgroup_sugestii_pt_tinta(client):
|
||||
"""Cu tinta activa (operatie nemapata): optgroup 'Sugestii' + __NEDECLARAT__ + nomenclator."""
|
||||
_login_seed(client, "sugestii.tinta@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
_seed_cod("FRN1", "Sistem de franare")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
select_html = _extrage_select(resp.text)
|
||||
|
||||
assert '<optgroup label="Sugestii">' in select_html, (
|
||||
f"optgroup Sugestii lipseste cu tinta activa: {select_html[:600]}"
|
||||
)
|
||||
assert "OE-1" in select_html.split('<optgroup label="Sugestii">')[1].split("</optgroup>")[0], (
|
||||
"sugestia fuzzy pentru 'Schimb ulei motor' trebuie sa contina OE-1"
|
||||
)
|
||||
assert "__NEDECLARAT__" in select_html
|
||||
assert "Nu se declara la RAR" in select_html
|
||||
# Nomenclatorul complet ramane dupa Sugestii/NEDECLARAT.
|
||||
assert "FRN1" in select_html
|
||||
|
||||
|
||||
def test_select_are_nedeclarat_doar_cu_tinta(client):
|
||||
"""Optiunea __NEDECLARAT__ apare DOAR cand exista o tinta (operatie nemapata) activa."""
|
||||
_login_seed(client, "nedeclarat.doar.tinta@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
# Cu tinta (operatie nemapata) -> NEDECLARAT prezent.
|
||||
resp_cu_tinta = _post_chips(client, csrf, [
|
||||
{"cod": "", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
||||
])
|
||||
assert "__NEDECLARAT__" in _extrage_select(resp_cu_tinta.text)
|
||||
|
||||
# Fara tinta (doar cod liber, nicio operatie nemapata) -> NEDECLARAT absent.
|
||||
resp_fara_tinta = _post_chips(client, csrf, [
|
||||
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
||||
])
|
||||
assert "__NEDECLARAT__" not in _extrage_select(resp_fara_tinta.text)
|
||||
|
||||
|
||||
def test_select_fara_tinta_fara_sugestii(client):
|
||||
"""Fara tinta: selectul listeaza DOAR nomenclatorul (fara optgroup Sugestii, fara NEDECLARAT)."""
|
||||
_login_seed(client, "fara.tinta.fara.sugestii@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
csrf = _csrf(client)
|
||||
|
||||
items = [
|
||||
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
select_html = _extrage_select(resp.text)
|
||||
|
||||
assert "<optgroup" not in select_html
|
||||
assert "__NEDECLARAT__" not in select_html
|
||||
assert "OE-1" in select_html
|
||||
|
||||
|
||||
def test_sugestii_indisponibile_degradare_fara_optgroup(client, monkeypatch):
|
||||
"""Motor de sugestii care arunca exceptie -> select DOAR cu nomenclatorul (fara optgroup),
|
||||
fara eroare 500 si fara blocare (degradare gratioasa, GAP inchis din PRD 5.23)."""
|
||||
_login_seed(client, "sugestii.indisponibile@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
csrf = _csrf(client)
|
||||
|
||||
from app.web import routes as _routes
|
||||
|
||||
def _boom(*args, **kwargs):
|
||||
raise RuntimeError("motor de sugestii indisponibil (simulat)")
|
||||
|
||||
monkeypatch.setattr(_routes, "suggest_codes", _boom)
|
||||
monkeypatch.setattr(_routes, "enrich_suggestions", _boom)
|
||||
|
||||
items = [
|
||||
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
||||
]
|
||||
resp = _post_chips(client, csrf, items)
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
select_html = _extrage_select(resp.text)
|
||||
|
||||
assert "<optgroup" not in select_html, (
|
||||
f"optgroup nu trebuie sa apara cand motorul de sugestii arunca exceptie: {select_html[:600]}"
|
||||
)
|
||||
assert "OE-1" in select_html, "nomenclatorul complet trebuie sa ramana disponibil"
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# FIX: tinta implicita + sugestii la RANDAREA INITIALA a modalului (fara #
|
||||
# niciun round-trip /form-chips) — invariant PRD 5.23 §3.1. #
|
||||
# =========================================================================== #
|
||||
|
||||
def _insert_submission(acct: int, *, status: str, payload: dict) -> int:
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(f"k-{os.urandom(6).hex()}", acct, status, json.dumps(payload)),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_randare_initiala_are_tinta_si_optgroup(client):
|
||||
"""Deschiderea INITIALA a modalului de detaliu (needs_mapping, operatie nemapata),
|
||||
FARA niciun /form-chips, trebuie sa aiba deja is-target pe chip-ul warning,
|
||||
placeholder-ul D2 "Cod pentru OP-X — alege" si optgroup-ul Sugestii + NEDECLARAT."""
|
||||
acct = _login_seed_ret(client, "randare.initiala@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
_seed_cod("FRN1", "Sistem de franare")
|
||||
|
||||
op = "Schimb ulei motor"
|
||||
sid = _insert_submission(acct, status="needs_mapping", payload={
|
||||
"vin": "WVWZZZ1JZXW0SC001",
|
||||
"nr_inmatriculare": "B100AAA",
|
||||
"data_prestatie": "2026-06-10",
|
||||
"odometru_final": "50000",
|
||||
"prestatii": [{"cod_op_service": op, "denumire": op}],
|
||||
})
|
||||
|
||||
resp = client.get(f"/_fragments/trimitere/{sid}")
|
||||
assert resp.status_code == 200, resp.text[:400]
|
||||
html = resp.text
|
||||
|
||||
assert "is-target" in html, (
|
||||
f"chip-ul warning al primei operatii nemapate trebuie sa fie tinta la deschiderea initiala: {html[:2000]}"
|
||||
)
|
||||
assert f"Cod pentru {op} — alege" in html, (
|
||||
f"placeholder-ul D2 trebuie sa numeasca operatia tinta la deschiderea initiala: {html[:2000]}"
|
||||
)
|
||||
select_html = _extrage_select(html)
|
||||
assert '<optgroup label="Sugestii">' in select_html, (
|
||||
f"optgroup Sugestii trebuie sa apara la deschiderea initiala (fara /form-chips): {select_html[:600]}"
|
||||
)
|
||||
assert "OE-1" in select_html.split('<optgroup label="Sugestii">')[1].split("</optgroup>")[0], (
|
||||
"sugestia fuzzy pentru 'Schimb ulei motor' trebuie sa contina OE-1 chiar la randarea initiala"
|
||||
)
|
||||
assert "__NEDECLARAT__" in select_html
|
||||
|
||||
|
||||
def _login_seed_ret(client, email: str) -> int:
|
||||
"""Ca `_login_seed`, dar intoarce account_id (necesar pentru inserarea directa a submission-ului)."""
|
||||
acct = _create_account_user(email)
|
||||
_login(client, email)
|
||||
return acct
|
||||
@@ -535,8 +535,8 @@ def test_picker_flat_arata_cod_si_denumire(client):
|
||||
|
||||
|
||||
def test_adauga_cod_extra_in_mod_operatii(client):
|
||||
"""In mod operatii, actiunea add_extra adauga un cod RAR liber."""
|
||||
acct = _create_account_user("add.extra.ops@test.com")
|
||||
"""In mod operatii, actiunea unificata 'add' fara tinta adauga un cod RAR liber."""
|
||||
_create_account_user("add.extra.ops@test.com")
|
||||
_login(client, "add.extra.ops@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
_seed_cod("FRN1", "Sistem de franare")
|
||||
@@ -547,11 +547,11 @@ def test_adauga_cod_extra_in_mod_operatii(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1"], # chip existent (op mapata)
|
||||
"chip_op_service": ["SchimbUlei"],
|
||||
"chip_denumire": ["Schimb ulei motor"],
|
||||
"chips_action": "add_extra",
|
||||
"chips_add_cod_flat": "FRN1", # codul extra de adaugat
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "SchimbUlei", "den": "Schimb ulei motor", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "FRN1", # codul extra de adaugat, fara tinta -> chip liber
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text[:300]
|
||||
@@ -601,32 +601,36 @@ def test_extra_cod_persistat_la_salvare(client):
|
||||
|
||||
|
||||
def test_extra_cod_validat_nomenclator(client):
|
||||
"""add_extra respinge cod necunoscut in nomenclator (invariant ORA-12899)."""
|
||||
acct = _create_account_user("extra.valid@test.com")
|
||||
"""'add' fara tinta respinge cod necunoscut in nomenclator (invariant ORA-12899)."""
|
||||
_create_account_user("extra.valid@test.com")
|
||||
_login(client, "extra.valid@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
# add_extra cu cod INVALID (XX-99 nu e in nomenclator)
|
||||
# add fara tinta cu cod INVALID (XX-99 nu e in nomenclator)
|
||||
resp = client.post(
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1"],
|
||||
"chip_op_service": ["SchimbUlei"],
|
||||
"chip_denumire": ["Schimb ulei"],
|
||||
"chips_action": "add_extra",
|
||||
"chips_add_cod_flat": "XX-99", # cod necunoscut
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "SchimbUlei", "den": "Schimb ulei", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "XX-99", # cod necunoscut
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
# XX-99 NU trebuie sa apara ca chip valid (hidden input cu valoarea XX-99)
|
||||
# XX-99 NU trebuie sa apara in starea persistata (chips_state)
|
||||
import re as _re
|
||||
hidden_xx99 = _re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="XX-99"', html)
|
||||
assert hidden_xx99 is None, (
|
||||
f"Codul invalid XX-99 a fost adaugat ca chip! HTML: {html[:500]}"
|
||||
m = _re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
coduri = [it["cod"] for it in payload["items"]]
|
||||
assert "XX-99" not in coduri, (
|
||||
f"Codul invalid XX-99 a fost adaugat ca chip! items={payload['items']}"
|
||||
)
|
||||
assert "chips-extra-error" in html
|
||||
|
||||
|
||||
def test_cod_ales_in_picker_se_salveaza_fara_buton_add(client):
|
||||
@@ -703,12 +707,12 @@ def test_salvare_fara_chip_explicit_nu_e_no_op(client):
|
||||
|
||||
|
||||
def test_picker_by_index_op2_nu_op1(client):
|
||||
"""Codul ales pe picker-ul op#2 aterizeaza pe op#2, NU pe op#1.
|
||||
"""Codul ales pentru op#2 aterizeaza pe op#2, NU pe op#1.
|
||||
|
||||
Verifica alinierea by-index in modul operatii: chips_add_op_index=1 + chips_add_cod_1
|
||||
actualizeaza chips[1] (op#2), nu chips[0] (op#1).
|
||||
Verifica alinierea by-index in modul operatii: chips_target_index=1 +
|
||||
chips_pick actualizeaza chips[1] (op#2), nu chips[0] (op#1).
|
||||
"""
|
||||
acct = _create_account_user("byindex.op2@test.com")
|
||||
_create_account_user("byindex.op2@test.com")
|
||||
_login(client, "byindex.op2@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
_seed_cod("FRN1", "Sistem de franare")
|
||||
@@ -719,27 +723,25 @@ def test_picker_by_index_op2_nu_op1(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1", ""], # idx 0=OE-1 (mapata), idx 1="" (nemapata)
|
||||
"chip_op_service": ["Op-A", "Op-B"],
|
||||
"chip_denumire": ["Prima", "A doua"],
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "Op-A", "den": "Prima", "exclus": 0},
|
||||
{"cod": "", "op": "Op-B", "den": "A doua", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_add_op_index": "1", # adauga pe op#2 (idx=1)
|
||||
"chips_add_cod_1": "FRN1", # picker-ul op#2 contine FRN1
|
||||
"chips_target_index": "1", # adauga pe op#2 (idx=1)
|
||||
"chips_pick": "FRN1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text[:300]
|
||||
html = resp.text
|
||||
|
||||
import re as _re
|
||||
hidden_vals = _re.findall(r'<input[^>]+name="cod_prestatie"[^>]+value="([^"]*)"', html)
|
||||
assert "OE-1" in hidden_vals, f"OE-1 (op#1) a disparut dupa adaugare pe op#2: {hidden_vals}"
|
||||
assert "FRN1" in hidden_vals, f"FRN1 nu a aterizat pe op#2: {hidden_vals}"
|
||||
# By-index: OE-1 trebuie sa fie INAINTE de FRN1 (idx 0 < idx 1)
|
||||
oe1_pos = hidden_vals.index("OE-1") if "OE-1" in hidden_vals else -1
|
||||
frn1_pos = hidden_vals.index("FRN1") if "FRN1" in hidden_vals else -1
|
||||
assert oe1_pos < frn1_pos, (
|
||||
f"FRN1 (op#2, idx=1) trebuie dupa OE-1 (op#1, idx=0) by-index: {hidden_vals}"
|
||||
)
|
||||
m = _re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
items = payload["items"]
|
||||
assert items[0]["cod"] == "OE-1", f"OE-1 (op#1) a disparut dupa adaugare pe op#2: {items}"
|
||||
assert items[1]["cod"] == "FRN1", f"FRN1 nu a aterizat pe op#2 (idx=1): {items}"
|
||||
|
||||
|
||||
def test_empty_state_picker_nomenclator_gol(client):
|
||||
@@ -774,25 +776,25 @@ def test_empty_state_picker_nomenclator_gol(client):
|
||||
|
||||
|
||||
def test_add_extra_semnal_vizibil_cod_invalid(client):
|
||||
"""add_extra cu cod invalid da semnal vizibil (nu esua silentios).
|
||||
"""'add' fara tinta cu cod invalid da semnal vizibil (nu esua silentios).
|
||||
|
||||
div.chips-extra-error vizibil cand codul e invalid sau selectul e gol.
|
||||
"""
|
||||
acct = _create_account_user("extra.err.signal@test.com")
|
||||
_create_account_user("extra.err.signal@test.com")
|
||||
_login(client, "extra.err.signal@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
csrf = _csrf(client)
|
||||
|
||||
# add_extra cu cod necunoscut in nomenclator
|
||||
# add fara tinta cu cod necunoscut in nomenclator
|
||||
resp = client.post(
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1"],
|
||||
"chip_op_service": ["SchimbUlei"],
|
||||
"chip_denumire": ["Schimb ulei"],
|
||||
"chips_action": "add_extra",
|
||||
"chips_add_cod_flat": "XX-99", # cod inexistent
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "SchimbUlei", "den": "Schimb ulei", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "XX-99", # cod inexistent
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -802,7 +804,7 @@ def test_add_extra_semnal_vizibil_cod_invalid(client):
|
||||
|
||||
|
||||
def test_add_per_op_cod_invalid_afiseaza_mesaj(client):
|
||||
"""Actiunea 'add' (picker per operatie) cu cod invalid da semnal vizibil."""
|
||||
"""Actiunea 'add' (picker per operatie, cu tinta) cu cod invalid da semnal vizibil."""
|
||||
_create_account_user("add.op.err@test.com")
|
||||
_login(client, "add.op.err@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
@@ -812,12 +814,12 @@ def test_add_per_op_cod_invalid_afiseaza_mesaj(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": [""],
|
||||
"chip_op_service": ["SchimbUlei"],
|
||||
"chip_denumire": ["Schimb ulei"],
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "", "op": "SchimbUlei", "den": "Schimb ulei", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_add_op_index": "0",
|
||||
"chips_add_cod_0": "XX-99", # cod inexistent
|
||||
"chips_target_index": "0",
|
||||
"chips_pick": "XX-99", # cod inexistent
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -837,21 +839,19 @@ def test_add_flat_cod_invalid_afiseaza_mesaj(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1"],
|
||||
"chip_op_service": [""],
|
||||
"chip_denumire": [""],
|
||||
"chips_action": "add_flat",
|
||||
"chips_add_cod_flat": "XX-99",
|
||||
"chips_state": json.dumps({"v": 1, "items": [{"cod": "OE-1", "op": "", "den": "", "exclus": 0}]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "XX-99",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "chips-extra-error" in resp.text, (
|
||||
f"Semnal de eroare lipsa la add_flat cu cod invalid: {resp.text[:300]}"
|
||||
f"Semnal de eroare lipsa la add cu cod invalid: {resp.text[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def test_add_flat_duplicat_afiseaza_mesaj(client):
|
||||
"""add_flat cu un cod deja prezent NU dubleaza chip-ul si afiseaza mesaj."""
|
||||
"""'add' fara tinta cu un cod deja prezent NU dubleaza chip-ul si afiseaza mesaj."""
|
||||
_create_account_user("add.flat.dup@test.com")
|
||||
_login(client, "add.flat.dup@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
@@ -861,24 +861,24 @@ def test_add_flat_duplicat_afiseaza_mesaj(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1"],
|
||||
"chip_op_service": [""],
|
||||
"chip_denumire": [""],
|
||||
"chips_action": "add_flat",
|
||||
"chips_add_cod_flat": "OE-1", # deja in lista
|
||||
"chips_state": json.dumps({"v": 1, "items": [{"cod": "OE-1", "op": "", "den": "", "exclus": 0}]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "OE-1", # deja in lista
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
import re as _re
|
||||
hidden = _re.findall(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', resp.text)
|
||||
assert len(hidden) == 1, f"Chip OE-1 duplicat: {len(hidden)} aparitii"
|
||||
m = _re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert len(payload["items"]) == 1, f"Chip OE-1 duplicat: {payload['items']}"
|
||||
assert "deja in lista" in resp.text, (
|
||||
f"Mesaj de duplicat lipsa la add_flat: {resp.text[:300]}"
|
||||
f"Mesaj de duplicat lipsa la add fara tinta: {resp.text[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def test_add_extra_duplicat_afiseaza_mesaj(client):
|
||||
"""add_extra cu chip extra identic existent afiseaza mesaj (nu no-op)."""
|
||||
"""'add' fara tinta cu chip liber identic existent afiseaza mesaj (nu no-op)."""
|
||||
_create_account_user("add.extra.dup@test.com")
|
||||
_login(client, "add.extra.dup@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
@@ -889,16 +889,17 @@ def test_add_extra_duplicat_afiseaza_mesaj(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1", "FRN1"],
|
||||
"chip_op_service": ["SchimbUlei", ""],
|
||||
"chip_denumire": ["Schimb ulei", ""],
|
||||
"chips_action": "add_extra",
|
||||
"chips_add_cod_flat": "FRN1", # chip extra identic deja existent
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "SchimbUlei", "den": "Schimb ulei", "exclus": 0},
|
||||
{"cod": "FRN1", "op": "", "den": "", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "FRN1", # chip liber identic deja existent
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "deja in lista" in resp.text, (
|
||||
f"Mesaj de duplicat lipsa la add_extra: {resp.text[:300]}"
|
||||
f"Mesaj de duplicat lipsa la add fara tinta: {resp.text[:300]}"
|
||||
)
|
||||
|
||||
|
||||
@@ -914,11 +915,11 @@ def test_add_reusit_marcheaza_chip_nou(client):
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
"cod_prestatie": ["OE-1"],
|
||||
"chip_op_service": ["SchimbUlei"],
|
||||
"chip_denumire": ["Schimb ulei"],
|
||||
"chips_action": "add_extra",
|
||||
"chips_add_cod_flat": "FRN1",
|
||||
"chips_state": json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "SchimbUlei", "den": "Schimb ulei", "exclus": 0},
|
||||
]}),
|
||||
"chips_action": "add",
|
||||
"chips_pick": "FRN1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
@@ -125,7 +125,8 @@ def client(monkeypatch):
|
||||
|
||||
|
||||
def test_editabil_arata_select_cod_rar(client):
|
||||
"""needs_data cu nomenclator populat → formularul de detaliu afiseaza <select name='cod_prestatie'>."""
|
||||
"""needs_data cu nomenclator populat → formularul de detaliu afiseaza <select name='chips_pick'>
|
||||
(picker-ul unificat de cod RAR; starea chips traieste in hidden name='chips_state', PRD 5.23)."""
|
||||
acct = _create_account_user("sel1@test.com")
|
||||
_ins_nomenclator("OE-1", "OE-2")
|
||||
sid = _ins(acct, status="needs_data")
|
||||
@@ -133,8 +134,11 @@ def test_editabil_arata_select_cod_rar(client):
|
||||
|
||||
html = _detaliu(client, sid)
|
||||
|
||||
assert 'name="cod_prestatie"' in html, (
|
||||
"Formularul de detaliu needs_data trebuie sa contina un select cu name='cod_prestatie'"
|
||||
assert 'name="chips_pick"' in html, (
|
||||
"Formularul de detaliu needs_data trebuie sa contina un select cu name='chips_pick'"
|
||||
)
|
||||
assert 'name="chips_state"' in html, (
|
||||
"Starea chips trebuie persistata intr-un hidden name='chips_state'"
|
||||
)
|
||||
assert "<select" in html, (
|
||||
"Elementul <select> trebuie sa apara in detaliu pentru starea needs_data"
|
||||
|
||||
421
tests/test_web_editare_rand.py
Normal file
421
tests/test_web_editare_rand.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""US-004: persistarea `exclus` prin salvare (import + Trimiteri), round-trip complet.
|
||||
|
||||
Acopera:
|
||||
- resolve_prestatii: item-exclus EXPLICIT supravietuieste re-rezolvarii (treapta noua
|
||||
de precedenta) FARA sa strice restul precedentei (IRON RULE, regresiune-cheie).
|
||||
- Import (web_editeaza_rand / apply_row_override): `exclus` persistat in override_json,
|
||||
chip Nedeclarat reapare la redeschiderea modalului, x il readuce la warning.
|
||||
- Rand cu toate operatiile excluse -> stare preview 'excluded' ("Nedeclarat").
|
||||
- Trimiteri (post_corectie_trimitere): acelasi round-trip pe chips_state; toate
|
||||
excluse -> needs_data cu motiv explicit.
|
||||
- Sentinelul __NEDECLARAT__ NU ajunge in payload daca apare direct ca cod_prestatie
|
||||
in starea hidden (nu doar ales din select), pe AMBELE cai.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test unitar (fara client): precedenta resolve_prestatii #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_exclus_supravietuieste_resolve_chiar_cu_mapare_pe_cont():
|
||||
"""IRON RULE: item cu `exclus=1` explicit pe item RAMANE exclus dupa resolve_prestatii,
|
||||
chiar daca operatia are (a) o mapare exacta pe cont SAU (b) un cod_prestatie deja
|
||||
valid pe rand. Regresiune-cheie: restul precedentei (cod valid > exclus de cont >
|
||||
mapare exacta > reguli text > nemapat) trebuie sa ramana IDENTICA pentru itemii
|
||||
FARA `exclus` explicit — altfel stricam maparile existente.
|
||||
"""
|
||||
from app.mapping import resolve_prestatii
|
||||
|
||||
valid_codes = {"OE-1", "IG-1", "FRN1"}
|
||||
mapping = {"Schimb-Ulei": "OE-1"} # mapare exacta pe cont pentru Schimb-Ulei
|
||||
excluded_ops_cont = {"Filtru-Aer"} # regula de excludere PE CONT (alt mecanism)
|
||||
|
||||
prestatii = [
|
||||
# (a) exclus explicit + mapare exacta pe cont exista -> exclus CASTIGA
|
||||
{"cod_op_service": "Schimb-Ulei", "denumire": "Schimb ulei motor", "exclus": 1},
|
||||
# (b) exclus explicit + cod_prestatie deja valid pe rand -> exclus CASTIGA
|
||||
{"cod_op_service": "Frana", "denumire": "Reparatie frana", "cod_prestatie": "FRN1", "exclus": 1},
|
||||
# regresiune: FARA exclus explicit, mapare exacta trebuie sa functioneze ca inainte
|
||||
{"cod_op_service": "Schimb-Ulei", "denumire": "Schimb ulei motor 2"},
|
||||
# regresiune: FARA exclus explicit, regula de excludere PE CONT trebuie sa functioneze
|
||||
{"cod_op_service": "Filtru-Aer", "denumire": "Filtru aer"},
|
||||
# regresiune: cod deja valid pe rand, FARA exclus -> pastrat ca atare (cod_valid)
|
||||
{"cod_op_service": "", "denumire": "", "cod_prestatie": "IG-1"},
|
||||
# regresiune: nemapat ramane nemapat
|
||||
{"cod_op_service": "Necunoscut-Op", "denumire": "Operatie necunoscuta"},
|
||||
]
|
||||
|
||||
resolved, unmapped = resolve_prestatii(
|
||||
prestatii, mapping, valid_codes=valid_codes, text_rules=None, excluded_ops=excluded_ops_cont,
|
||||
)
|
||||
|
||||
item_a = resolved[0]
|
||||
assert item_a.get("exclus") is True, f"exclus explicit trebuia sa supravietuiasca: {item_a}"
|
||||
assert not item_a.get("cod_prestatie"), f"item exclus explicit nu trebuie sa aiba cod_prestatie: {item_a}"
|
||||
|
||||
item_b = resolved[1]
|
||||
assert item_b.get("exclus") is True, f"exclus explicit trebuia sa domine cod_prestatie valid: {item_b}"
|
||||
assert not item_b.get("cod_prestatie"), f"cod_prestatie valid trebuia sters de exclus explicit: {item_b}"
|
||||
|
||||
# Regresiune: mapare exacta pe cont NESCHIMBATA pentru itemul fara exclus
|
||||
item_c = resolved[2]
|
||||
assert item_c.get("cod_prestatie") == "OE-1", f"mapare exacta pe cont stricata: {item_c}"
|
||||
assert not item_c.get("exclus"), f"item fara exclus explicit nu trebuie marcat exclus: {item_c}"
|
||||
|
||||
# Regresiune: regula de excludere PE CONT (mecanism vechi) NESCHIMBATA
|
||||
item_d = resolved[3]
|
||||
assert item_d.get("exclus") is True, f"regula de excludere pe cont stricata: {item_d}"
|
||||
assert not item_d.get("cod_prestatie")
|
||||
|
||||
# Regresiune: cod deja valid pastrat ca atare
|
||||
item_e = resolved[4]
|
||||
assert item_e.get("cod_prestatie") == "IG-1", f"cod valid pastrat stricat: {item_e}"
|
||||
|
||||
# Regresiune: nemapat ramane nemapat
|
||||
assert len(unmapped) == 1
|
||||
assert unmapped[0]["cod_op_service"] == "Necunoscut-Op"
|
||||
|
||||
|
||||
def test_precedenta_neschimbata_pentru_itemi_fara_exclus():
|
||||
"""Regresiune suplimentara: fara `exclus` explicit pe niciun item, comportamentul
|
||||
lui resolve_prestatii e IDENTIC cu cel dinaintea treptei noi (cod valid > exclus
|
||||
de cont > mapare exacta > reguli text > nemapat).
|
||||
"""
|
||||
from app.mapping import resolve_prestatii
|
||||
|
||||
valid_codes = {"OE-1"}
|
||||
mapping = {"Op-Mapata": "OE-1"}
|
||||
text_rules = [{"pattern": "frana", "cod_prestatie": "OE-1", "auto_send": 1, "priority": 1}]
|
||||
excluded_ops = {"Op-Exclusa"}
|
||||
|
||||
prestatii = [
|
||||
{"cod_op_service": "Op-Mapata", "denumire": "Operatie mapata"},
|
||||
{"cod_op_service": "Op-Exclusa", "denumire": "Operatie exclusa"},
|
||||
{"cod_op_service": "Op-Text", "denumire": "Reparatie frana fata"},
|
||||
{"cod_op_service": "Op-Nemapat", "denumire": "Chestie noua"},
|
||||
]
|
||||
|
||||
resolved, unmapped = resolve_prestatii(prestatii, mapping, valid_codes, text_rules, excluded_ops)
|
||||
|
||||
assert resolved[0]["cod_prestatie"] == "OE-1"
|
||||
assert not resolved[0].get("exclus")
|
||||
assert resolved[1]["exclus"] is True
|
||||
assert not resolved[1].get("cod_prestatie")
|
||||
assert resolved[2]["cod_prestatie"] == "OE-1" # via regula text
|
||||
assert resolved[2].get("cod_sursa")
|
||||
assert len(unmapped) == 1
|
||||
assert unmapped[0]["cod_op_service"] == "Op-Nemapat"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fixtures + helpere HTTP (import + Trimiteri, fara auth — cont 1 implicit) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "editare_rand.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()
|
||||
|
||||
|
||||
def _seed_cod(cod: str, denumire: str = "Prestatie test") -> None:
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
||||
(cod, denumire),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _csv_bytes(rows: list[dict]) -> bytes:
|
||||
import csv as _csv
|
||||
import io
|
||||
buf = io.StringIO()
|
||||
writer = _csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=";")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return buf.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
_MAP_COLS = {
|
||||
"VIN": "vin",
|
||||
"Nr": "nr_inmatriculare",
|
||||
"Data": "data_prestatie",
|
||||
"KM": "odometru_final",
|
||||
"Operatie": "operatie",
|
||||
}
|
||||
|
||||
|
||||
def _upload_and_preview(client: TestClient, rows: list[dict]) -> int:
|
||||
import io as _io
|
||||
csv_data = _csv_bytes(rows)
|
||||
r = client.post(
|
||||
"/_import/upload",
|
||||
files={"file": ("test.csv", _io.BytesIO(csv_data), "text/csv")},
|
||||
data={},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
|
||||
assert m, f"import_id negasit in raspuns: {r.text[:300]}"
|
||||
iid = int(m.group(1))
|
||||
colnames = list(rows[0].keys())
|
||||
canons = [_MAP_COLS[c] for c in colnames]
|
||||
r2 = client.post(f"/_import/{iid}/mapare-coloane", data={
|
||||
"colname": colnames,
|
||||
"canon": canons,
|
||||
"format_data": "YYYY-MM-DD",
|
||||
})
|
||||
assert r2.status_code == 200, r2.text
|
||||
return iid
|
||||
|
||||
|
||||
def _chips_state_din_html(html: str) -> dict:
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m, f"hidden chips_state lipsa din HTML: {html[:400]}"
|
||||
return json.loads(m.group(1).replace(""", '"'))
|
||||
|
||||
|
||||
# --- Trimiteri: helpere submissions direct in DB (cont 1, fara auth) --------- #
|
||||
|
||||
def _insert_submission(*, status: str, payload: dict) -> int:
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(f"k-{os.urandom(6).hex()}", 1, status, json.dumps(payload)),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _submission_row(sid: int):
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _submission_payload(sid: int) -> dict:
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
r = conn.execute("SELECT payload_json FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
return json.loads(r["payload_json"])
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _payload_cu_ops(vin: str, ops: list[tuple[str, str]]) -> dict:
|
||||
return {
|
||||
"vin": vin,
|
||||
"nr_inmatriculare": "B100AAA",
|
||||
"data_prestatie": "2026-06-10",
|
||||
"odometru_final": "50000",
|
||||
"prestatii": [{"cod_op_service": op, "denumire": den} for op, den in ops],
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Import: salvare + round-trip (web_editeaza_rand / apply_row_override) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
_SAMPLE_ROW_1OP = [{
|
||||
"VIN": "WVWZZZ1KZAW000999",
|
||||
"Nr": "B009TST",
|
||||
"Data": "2026-06-10",
|
||||
"KM": "123456",
|
||||
"Operatie": "Op-Nemapata",
|
||||
}]
|
||||
|
||||
|
||||
def test_salvare_pastreaza_exclus_in_override(client):
|
||||
"""Salvarea unui chip Nedeclarat in modalul de editare (import) persista `exclus=1`
|
||||
in override_json — la reincarcarea preview-ului randul ramane 'Nedeclarat'."""
|
||||
iid = _upload_and_preview(client, _SAMPLE_ROW_1OP)
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "", "op": "Op-Nemapata", "den": "Op-Nemapata", "exclus": 1},
|
||||
]})
|
||||
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200, r.text
|
||||
trigger_settle = r.headers.get("HX-Trigger-After-Settle", "")
|
||||
assert "inchideModal" in trigger_settle, f"Salvarea trebuia sa reuseasca: {r.text[:400]}"
|
||||
|
||||
r2 = client.get(f"/_import/{iid}/rand/0/editare-modal")
|
||||
assert r2.status_code == 200
|
||||
payload = _chips_state_din_html(r2.text)
|
||||
item = payload["items"][0]
|
||||
assert item.get("exclus") == 1, f"exclus nu a fost persistat: {payload}"
|
||||
assert not item.get("cod"), f"item exclus nu trebuie sa aiba cod: {payload}"
|
||||
|
||||
|
||||
def test_redeschidere_modal_arata_chip_nedeclarat_si_x_readuce_warning(client):
|
||||
"""Round-trip complet: dupa salvare, redeschiderea modalului arata chipul
|
||||
'Nedeclarat' (nu warning); actiunea x (/form-chips remove) il readuce la warning."""
|
||||
iid = _upload_and_preview(client, _SAMPLE_ROW_1OP)
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "", "op": "Op-Nemapata", "den": "Op-Nemapata", "exclus": 1},
|
||||
]})
|
||||
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200
|
||||
|
||||
r2 = client.get(f"/_import/{iid}/rand/0/editare-modal")
|
||||
assert r2.status_code == 200
|
||||
html = r2.text
|
||||
assert "Nedeclarat" in html, f"Chipul Nedeclarat trebuia sa reapara la redeschidere: {html[:600]}"
|
||||
assert "chip-nedeclarat" in html
|
||||
|
||||
# x pe chip Nedeclarat -> /form-chips remove index 0 -> revine la warning.
|
||||
payload = _chips_state_din_html(html)
|
||||
r3 = client.post("/form-chips", data={
|
||||
"chips_state": json.dumps(payload),
|
||||
"chips_action": "remove",
|
||||
"chips_remove_index": "0",
|
||||
})
|
||||
assert r3.status_code == 200, r3.text
|
||||
assert "Nedeclarat" not in r3.text, "Dupa x, chipul Nedeclarat nu mai trebuie sa apara"
|
||||
payload3 = _chips_state_din_html(r3.text)
|
||||
item3 = payload3["items"][0]
|
||||
assert not item3.get("exclus"), f"x trebuia sa readuca operatia la warning (exclus=0): {payload3}"
|
||||
assert not item3.get("cod"), f"operatia trebuia sa ramana fara cod dupa x: {payload3}"
|
||||
|
||||
|
||||
def test_rand_toate_operatiile_excluse_devine_excluded(client):
|
||||
"""Rand cu unica operatie exclusa -> stare preview 'excluded' ('Nedeclarat')."""
|
||||
iid = _upload_and_preview(client, _SAMPLE_ROW_1OP)
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "", "op": "Op-Nemapata", "den": "Op-Nemapata", "exclus": 1},
|
||||
]})
|
||||
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200, r.text
|
||||
trigger = json.loads(r.headers.get("HX-Trigger", "{}"))
|
||||
stare = trigger.get("randSalvat", {}).get("stare", "")
|
||||
assert "Nedeclarat" in stare, f"randSalvat.stare trebuia sa arate Nedeclarat, got: {stare!r}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Trimiteri: salvare + round-trip (post_corectie_trimitere) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_salvare_trimitere_cu_exclus(client):
|
||||
"""/trimitere/{id}/corecteaza cu un item mapat + un item exclus -> queued,
|
||||
payload_json pastreaza AMBELE (itemul exclus adnotat exclus=1, fara cod_prestatie)."""
|
||||
_seed_cod("OE-1", "Schimb ulei")
|
||||
sid = _insert_submission(status="needs_mapping", payload=_payload_cu_ops(
|
||||
"WVWZZZ1JZXW0EX001",
|
||||
[("SchimbUlei", "Schimb ulei motor"), ("Filtru-Aer", "Filtru aer")],
|
||||
))
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "SchimbUlei", "den": "Schimb ulei motor", "exclus": 0},
|
||||
{"cod": "", "op": "Filtru-Aer", "den": "Filtru aer", "exclus": 1},
|
||||
]})
|
||||
r = client.post(f"/trimitere/{sid}/corecteaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200, r.text[:500]
|
||||
|
||||
row = _submission_row(sid)
|
||||
assert row["status"] == "queued", f"status asteptat queued, got {row['status']}: {r.text[:500]}"
|
||||
prestatii = _submission_payload(sid)["prestatii"]
|
||||
assert len(prestatii) == 2, f"ambele itemi trebuiau pastrate in payload: {prestatii}"
|
||||
by_op = {p.get("cod_op_service"): p for p in prestatii}
|
||||
assert by_op["SchimbUlei"].get("cod_prestatie") == "OE-1"
|
||||
assert by_op["Filtru-Aer"].get("exclus") is True, f"itemul exclus trebuia adnotat: {prestatii}"
|
||||
assert not by_op["Filtru-Aer"].get("cod_prestatie"), f"itemul exclus nu trebuie sa aiba cod: {prestatii}"
|
||||
|
||||
|
||||
def test_trimitere_toate_excluse_devine_needs_data(client):
|
||||
"""Toate operatiile excluse -> needs_data cu motiv explicit (nu se trimite la RAR)."""
|
||||
sid = _insert_submission(status="needs_mapping", payload=_payload_cu_ops(
|
||||
"WVWZZZ1JZXW0EX002",
|
||||
[("Filtru-Aer", "Filtru aer")],
|
||||
))
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "", "op": "Filtru-Aer", "den": "Filtru aer", "exclus": 1},
|
||||
]})
|
||||
r = client.post(f"/trimitere/{sid}/corecteaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200, r.text[:500]
|
||||
|
||||
row = _submission_row(sid)
|
||||
assert row["status"] == "needs_data", f"status asteptat needs_data, got {row['status']}"
|
||||
assert "excluse" in (row["rar_error"] or "").lower()
|
||||
prestatii = _submission_payload(sid)["prestatii"]
|
||||
assert prestatii[0].get("exclus") is True, "itemul exclus trebuie pastrat in payload pt afisare motiv"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Sentinel __NEDECLARAT__ respins explicit daca apare ca cod_prestatie #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_sentinel_nedeclarat_nu_ajunge_in_payload_import(client):
|
||||
"""Daca cineva fabrica direct chips_state cu cod=__NEDECLARAT__ (nu prin selectul
|
||||
care seteaza exclus), salvarea trebuie respinsa — sentinelul nu e in nomenclator."""
|
||||
iid = _upload_and_preview(client, _SAMPLE_ROW_1OP)
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "__NEDECLARAT__", "op": "Op-Nemapata", "den": "Op-Nemapata", "exclus": 0},
|
||||
]})
|
||||
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200
|
||||
assert "necunoscut" in r.text.lower(), f"Sentinelul trebuia respins ca cod necunoscut: {r.text[:400]}"
|
||||
trigger_settle = r.headers.get("HX-Trigger-After-Settle", "")
|
||||
assert "inchideModal" not in trigger_settle, "Salvarea NU trebuia sa reuseasca cu sentinelul in payload"
|
||||
|
||||
# Confirma ca sentinelul NU a ajuns in override (redeschiderea nu-l arata ca si cod).
|
||||
r2 = client.get(f"/_import/{iid}/rand/0/editare-modal")
|
||||
payload2 = _chips_state_din_html(r2.text)
|
||||
coduri = [it.get("cod") for it in payload2["items"]]
|
||||
assert "__NEDECLARAT__" not in coduri, f"Sentinelul a ajuns in starea persistata: {payload2}"
|
||||
|
||||
|
||||
def test_sentinel_nedeclarat_nu_ajunge_in_payload_trimitere(client):
|
||||
"""Aceeasi respingere pe calea Trimiteri (post_corectie_trimitere)."""
|
||||
sid = _insert_submission(status="needs_mapping", payload=_payload_cu_ops(
|
||||
"WVWZZZ1JZXW0SN001",
|
||||
[("Op-Test", "Operatie test")],
|
||||
))
|
||||
old_status = _submission_row(sid)["status"]
|
||||
|
||||
chips_state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "__NEDECLARAT__", "op": "Op-Test", "den": "Operatie test", "exclus": 0},
|
||||
]})
|
||||
r = client.post(f"/trimitere/{sid}/corecteaza", data={"chips_state": chips_state})
|
||||
assert r.status_code == 200
|
||||
assert "necunoscut" in r.text.lower(), f"Sentinelul trebuia respins ca cod necunoscut: {r.text[:400]}"
|
||||
|
||||
row = _submission_row(sid)
|
||||
assert row["status"] == old_status, f"Statusul nu trebuia sa se schimbe: {row['status']}"
|
||||
prestatii = _submission_payload(sid)["prestatii"]
|
||||
coduri = [p.get("cod_prestatie") for p in prestatii]
|
||||
assert "__NEDECLARAT__" not in coduri, f"Sentinelul a ajuns in payload: {prestatii}"
|
||||
@@ -212,7 +212,7 @@ def test_camp_observatii_prezent(client):
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_chips_multi_select_prestatii(client):
|
||||
"""Submission cu cod_prestatie setat afiseaza chip cu hidden input."""
|
||||
"""Submission cu cod_prestatie setat afiseaza chip cu OE-1 in starea hidden chips_state."""
|
||||
acct = _create_account_user("chips.test@test.com")
|
||||
_login(client, "chips.test@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
@@ -222,14 +222,13 @@ def test_chips_multi_select_prestatii(client):
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Trebuie sa existe un input (de obicei hidden) cu name="cod_prestatie" si valoarea "OE-1"
|
||||
has_cod_prestatie_chip = bool(
|
||||
re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', html) or
|
||||
re.search(r'<input[^>]+value="OE-1"[^>]+name="cod_prestatie"', html)
|
||||
)
|
||||
assert has_cod_prestatie_chip, (
|
||||
"Formularul trebuie sa contina un hidden input cu name='cod_prestatie' value='OE-1' "
|
||||
"reprezentand chip-ul de prestatie."
|
||||
# Starea chips traieste intr-UN SINGUR hidden JSON versionat (D4, PRD 5.23).
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m, "Formularul trebuie sa contina hidden input name='chips_state'"
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
coduri = [it["cod"] for it in payload["items"]]
|
||||
assert "OE-1" in coduri, (
|
||||
f"chips_state trebuie sa contina codul OE-1 al prestatiei. items={payload['items']}"
|
||||
)
|
||||
|
||||
|
||||
@@ -244,19 +243,19 @@ def test_adauga_sterge_chip(client):
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
csrf = _csrf(client)
|
||||
|
||||
# POST /form-chips: adauga OE-1 la prima operatie (index 0)
|
||||
# POST /form-chips: adauga OE-1 la prima operatie (index 0), stare curenta = un
|
||||
# singur hidden chips_state (JSON versionat, D4).
|
||||
state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
||||
]})
|
||||
resp = client.post(
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
# Starea curenta: o operatie fara cod
|
||||
"chip_op_service": ["Op-A"],
|
||||
"chip_denumire": ["Schimb ulei motor"],
|
||||
"cod_prestatie": [""], # nemaapat initial
|
||||
# Actiunea
|
||||
"chips_state": state,
|
||||
"chips_action": "add",
|
||||
"chips_add_op_index": "0",
|
||||
"chips_add_cod_0": "OE-1",
|
||||
"chips_target_index": "0",
|
||||
"chips_pick": "OE-1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, f"/form-chips a returnat {resp.status_code}: {resp.text[:400]}"
|
||||
@@ -266,34 +265,31 @@ def test_adauga_sterge_chip(client):
|
||||
assert "OE-1" in html, (
|
||||
f"Dupa add, OE-1 trebuie sa apara in raspunsul /form-chips. html[:500]={html[:500]}"
|
||||
)
|
||||
# Si hidden input cu valoarea OE-1
|
||||
has_hidden = bool(
|
||||
re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', html) or
|
||||
re.search(r'<input[^>]+value="OE-1"[^>]+name="cod_prestatie"', html)
|
||||
)
|
||||
assert has_hidden, (
|
||||
"Dupa add, trebuie sa existe un input cu name='cod_prestatie' value='OE-1' "
|
||||
f"in raspunsul /form-chips. html[:600]={html[:600]}"
|
||||
# Si in starea hidden chips_state
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m, f"hidden chips_state lipsa. html[:600]={html[:600]}"
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
assert payload["items"][0]["cod"] == "OE-1", (
|
||||
f"Dupa add, chips_state trebuie sa contina OE-1. items={payload['items']}"
|
||||
)
|
||||
|
||||
|
||||
def test_sterge_chip(client):
|
||||
"""POST /form-chips cu action=remove sterge chip-ul la indexul dat."""
|
||||
"""POST /form-chips cu action=remove sterge codul chip-ului la indexul dat."""
|
||||
acct = _create_account_user("form.chips.del@test.com")
|
||||
_login(client, "form.chips.del@test.com")
|
||||
_seed_cod("OE-1", "Schimb ulei motor")
|
||||
csrf = _csrf(client)
|
||||
|
||||
# POST /form-chips: sterge chip-ul de la index 0 (OE-1 existent)
|
||||
# POST /form-chips: sterge codul chip-ului de la index 0 (OE-1 existent, mapat pe Op-A)
|
||||
state = json.dumps({"v": 1, "items": [
|
||||
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
||||
]})
|
||||
resp = client.post(
|
||||
"/form-chips",
|
||||
data={
|
||||
"csrf_token": csrf,
|
||||
# Starea curenta: OE-1 mapat pe Op-A
|
||||
"chip_op_service": ["Op-A"],
|
||||
"chip_denumire": ["Schimb ulei motor"],
|
||||
"cod_prestatie": ["OE-1"],
|
||||
# Actiunea: sterge indexul 0
|
||||
"chips_state": state,
|
||||
"chips_action": "remove",
|
||||
"chips_remove_index": "0",
|
||||
},
|
||||
@@ -301,15 +297,15 @@ def test_sterge_chip(client):
|
||||
assert resp.status_code == 200, f"/form-chips remove a returnat {resp.status_code}: {resp.text[:400]}"
|
||||
|
||||
html = resp.text
|
||||
# Dupa remove, OE-1 nu mai apare ca chip (input hidden cu acea valoare)
|
||||
has_oe1_chip = bool(
|
||||
re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', html) or
|
||||
re.search(r'<input[^>]+value="OE-1"[^>]+name="cod_prestatie"', html)
|
||||
)
|
||||
assert not has_oe1_chip, (
|
||||
"Dupa remove, OE-1 NU mai trebuie sa apara ca chip "
|
||||
f"(hidden input cu cod_prestatie=OE-1). html[:500]={html[:500]}"
|
||||
# Dupa remove, OE-1 nu mai apare ca cod in starea chips (operatia RAMANE, nemapata)
|
||||
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
||||
assert m
|
||||
payload = json.loads(m.group(1).replace(""", '"'))
|
||||
coduri = [it["cod"] for it in payload["items"]]
|
||||
assert "OE-1" not in coduri, (
|
||||
f"Dupa remove, OE-1 NU mai trebuie sa apara ca cod in chips_state. items={payload['items']}"
|
||||
)
|
||||
assert payload["items"][0]["op"] == "Op-A", "Operatia trebuie sa ramana (doar codul dispare)"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -437,11 +433,16 @@ def test_form_chips_reveal_r_odo(client):
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 8: Picker per operatie (E4 binding) -- format op-row #
|
||||
# Test 8: Chip warning pentru operatie nemapata (control unificat, PRD 5.23) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_picker_per_operatie_in_form(client):
|
||||
"""Operatie nemapata (needs_mapping) -> formularul afiseaza picker pe operatie."""
|
||||
"""Operatie nemapata (needs_mapping) -> formularul afiseaza chip warning + selectul unic.
|
||||
|
||||
PRD 5.23 US-002: controlul unificat elimina `.op-row` (rand per operatie cu
|
||||
picker propriu); operatia nemapata devine un chip warning in containerul
|
||||
unic `.chips`, iar selectul e unul singur (`chips_pick`, id `chips-picker`).
|
||||
"""
|
||||
acct = _create_account_user("picker.op@test.com")
|
||||
_login(client, "picker.op@test.com")
|
||||
# NU seed-uim nicio mapare -> operatia ramane nemapata
|
||||
@@ -455,17 +456,15 @@ def test_picker_per_operatie_in_form(client):
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Operatia REVIZIE PERIODICA trebuie sa apara in form (op-row cu clasa specifica)
|
||||
# clasa "op-row" din CSS base.html e adaugata NUMAI de chips_prestatii.html
|
||||
has_op_row = "op-row" in html
|
||||
assert has_op_row, (
|
||||
"Formularul trebuie sa contina clasa 'op-row' "
|
||||
"pentru picker-ul per-operatie. "
|
||||
"Aceasta clasa e adaugata de _chips_prestatii.html. "
|
||||
# Controlul unificat (PRD 5.23): fara .op-row, UN singur container .chips.
|
||||
assert "op-row" not in html, (
|
||||
"op-row a fost eliminat de controlul unificat (PRD 5.23 US-002). "
|
||||
f"html[:600]={html[:600]}"
|
||||
)
|
||||
# Operatia REVIZIE PERIODICA trebuie sa apara in context op-row
|
||||
assert 'class="chips"' in html
|
||||
assert 'id="chips-picker"' in html
|
||||
# Operatia REVIZIE PERIODICA trebuie sa apara ca chip warning
|
||||
assert "REVIZIE PERIODICA" in html, (
|
||||
"Operatia 'REVIZIE PERIODICA' trebuie sa apara in formularul de editare (op-row). "
|
||||
f"html[:500]={html[:500]}"
|
||||
"Operatia 'REVIZIE PERIODICA' trebuie sa apara in formularul de editare "
|
||||
f"(chip warning). html[:500]={html[:500]}"
|
||||
)
|
||||
|
||||
@@ -280,8 +280,8 @@ def test_optional_nu_forteaza(client):
|
||||
def test_fara_select_vechi_redundant(client):
|
||||
"""Detaliu needs_data NU mai contine <select name='cod_prestatie'>.
|
||||
|
||||
Formularul editabil foloseste NUMAI chips (hidden inputs cod_prestatie),
|
||||
fara vechiul select dublu. Chips functioneaza ca singura sursa de cod_prestatie.
|
||||
Formularul editabil foloseste NUMAI chips (stare in hidden name='chips_state',
|
||||
JSON versionat, PRD 5.23), fara vechiul select dublu.
|
||||
|
||||
Nota: <select name="cod_prestatie"> RAMANE in formularul /repune pentru starea error.
|
||||
"""
|
||||
@@ -302,15 +302,14 @@ def test_fara_select_vechi_redundant(client):
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Chipurile trebuie sa fie prezente (hidden input cu name="cod_prestatie")
|
||||
has_chip_hidden = (
|
||||
re.search(r'<input[^>]+type=["\']hidden["\'][^>]+name=["\']cod_prestatie["\']', html) or
|
||||
re.search(r'<input[^>]+name=["\']cod_prestatie["\'][^>]+type=["\']hidden["\']', html)
|
||||
)
|
||||
# Chipurile trebuie sa fie prezente (hidden input cu name="chips_state", JSON versionat)
|
||||
has_chip_hidden = re.search(r'<input[^>]+type=["\']hidden["\'][^>]+name=["\']chips_state["\']', html)
|
||||
assert has_chip_hidden, (
|
||||
"Chips: trebuie sa existe input hidden cu name='cod_prestatie' (din _chips_prestatii.html). "
|
||||
"Chips: trebuie sa existe input hidden cu name='chips_state' (din _chips_prestatii.html). "
|
||||
f"html[:600]={html[:600]}"
|
||||
)
|
||||
# Cod-ul OE-1 trebuie sa apara in stare (JSON), nu ca input separat
|
||||
assert "OE-1" in html, f"OE-1 trebuie sa apara in chips_state. html[:600]={html[:600]}"
|
||||
|
||||
# Vechiul <select name="cod_prestatie"> NU trebuie sa existe in sectiunea editabila.
|
||||
# (needs_data nu are formular /repune, deci niciun select cu name="cod_prestatie" legal)
|
||||
|
||||
97
tests/test_worker_exclude_prestatii.py
Normal file
97
tests/test_worker_exclude_prestatii.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Prestatiile excluse de la declarare nu ajung niciodata la RAR.
|
||||
|
||||
content['prestatii'] pastreaza itemii exclusi (round-trip in editor), dar
|
||||
payload-ul trimis efectiv la RAR (build_rar_payload, apelat din process_one)
|
||||
trebuie sa contina DOAR partea declarabila. Un item exclus/fara cod nu produce
|
||||
niciodata {codPrestatie: null} in payload.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def env(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.db import get_connection, init_db
|
||||
init_db()
|
||||
conn = get_connection()
|
||||
settings = get_settings()
|
||||
yield conn, settings
|
||||
conn.close()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
_CONTENT_CU_EXCLUS = {
|
||||
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15", "odometru_final": "123456",
|
||||
"prestatii": [
|
||||
{"cod_prestatie": "OE-1", "cod_op_service": "100"},
|
||||
{"cod_prestatie": None, "cod_op_service": "200", "denumire": "Spalare", "exclus": True},
|
||||
],
|
||||
"sistem_reparat": "null",
|
||||
}
|
||||
|
||||
|
||||
def _insert(conn, content):
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, status, payload_json, retry_count) "
|
||||
"VALUES (?, 'queued', ?, 0)",
|
||||
(f"key-{os.urandom(4).hex()}", json.dumps(content)),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
class FakeRarCapturePayload:
|
||||
"""RAR mock care retine payload-ul trimis efectiv la postPrezentare."""
|
||||
|
||||
def __init__(self):
|
||||
self.sent_payload = None
|
||||
|
||||
def get_finalizate(self, token):
|
||||
return []
|
||||
|
||||
def post_prezentare(self, token, payload):
|
||||
self.sent_payload = payload
|
||||
return {"id": 999}
|
||||
|
||||
|
||||
def test_process_one_nu_trimite_item_exclus_la_rar(env):
|
||||
from app.worker.__main__ import process_one
|
||||
conn, settings = env
|
||||
sid = _insert(conn, _CONTENT_CU_EXCLUS)
|
||||
rar = FakeRarCapturePayload()
|
||||
|
||||
claimed = {"id": sid, "account_id": None, "content": _CONTENT_CU_EXCLUS}
|
||||
status = process_one(conn, settings, rar, "tok", claimed)
|
||||
|
||||
assert status == "sent"
|
||||
assert rar.sent_payload is not None
|
||||
coduri = [p["codPrestatie"] for p in rar.sent_payload["prestatii"]]
|
||||
assert None not in coduri
|
||||
assert coduri == ["OE-1"]
|
||||
|
||||
|
||||
def test_process_one_pastreaza_content_intact_in_db(env):
|
||||
"""content['prestatii'] din DB nu se ingusteaza — doar payload-ul RAR."""
|
||||
from app.worker.__main__ import process_one
|
||||
conn, settings = env
|
||||
sid = _insert(conn, _CONTENT_CU_EXCLUS)
|
||||
rar = FakeRarCapturePayload()
|
||||
|
||||
claimed = {"id": sid, "account_id": None, "content": dict(_CONTENT_CU_EXCLUS)}
|
||||
process_one(conn, settings, rar, "tok", claimed)
|
||||
|
||||
row = conn.execute("SELECT payload_json FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
saved = json.loads(row["payload_json"])
|
||||
# payload_json in DB nu e atins de process_one (doar payload-ul RAR e ingustat) —
|
||||
# ramane cu ambele prestatii, inclusiv cea exclusa.
|
||||
assert len(saved["prestatii"]) == 2
|
||||
Reference in New Issue
Block a user