"""Prestatii multi-cod (lista) la editare/corectie. Verifica: - Handler-ele accepta LISTA de cod_prestatie (form.getlist) -> prestatii cu mai multe coduri. - cod_op_service/denumire RAMAN pe item. - Cod invalid -> respins cu mesaj; cod necunoscut NU ajunge la RAR (ORA-12899). - Lista goala -> ramane needs_mapping. - Dedup per-item: (op_service, cod) unic, NU cod unic (doua ops diferite cu acelasi cod ok). - Recalcul idempotenta dupa editare. - odometruInitial obligatoriu cand cod_prestatie contine R-ODO/I-ODO. - op_service supravietuieste /repune cu cod. """ 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, "prestatii.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() # --------------------------------------------------------------------------- # # Helpere # # --------------------------------------------------------------------------- # 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 _insert(acct: int, *, status: str, payload: dict, key: str | None = None) -> int: from app.db import get_connection conn = get_connection() try: k = key or f"k-{os.urandom(6).hex()}" cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) " "VALUES (?, ?, ?, ?)", (k, acct, status, json.dumps(payload)), ) conn.commit() return int(cur.lastrowid) finally: conn.close() def _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 _payload_json(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 _seed_cod(cod: str, denumire: str = "Prestatie test") -> None: """Insereaza un cod in nomenclator_rar (fara operatii_mapping).""" 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 _payload_cu_ops(vin: str, ops: list[tuple[str, str]]) -> dict: """Payload cu prestatii avand cod_op_service/denumire (needs_mapping state).""" 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 ], } # --------------------------------------------------------------------------- # # Teste # # --------------------------------------------------------------------------- # def test_mai_multe_coduri_acceptate(client): """LISTA de cod_prestatie -> prestatii cu N itemi, fiecare cu cod setat.""" acct = _create_account_user("multi.cod@test.com") _login(client, "multi.cod@test.com") _seed_cod("OE-1", "Schimb ulei") _seed_cod("IG-1", "Inlocuire garnitura") sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0MC001", [("Op-A", "Schimb ulei motor"), ("Op-B", "Inlocuire garnitura chiulasa")], )) csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/corecteaza", data={ "csrf_token": csrf, "cod_prestatie": ["OE-1", "IG-1"], # 2 coduri pentru 2 operatii }, ) assert resp.status_code == 200, resp.text[:500] r = _row(sid) assert r["status"] == "queued", f"status asteptat queued, got {r['status']}" prestatii = _payload_json(sid)["prestatii"] assert len(prestatii) == 2, f"asteptat 2 prestatii, got {len(prestatii)}: {prestatii}" coduri = [p.get("cod_prestatie") for p in prestatii] assert "OE-1" in coduri, f"OE-1 lipsa din prestatii: {prestatii}" assert "IG-1" in coduri, f"IG-1 lipsa din prestatii: {prestatii}" def test_cod_op_service_pastrat_dupa_corecteaza(client): """cod_op_service si denumire RAMAN pe item dupa /corecteaza cu cod direct.""" acct = _create_account_user("op.pastrat@test.com") _login(client, "op.pastrat@test.com") _seed_cod("OE-1") sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0OP001", [("Schimb ulei", "Schimb ulei motor 5W30")], )) csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf, "cod_prestatie": "OE-1"}, ) assert resp.status_code == 200 prestatii = _payload_json(sid)["prestatii"] assert len(prestatii) == 1 item = prestatii[0] assert item.get("cod_prestatie") == "OE-1", f"cod_prestatie lipsa: {item}" assert item.get("cod_op_service") == "Schimb ulei", f"cod_op_service pierdut: {item}" assert item.get("denumire") == "Schimb ulei motor 5W30", f"denumire pierduta: {item}" def test_cod_invalid_respins(client): """Cod necunoscut in nomenclator -> respins cu mesaj, status neschimbat.""" acct = _create_account_user("cod.invalid@test.com") _login(client, "cod.invalid@test.com") # NU seed-uim "XX-99" -> cod necunoscut sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0CI001", [("Op-Test", "Operatie test")], )) old_status = _row(sid)["status"] csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf, "cod_prestatie": "XX-99"}, ) assert resp.status_code == 200 # Cod invalid -> mesaj de eroare vizibil assert "XX-99" in resp.text or "necunoscut" in resp.text.lower(), ( f"Mesaj de eroare lipsa pentru cod invalid; text={resp.text[:500]}" ) # Status neschimbat assert _row(sid)["status"] == old_status, ( f"Status s-a schimbat desi codul e invalid: {_row(sid)['status']}" ) def test_lista_goala_needs_mapping(client): """Nicio cod_prestatie trimis -> submission ramane needs_mapping.""" acct = _create_account_user("goala.nemap@test.com") _login(client, "goala.nemap@test.com") # NU seed-uim nicio mapare -> operatia ramane nemapata sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0GN001", [("Op-Nemap", "Operatie nemapata")], )) csrf = _csrf(client) # Trimit form FARA cod_prestatie (lista goala) resp = client.post( f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf}, ) assert resp.status_code == 200 assert _row(sid)["status"] == "needs_mapping", ( f"Status trebuia sa ramana needs_mapping, got {_row(sid)['status']}" ) def test_idempotency_recalculat(client): """Dupa setarea de coduri noi, cheia de idempotenta e recalculata.""" acct = _create_account_user("ido.recalc@test.com") _login(client, "ido.recalc@test.com") _seed_cod("OE-1") sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0ND001", [("Op-Ido", "Operatie ido")], )) old_key = _row(sid)["idempotency_key"] csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf, "cod_prestatie": "OE-1"}, ) assert resp.status_code == 200 assert _row(sid)["status"] == "queued" new_key = _row(sid)["idempotency_key"] assert new_key != old_key, ( f"Cheia de idempotenta NU s-a schimbat dupa setarea codului: {new_key}" ) def test_odometru_initial_conditionat_R_ODO(client): """cod_prestatie=R-ODO fara odometruInitial -> validate_prezentare intoarce eroare -> submission ramane needs_data (NU queued).""" acct = _create_account_user("odo.rodo@test.com") _login(client, "odo.rodo@test.com") _seed_cod("R-ODO", "Revizie odometru") # Payload: needs_mapping (op fara cod), FARA odometru_initial sid = _insert(acct, status="needs_mapping", payload={ "vin": "WVWZZZ1JZXW0RO001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", # odometru_initial ABSENT "prestatii": [{"cod_op_service": "Revizie", "denumire": "Revizie odometru"}], }) csrf = _csrf(client) # Trimit R-ODO ca cod (valid in nomenclator), dar fara odometru_initial resp = client.post( f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf, "cod_prestatie": "R-ODO"}, ) assert resp.status_code == 200 status = _row(sid)["status"] # R-ODO fara odometruInitial -> validare esuata -> needs_data (nu queued) assert status in ("needs_data", "needs_mapping"), ( f"Status neasteptat: {status}; trebuia needs_data/needs_mapping (R-ODO fara odo initial)" ) assert status != "queued", ( "R-ODO fara odometruInitial NU trebuie sa treaca in queued!" ) def test_dedup_per_item_nu_dupa_cod(client): """Doua operatii DIFERITE cu ACELASI cod RAR ambele supravietuiesc. Dedup = (op_service, cod) identice, NU cod singur. Doua ops distincte pot mapa legitim la acelasi cod RAR fara sa fie sterse de dedup. """ acct = _create_account_user("dedup.ops@test.com") _login(client, "dedup.ops@test.com") _seed_cod("OE-1", "Schimb ulei") # Doua operatii distincte, ambele vor primi OE-1 sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0DD001", [("Op-A", "Prima operatie"), ("Op-B", "A doua operatie")], )) csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/corecteaza", data={ "csrf_token": csrf, "cod_prestatie": ["OE-1", "OE-1"], # acelasi cod pentru ambele ops }, ) assert resp.status_code == 200 prestatii = _payload_json(sid)["prestatii"] # Ambele TREBUIE sa supravietuiasca: (Op-A, OE-1) != (Op-B, OE-1) assert len(prestatii) == 2, ( f"Dedup a sters o operatie distincta! prestatii={prestatii} " "(doua ops cu acelasi cod trebuie pastrate)" ) ops = [p.get("cod_op_service") for p in prestatii] assert "Op-A" in ops and "Op-B" in ops, f"ops_service pierdute: {ops}" # --------------------------------------------------------------------------- # # op_service supravietuieste /repune cu cod # # --------------------------------------------------------------------------- # def test_op_service_supravietuieste_repune_cu_cod(client): """Dupa /repune cu cod_prestatie, cod_op_service/denumire RAMAN pe item. Fara acest invariant se pierde contextul op->cod necesar pentru salvarea maparii din chip. """ acct = _create_account_user("e1.repune@test.com") _login(client, "e1.repune@test.com") _seed_cod("OE-1", "Schimb ulei motor") # Starea error: payload cu op_service (operatia venita de la import/API) sid = _insert(acct, status="error", payload={ "vin": "WVWZZZ1JZXW0E1001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [{ "cod_op_service": "Schimb ulei", "denumire": "Schimb ulei motor 5W30", # fara cod_prestatie initial }], }) csrf = _csrf(client) # /repune cu cod direct resp = client.post( f"/trimitere/{sid}/repune", data={"csrf_token": csrf, "cod_prestatie": "OE-1"}, ) assert resp.status_code == 200, resp.text[:500] r = _row(sid) assert r["status"] == "queued", f"status neasteptat: {r['status']}" prestatii = _payload_json(sid)["prestatii"] assert len(prestatii) == 1 item = prestatii[0] # IRON RULE E1: op_service si denumire TREBUIE sa fie prezente assert item.get("cod_op_service") == "Schimb ulei", ( f"E1 VIOLATED: cod_op_service a fost sters de /repune! item={item}" ) assert item.get("denumire") == "Schimb ulei motor 5W30", ( f"E1 VIOLATED: denumire a fost stearsa de /repune! item={item}" ) # Codul trebuie setat assert item.get("cod_prestatie") == "OE-1", ( f"cod_prestatie nu a fost setat corect: item={item}" ) def test_repune_nu_trunchiaza_prestatii_multiple(client): """/repune nu pierde prestatii[1:]. Formularul /repune trimite UN SINGUR select cod_prestatie; codul se aplica pozitional peste toate prestatiile existente, care raman toate (altfel un rand error cu 2+ prestatii ar pierde restul -> declaratie INCOMPLETA la RAR, FINALIZATA ireversibil). """ acct = _create_account_user("repune.multi@test.com") _login(client, "repune.multi@test.com") _seed_cod("AAA", "Prestatie A") _seed_cod("BBB", "Prestatie B") _seed_cod("CCC", "Prestatie C") # Rand error cu DOUA prestatii (ambele cu cod valid). sid = _insert(acct, status="error", payload={ "vin": "WVWZZZ1JZXW0RM001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [ {"cod_prestatie": "AAA"}, {"cod_prestatie": "BBB"}, ], }) csrf = _csrf(client) # /repune cu UN SINGUR cod nou (schimba prima prestatie). resp = client.post( f"/trimitere/{sid}/repune", data={"csrf_token": csrf, "cod_prestatie": "CCC"}, ) assert resp.status_code == 200, resp.text[:500] r = _row(sid) assert r["status"] == "queued", f"status neasteptat: {r['status']}" prestatii = _payload_json(sid)["prestatii"] assert len(prestatii) == 2, ( f"AMBELE prestatii trebuie pastrate de /repune, nu doar prima! got={prestatii}" ) coduri = [p.get("cod_prestatie") for p in prestatii] assert coduri == ["CCC", "BBB"], ( f"Codul nou se aplica POZITIONAL primei prestatii, a doua ramane intacta: {coduri}" ) def test_corectie_eroare_validare_pastreaza_picker(client): """Re-render-ul de eroare validare pastreaza optiunile pickerului. _detaliu_ctx primeste `conn`+`account_id` pe toate ramurile de re-render; altfel `nomenclator_rar=[]` si picker-ul chips randeaza zero optiuni. """ acct = _create_account_user("corectie.picker@test.com") _login(client, "corectie.picker@test.com") _seed_cod("ZZ-9", "Operatie existenta") # codul curent al randului (valid -> fara unmapped) _seed_cod("PK-1", "Optiune picker") # cod doar in nomenclator (detector de picker) # needs_data editabil, prestatie cu cod direct valid (resolve OK, fara unmapped). sid = _insert(acct, status="needs_data", payload={ "vin": "WVWZZZ1JZXW0PK001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [{"cod_prestatie": "ZZ-9"}], }) csrf = _csrf(client) # Corectie cu VIN invalid -> validare esueaza -> ramura de re-render 1432. resp = client.post( f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf, "vin": "BAD"}, ) assert resp.status_code == 200 assert _row(sid)["status"] == "needs_data" # Picker-ul trebuie sa contina optiunile din nomenclator (conn/account_id pasate). assert "PK-1" in resp.text, ( "Picker-ul chips e GOL dupa eroare de validare — _detaliu_ctx fara conn/account_id" ) def test_repune_select_afiseaza_denumirea(client): """Selectul /repune afiseaza denumirea operatiei, nu doar codul gol.""" acct = _create_account_user("repune.denumire@test.com") _login(client, "repune.denumire@test.com") _seed_cod("AAA", "Schimb ulei motor") sid = _insert(acct, status="error", payload={ "vin": "WVWZZZ1JZXW0RD001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [{"cod_prestatie": "AAA"}], }) resp = client.get(f"/_fragments/trimitere/{sid}") assert resp.status_code == 200 html = resp.text # Optiunea trebuie sa afiseze denumirea, nu doar codul gol. assert "Schimb ulei motor" in html, ( "Selectul /repune nu afiseaza denumirea operatiei (typo nome_prestatie)" ) assert "AAA — Schimb ulei motor" in html, ( f"Optiunea select nu randeaza 'cod — denumire': {html[html.find('AAA'):html.find('AAA')+60]}" ) # ============================================================================= # # Picker: denumiri, cod extra, salvare fara buton, aliniere by-index # # ============================================================================= # def test_picker_flat_arata_cod_si_denumire(client): """Picker plat afiseaza 'cod — denumire', nu doar codul.""" acct = _create_account_user("picker.flat.denu@test.com") _login(client, "picker.flat.denu@test.com") _seed_cod("FRN1", "Sistem de franare") # Submission flat: fara cod_op_service (mod plat) sid = _insert(acct, status="needs_mapping", payload={ "vin": "WVWZZZ1JZXW0US4001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [], # mod plat: fara operatii cu cod_op_service }) resp = client.get(f"/_fragments/trimitere/{sid}") assert resp.status_code == 200 # Optiunea trebuie sa arate 'FRN1 — Sistem de franare', nu doar 'FRN1' assert "FRN1 — Sistem de franare" in resp.text, ( f"Picker plat nu arata denumirea: " f"{resp.text[resp.text.find('FRN1'):resp.text.find('FRN1')+80] if 'FRN1' in resp.text else 'FRN1 absent'}" ) def test_adauga_cod_extra_in_mod_operatii(client): """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") csrf = _csrf(client) # Chips stare: 1 operatie deja mapata (mod ops) → _has_ops = True resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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] # FRN1 trebuie sa apara in raspuns (chip extra adaugat) assert "FRN1" in resp.text, ( f"Codul extra FRN1 nu a fost adaugat in mod operatii: {resp.text[:300]}" ) # OE-1 trebuie sa ramana (chip original neatins) assert "OE-1" in resp.text, f"Chip original OE-1 disparut: {resp.text[:300]}" def test_extra_cod_persistat_la_salvare(client): """Codul extra adaugat via form-chips e salvat la /corecteaza. Simulam starea form dupa add_extra: hidden inputs pentru op mapata (OE-1) + hidden inputs pentru chip extra flat (FRN1, fara op_service). """ acct = _create_account_user("extra.persist@test.com") _login(client, "extra.persist@test.com") _seed_cod("OE-1", "Schimb ulei") _seed_cod("FRN1", "Sistem de franare") sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops( "WVWZZZ1JZXW0XP001", [("SchimbUlei", "Schimb ulei motor")], )) csrf = _csrf(client) # Form state dupa add_extra: op mapata (idx=0, OE-1) + chip extra flat (idx=1, FRN1) resp = client.post( f"/trimitere/{sid}/corecteaza", data={ "csrf_token": csrf, "cod_prestatie": ["OE-1", "FRN1"], # OE-1 pt op, FRN1 chip extra "chip_op_service": ["SchimbUlei", ""], # idx 0 are op_service, idx 1 nu "chip_denumire": ["Schimb ulei motor", ""], }, ) assert resp.status_code == 200, resp.text[:300] r = _row(sid) assert r["status"] == "queued", f"status asteptat queued, got {r['status']}" prestatii = _payload_json(sid)["prestatii"] coduri = [p.get("cod_prestatie") for p in prestatii] assert "OE-1" in coduri, f"OE-1 (op mapata) lipsa: {prestatii}" assert "FRN1" in coduri, f"FRN1 (chip extra) lipsa: {prestatii}" def test_extra_cod_validat_nomenclator(client): """'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 fara tinta cu cod INVALID (XX-99 nu e in nomenclator) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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 in starea persistata (chips_state) import re as _re 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): """Codul ales in picker flat se aplica la /corecteaza fara a apasa '+'.""" acct = _create_account_user("picker.save.nobutton@test.com") _login(client, "picker.save.nobutton@test.com") _seed_cod("OE-1", "Schimb ulei motor") # Submission flat fara prestatii sid = _insert(acct, status="needs_mapping", payload={ "vin": "WVWZZZ1JZXW0PS001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [], }) csrf = _csrf(client) # Browser trimite chips_add_cod_flat=OE-1 (ales in picker) dar FARA hidden cod_prestatie # (userul nu a apasat '+' sa promoveze selectia intr-un chip). resp = client.post( f"/trimitere/{sid}/corecteaza", data={ "csrf_token": csrf, "chips_add_cod_flat": "OE-1", # ales in picker, ne-aprobat prin '+' # NU exista 'cod_prestatie' in form (zero hidden chips) }, ) assert resp.status_code == 200, resp.text[:300] r = _row(sid) assert r["status"] == "queued", ( f"Codul ales in picker trebuia sa se aplice la salvare fara '+': status={r['status']}" ) prestatii = _payload_json(sid)["prestatii"] coduri = [p.get("cod_prestatie") for p in prestatii] assert "OE-1" in coduri, f"OE-1 (ales in picker) lipsa din prestatii: {prestatii}" def test_salvare_fara_chip_explicit_nu_e_no_op(client): """O trimitere needs_mapping cu cod ales in picker nu ramane no-op. Complementar cu test_cod_ales_in_picker_se_salveaza_fara_buton_add: verifica explicit ca statusul se schimba (nu ramane needs_mapping). """ acct = _create_account_user("noop.previne@test.com") _login(client, "noop.previne@test.com") _seed_cod("FRN1", "Sistem de franare") sid = _insert(acct, status="needs_mapping", payload={ "vin": "WVWZZZ1JZXW0NP001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [], }) old_status = _row(sid)["status"] assert old_status == "needs_mapping" csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/corecteaza", data={ "csrf_token": csrf, "chips_add_cod_flat": "FRN1", }, ) assert resp.status_code == 200 new_status = _row(sid)["status"] assert new_status != "needs_mapping", ( f"Salvarea cu cod ales in picker trebuia sa nu fie no-op: status ramas {new_status}" ) assert new_status == "queued", f"status asteptat queued, got {new_status}" def test_picker_by_index_op2_nu_op1(client): """Codul ales pentru op#2 aterizeaza pe op#2, NU pe 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). """ _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") csrf = _csrf(client) # Chips: op#1 (idx=0) deja mapata cu OE-1, op#2 (idx=1) nemapata (cod gol) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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_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 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): """Empty-state vizibil cand nomenclatorul e gol. Un rand needs_mapping fara nomenclator randeaza div.chips-nom-gol, altfel nu are nicio cale de a adauga cod. """ acct = _create_account_user("empty.nom@test.com") _login(client, "empty.nom@test.com") # Golim nomenclatorul: seed_nomenclator_if_empty populeaza la initializare DB; # testul simuleaza cazul extrem cand tabla e goala (post-update, inainte de re-seed). from app.db import get_connection as _gconn _c = _gconn() _c.execute("DELETE FROM nomenclator_rar") _c.commit() _c.close() sid = _insert(acct, status="needs_mapping", payload={ "vin": "WVWZZZ1JZXW0EN001", "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "50000", "prestatii": [], }) resp = client.get(f"/_fragments/trimitere/{sid}") assert resp.status_code == 200 assert "chips-nom-gol" in resp.text, ( f"Empty state 'chips-nom-gol' lipsa cand nomenclatorul e gol: {resp.text[resp.text.find('chips'):resp.text.find('chips')+200] if 'chips' in resp.text else resp.text[:500]}" ) def test_add_extra_semnal_vizibil_cod_invalid(client): """'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. """ _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 fara tinta cu cod necunoscut in nomenclator resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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 assert "chips-extra-error" in resp.text, ( f"Semnalul 'chips-extra-error' lipsa pentru cod invalid: {resp.text[:300]}" ) def test_add_per_op_cod_invalid_afiseaza_mesaj(client): """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") csrf = _csrf(client) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "chips_state": json.dumps({"v": 1, "items": [ {"cod": "", "op": "SchimbUlei", "den": "Schimb ulei", "exclus": 0}, ]}), "chips_action": "add", "chips_target_index": "0", "chips_pick": "XX-99", # cod inexistent }, ) assert resp.status_code == 200 assert "chips-extra-error" in resp.text, ( f"Semnal de eroare lipsa la add cu cod invalid: {resp.text[:300]}" ) def test_add_flat_cod_invalid_afiseaza_mesaj(client): """Actiunea 'add_flat' cu cod invalid da semnal vizibil (nu no-op silentios).""" _create_account_user("add.flat.err@test.com") _login(client, "add.flat.err@test.com") _seed_cod("OE-1", "Schimb ulei") csrf = _csrf(client) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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 cu cod invalid: {resp.text[:300]}" ) def test_add_flat_duplicat_afiseaza_mesaj(client): """'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") csrf = _csrf(client) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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 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 fara tinta: {resp.text[:300]}" ) def test_add_extra_duplicat_afiseaza_mesaj(client): """'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") _seed_cod("FRN1", "Franare") csrf = _csrf(client) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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 fara tinta: {resp.text[:300]}" ) def test_add_reusit_marcheaza_chip_nou(client): """Adaugare reusita: chip-ul nou e evidentiat (chip-nou) + mesaj de confirmare.""" _create_account_user("add.ok.nou@test.com") _login(client, "add.ok.nou@test.com") _seed_cod("OE-1", "Schimb ulei") _seed_cod("FRN1", "Franare") csrf = _csrf(client) resp = client.post( "/form-chips", data={ "csrf_token": csrf, "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 assert "chip-nou" in resp.text, f"Chip nou neevidentiat: {resp.text[:300]}" assert "adaugat" in resp.text, f"Mesaj de confirmare lipsa: {resp.text[:300]}"