"""Teste US-006 (PRD 5.15): prestatii multi-cod (lista) la editare/corectie. AC-uri verificate: - Handler-ele accepta LISTA de cod_prestatie (form.getlist) -> prestatii cu mai multe coduri. - cod_op_service/denumire RAMAN pe item (invariant D7, E1 IRON RULE). - 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. - REGRESIE E1 (IRON RULE): op_service supravietuieste /repune cu cod. TDD: toate testele sunt scrise INAINTE de implementare (RED -> GREEN). """ 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): """US-006 AC1: LISTA de cod_prestatie -> prestatii cu N itemi, fiecare cu cod setat. RED: form.get("cod_prestatie") intoarce doar primul cod; form.getlist necesar. """ 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): """E1/D7: cod_op_service si denumire RAMAN pe item dupa /corecteaza cu cod direct. RED: implementarea veche injecta in prestatii[0] fara sa afecteze op_service (intr-adevar in /corecteaza nu se facea pop), dar testul confirma explicit invariantul. """ 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): """US-006 AC3: cod necunoscut in nomenclator -> respins cu mesaj, status neschimbat. RED: validarea fata de nomenclator nu e aplicata per-cod la multi-select. """ 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): """US-006 AC4: nicio cod_prestatie trimis -> submission ramane needs_mapping. RED: cu multi-select, lista goala nu injecteaza nimic; resolve_prestatii gaseste inca operatii nemapate -> trebuie sa ramana 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): """US-006 AC6: dupa setarea de coduri noi, cheia de idempotenta e recalculata. RED: single-cod injecta in prestatii[0] si recalcula cheia; cu multi-cod acelasi mecanism se aplica tuturor itemilor. """ 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): """US-006 AC7: cod_prestatie=R-ODO fara odometruInitial -> validate_prezentare intoarce eroare -> submission ramane needs_data (NU queued). RED: validarea R-ODO e deja in validate_prezentare; testul confirma ca multi-cod nu bypass-eaza aceasta regula. """ 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): """US-006 AC5 (E4): 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. RED: dedupare naiva dupa cod ar sterge a doua operatie (op-B cu acelasi OE-1). """ 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}" # --------------------------------------------------------------------------- # # Test de regresie E1 (IRON RULE): op_service supravietuieste /repune cu cod # # --------------------------------------------------------------------------- # def test_op_service_supravietuieste_repune_cu_cod(client): """E1 IRON RULE: dupa /repune cu cod_prestatie, cod_op_service/denumire RAMAN pe item. RED: routes.py:1371 face `p0.pop("cod_op_service", None)` — sterge operatia cand se seteaza un cod direct prin /repune. US-006 ELIMINA acel pop. Aceasta regresie e CRITICA: sterge contextul op->cod necesar pentru US-009 (salvare mapare din chip) si rupe invariantul D7. """ 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): """Bug fix (code-review 5.15): /repune NU pierde prestatii[1:]. Formularul /repune trimite UN SINGUR select cod_prestatie. Implementarea veche itera `enumerate(codes)` -> pastra doar len(codes) itemi, deci un rand error cu 2+ prestatii pierdea toate prestatiile dupa prima -> declaratie INCOMPLETA la RAR (FINALIZATA ireversibil). Fix: iteram peste `existing`, aplicam codes pozitional, pastram toate prestatiile. RED inainte de fix: len(prestatii) == 1 (a doua prestatie pierduta). """ 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): """Bug fix (code-review 5.15): re-render-ul de eroare validare pastreaza optiunile pickerului. post_corectie_trimitere re-randa _trimitere_detaliu pe ramura erori-validare FARA `conn`/`account_id` -> `nomenclator_rar=[]` -> picker-ul chips randa ZERO optiuni -> userul nu mai poate alege cod RAR fara sa inchida+redeschida modalul. Fix: pasam `conn`+`account_id` la _detaliu_ctx pe TOATE ramurile de re-render. RED inainte de fix: codul de picker "PK-1" lipseste din re-render. """ 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): """Bug fix (code-review 5.15): selectul /repune afiseaza denumirea operatiei. Template-ul folosea cheia gresita `item.nome_prestatie` (typo) -> optiunile apareau ca "AAA — " fara denumire. Cheia corecta e `nume_prestatie`. """ 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]}" ) # ============================================================================= # # Teste noi 5.16: US-004 (denumiri picker), US-005 (add_extra), # # US-006 (save picker fara buton), T-E3 (by-index), T-D1/T-E5, T-C1/T-E4 # # ============================================================================= # def test_picker_flat_arata_cod_si_denumire(client): """US-004 (5.16): picker plat afiseaza 'cod — denumire', nu doar codul. RED: _chips_prestatii.html:147 afiseaza doar {{ n.cod_prestatie }}; modul operatii (:101) afiseaza deja 'cod — nume'. Fix: uniformizare. """ 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): """US-005 (5.16): in mod operatii, actiunea add_extra adauga un cod RAR liber. RED: post_form_chips nu are actiunea 'add_extra' -> chips_action ignorata. """ acct = _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, "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 }, ) 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): """US-005 (5.16): 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): """US-005 (5.16): add_extra respinge cod necunoscut in nomenclator (invariant ORA-12899). RED: actiunea add_extra nu exista; dupa fix, cod invalid nu se adauga. """ acct = _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) 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 }, ) assert resp.status_code == 200 html = resp.text # XX-99 NU trebuie sa apara ca chip valid (hidden input cu valoarea XX-99) import re as _re hidden_xx99 = _re.search(r']+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]}" ) def test_cod_ales_in_picker_se_salveaza_fara_buton_add(client): """US-006 (5.16): codul ales in picker flat se aplica la /corecteaza fara a apasa '+'. RED: post_corectie_trimitere citeste form.getlist('cod_prestatie') (hidden inputs) dar ignora 'chips_add_cod_flat' (picker neselectat ca chip) → submission ramane needs_mapping desi codul e ales. """ 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): """US-006 (5.16): 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): """T-E3 (5.16): codul ales pe picker-ul 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). """ acct = _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, "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_action": "add", "chips_add_op_index": "1", # adauga pe op#2 (idx=1) "chips_add_cod_1": "FRN1", # picker-ul op#2 contine FRN1 }, ) assert resp.status_code == 200, resp.text[:300] html = resp.text import re as _re hidden_vals = _re.findall(r']+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}" ) def test_empty_state_picker_nomenclator_gol(client): """T-D1/T-E5 (5.16): empty-state vizibil cand nomenclatorul e gol. RED: {% if nomenclator_rar %} fara {% else %} -> silentios; un rand needs_mapping fara nomenclator nu are nicio cale de a adauga cod (nereparabil silentios). GREEN: div.chips-nom-gol vizibil. """ 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): """T-C1/T-E4 (5.16): add_extra cu cod invalid da semnal vizibil (nu esua silentios). RED: actiunea add_extra nu exista → nu exista niciun semnal. GREEN: div.chips-extra-error vizibil cand codul e invalid sau selectul e gol. """ acct = _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 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 }, ) assert resp.status_code == 200 assert "chips-extra-error" in resp.text, ( f"Semnalul 'chips-extra-error' lipsa pentru cod invalid: {resp.text[:300]}" )