From 6d10f92452b99a09a1593f739c33181a44328e72 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 24 Jun 2026 21:17:53 +0000 Subject: [PATCH] feat(5.9): US-001 - eticheta umana scurta pe rand + cod brut pentru modal (R1) - _submission_row_view expune eticheta_problema (motiv || eticheta_scurta), gol pe queued/sending/sent, fara decoder nou (R1 DRY) - parse_erori expune cheia `cod` (cod brut catalog) pe ramurile imbogatite, pentru derivare in modal - 5 teste US-001 in tests/test_web_submissions.py - gates: tests PASS (819), /review (backend) PASS Co-Authored-By: Claude Opus 4.8 (1M context) --- app/web/labels.py | 4 ++ app/web/routes.py | 26 +++++++++++- scripts/ralph/prd.json | 4 +- scripts/ralph/progress.txt | 28 +++++++++++++ tests/test_web_submissions.py | 78 +++++++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 3 deletions(-) diff --git a/app/web/labels.py b/app/web/labels.py index d3d3419..388a033 100644 --- a/app/web/labels.py +++ b/app/web/labels.py @@ -275,6 +275,8 @@ def parse_erori(rar_error: object) -> list[dict]: "cauza": e.get("cauza") or e.get("message") or "", "fix": e.get("fix") or "", "field": e.get("field"), + # Cod BRUT de catalog (ex. RAR_EROARE_SERVER) — DOAR pentru modal (US-001/R1). + "cod": e.get("cod"), }) else: # Forma veche: {field, message} fara cod @@ -303,6 +305,8 @@ def parse_erori(rar_error: object) -> list[dict]: "cauza": data.get("cauza") or "", "fix": data.get("fix") or "", "field": data.get("field"), + # Cod BRUT de catalog (ex. COD_NEMAPAT) — DOAR pentru modal (US-001/R1). + "cod": data.get("cod"), }] # Dict vechi: unmapped if "unmapped" in data: diff --git a/app/web/routes.py b/app/web/routes.py index 422d5be..d3130b8 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -649,9 +649,31 @@ def _is_iso_date(value: object) -> bool: return False +# Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana +# scurta de pe rand (US-001, R1) e ne-goala DOAR pe acestea; pe queued/sending/sent e "". +_STARI_CU_PROBLEMA = ("error", "needs_data", "needs_mapping") + + +def _eticheta_problema(status: str, motiv: str) -> str: + """Eticheta umana scurta a problemei pentru randul de tabel (US-001, R1). + + Reutilizeaza `motiv` (motiv_uman, deja calculat in randul de view) si cade pe + `eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (R1: DRY, fara + al 3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand. + + Sir gol pe stari fara problema (queued/sending/sent); ne-gol pe error/needs_*. + Defensiv: motiv_uman nu arunca, iar starile cu problema au intotdeauna eticheta + scurta -> fallback-ul garanteaza un text ne-gol chiar la `rar_error` lipsa/corupt. + """ + if status not in _STARI_CU_PROBLEMA: + return "" + return motiv or eticheta_scurta(status) + + def _submission_row_view(r) -> dict: """Imbogateste un rand de submission cu campuri afisabile umane (US-003/US-004).""" eticheta = eticheta_stare(r["status"]) + motiv = motiv_uman(r["status"], r["rar_error"]) return { "id": r["id"], "status": r["status"], @@ -662,7 +684,9 @@ def _submission_row_view(r) -> dict: "prez": prezentare_din_payload(r["payload_json"]), "id_prezentare": r["id_prezentare"], "updated_at": format_data_rar(r["updated_at"]), - "motiv": motiv_uman(r["status"], r["rar_error"]), + "motiv": motiv, + # US-001/R1: eticheta umana scurta a problemei sub pill (text, nu cod brut). + "eticheta_problema": _eticheta_problema(r["status"], motiv), # US-011: randurile blocate (error/needs_data/needs_mapping) sunt selectabile # pentru stergere bulk; sent/sending/queued raman read-only (fara checkbox). "gestionabil": r["status"] in _GESTIONABILE_WEB, diff --git a/scripts/ralph/prd.json b/scripts/ralph/prd.json index 7e4b170..b0b0a9d 100644 --- a/scripts/ralph/prd.json +++ b/scripts/ralph/prd.json @@ -33,12 +33,12 @@ "dependsOn": [], "requiresBrowserCheck": false, "requiresDesignReview": false, - "passes": false, + "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", - "notes": "" + "notes": "Atins: app/web/routes.py (_eticheta_problema + _STARI_CU_PROBLEMA, camp eticheta_problema in _submission_row_view, motiv hoisted), app/web/labels.py (parse_erori expune `cod` brut aditiv pe ramurile imbogatite), tests/test_web_submissions.py (5 teste US-001). gates: tests PASS (819 suite, 22 fisier), /review (backend) PASS (no issues — DRY respectat, defensiv, aditiv non-regresie)." }, { "id": "US-003", diff --git a/scripts/ralph/progress.txt b/scripts/ralph/progress.txt index 7331d5c..7956193 100644 --- a/scripts/ralph/progress.txt +++ b/scripts/ralph/progress.txt @@ -5,3 +5,31 @@ Branch: ralph/5.9-ux-corectie-modal-mobil Source PRD: docs/prd/prd-5.9-ux-corectie-modal-mobil.md Note: PRD APROBAT 2026-06-24 cu revizii obligatorii R1-R12 (raport AUTOPLAN). R1-R12 au prioritate unde difera de AC original — sunt deja incorporate in acceptance criteria. --- + +## Iteratie: 2026-06-24 +### Story implementat: US-001 - Eticheta umana scurta pe randul de afisare + cod brut disponibil pentru modal (R1) (tags: backend) +### Status: Complete + +### Gates rulate: +- Typecheck: SKIP (techStack.commands.typecheck gol) +- Lint: SKIP (techStack.commands.lint gol) +- Tests: PASS (tests/test_web_submissions.py 22; suita completa 819 passed, 1 deselected) +- /review (backend): PASS — no issues (DRY respectat, defensiv, aditiv non-regresie) + +### Ce s-a schimbat: +- app/web/routes.py: helper `_eticheta_problema(status, motiv)` + constanta `_STARI_CU_PROBLEMA`; + `_submission_row_view` hoisteaza `motiv` intr-o variabila si adauga campul `eticheta_problema` + (text uman scurt: motiv || eticheta_scurta; gol pe queued/sending/sent). +- app/web/labels.py: `parse_erori` expune cheia `cod` (cod brut de catalog) pe ramurile imbogatite + (lista + dict), pentru ca modalul (US-004) sa-l deriveze via parse_erori(...)[0]['cod'] — fara decoder nou (R1 DRY). +- tests/test_web_submissions.py: 5 teste US-001 (sub_pill R1 rename, error/needs_mapping prezent, gol pe ok, defensiv JSON invalid). + +### Learnings: +- `parse_erori` traieste in app/web/labels.py (nu app/errors.py cum sugera AC#4) — `app/errors.py` defineste CATALOG-ul de coduri si `eroare()`. +- `_GESTIONABILE_WEB` == aceleasi 3 stari cu problema; am definit `_STARI_CU_PROBLEMA` separat pentru claritate semantica (gestionabil != are-problema conceptual). +- Eticheta de pe rand e adaugata DOAR in view-model; randarea sub pill apartine US-002 (de aceea testele sunt unitare pe _submission_row_view). + +### Next: +- US-003 (modal, ui, requiresDesignReview) — independent, priority 15. +- US-002 depinde de US-001 (acum done) + US-003. +--- diff --git a/tests/test_web_submissions.py b/tests/test_web_submissions.py index b6922bc..747333a 100644 --- a/tests/test_web_submissions.py +++ b/tests/test_web_submissions.py @@ -195,6 +195,84 @@ def test_detaliu_trimitere(client): assert "99001" in html # nr prezentare RAR +# --------------------------------------------------------------------------- +# US-001 (R1): eticheta umana scurta a problemei pe randul de tabel. +# Teste pure pe _submission_row_view — randarea sub pill e US-002. +# --------------------------------------------------------------------------- + +def _fake_row(status: str, *, rar_error=None, id_prezentare=None, payload=None) -> dict: + """Rand minimal compatibil cu _submission_row_view (acces prin chei).""" + return { + "id": 1, + "status": status, + "id_prezentare": id_prezentare, + "updated_at": "2026-06-24T10:00:00", + "rar_error": rar_error, + "payload_json": json.dumps(payload or { + "vin": "WVWZZZ1JZXW000777", + "nr_inmatriculare": "B777ZZZ", + "data_prestatie": "2026-06-18", + "odometru_final": "55000", + "prestatii": [{"cod_prestatie": "OE-2", "denumire": "Revizie"}], + }), + } + + +def test_eticheta_umana_sub_pill(): + """R1: randul expune `eticheta_problema` umana, reutilizand motiv (nu un nou decoder).""" + from app.web.routes import _submission_row_view + v = _submission_row_view(_fake_row( + "needs_data", + rar_error=json.dumps([{"field": "odometru_final", "message": "lipsa odometru"}]), + )) + # Eticheta = text uman scurt, reutilizeaza motiv (ne-gol) + assert v["eticheta_problema"], "eticheta_problema ar trebui ne-goala pe needs_data" + assert v["eticheta_problema"] == v["motiv"] + # NU expune cod brut de catalog pe rand + assert "COD_" not in v["eticheta_problema"] + assert "RAR_EROARE" not in v["eticheta_problema"] + + +def test_eticheta_problema_prezenta_pe_error(): + """Eticheta ne-goala pe error chiar fara rar_error (fallback pe eticheta_scurta).""" + from app.web.routes import _submission_row_view + # error cu mesaj RAR + v_err = _submission_row_view(_fake_row( + "error", rar_error=json.dumps({"cod": "RAR_EROARE_SERVER", "problema": "Eroare server RAR"}), + )) + assert v_err["eticheta_problema"] == "Eroare server RAR" + # error fara rar_error -> fallback ne-gol (eticheta scurta) + v_fb = _submission_row_view(_fake_row("error", rar_error=None)) + assert v_fb["eticheta_problema"] == "Eroare" + + +def test_eticheta_problema_prezenta_pe_needs_mapping(): + """Eticheta ne-goala pe needs_mapping (stare cu problema).""" + from app.web.routes import _submission_row_view + v = _submission_row_view(_fake_row( + "needs_mapping", + rar_error=json.dumps({"unmapped": [{"cod_op_service": "OP-9", "denumire": "X"}]}), + )) + assert v["eticheta_problema"] + assert "OP-9" in v["eticheta_problema"] + + +def test_eticheta_problema_goala_pe_rand_ok(): + """Eticheta este sir gol pe stari fara problema (queued/sending/sent).""" + from app.web.routes import _submission_row_view + for status in ("queued", "sending", "sent"): + v = _submission_row_view(_fake_row(status)) + assert v["eticheta_problema"] == "", f"{status} ar trebui sa aiba eticheta goala" + + +def test_eticheta_problema_defensiva_json_invalid(): + """rar_error JSON corupt pe stare cu problema -> nu ridica, eticheta ne-goala.""" + from app.web.routes import _submission_row_view + v = _submission_row_view(_fake_row("error", rar_error="{invalid json[[[")) + assert isinstance(v["eticheta_problema"], str) + assert v["eticheta_problema"] # fallback garanteaza text ne-gol + + def test_detaliu_trimitere_404_cross_account(client): """Detaliul altui cont -> 404 (fara leak).""" acct1 = _create_account_user("d1@test.com", name="C1")