diff --git a/app/api/v1/router.py b/app/api/v1/router.py index ecad129..e9c05b0 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -77,13 +77,61 @@ def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode) -> def _erori_nemapate(unmapped: list[dict]) -> list[dict]: - """Erori 3 niveluri (COD_NEMAPAT) pentru raspunsul on_unmapped_error=True.""" + """Coduri nemapate imbogatite cu 3 niveluri (COD_NEMAPAT), pentru raspuns onest.""" return [ {**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} necunoscut/fara mapare RAR")} for u in unmapped ] +def _motiv_clasificare(cl: dict) -> str | None: + """Rezumat uman pe o linie pentru un rezultat de clasificare (PRD 5.7). + + None cand status='queued'. Acopera toate ramurile de blocaj: erori de continut + (needs_data), coduri nemapate (needs_mapping) si auto_send oprit (needs_mapping). + """ + if cl["status"] == "queued": + return None + if cl["errors"]: + return "; ".join( + (e.get("problema") or e.get("message") or "") for e in cl["errors"] + ).strip("; ") or "Date incomplete (respinse de RAR)." + if cl["unmapped"]: + coduri = ", ".join((u.get("cod_op_service") or "") for u in cl["unmapped"]) + return f"Coduri fara mapare RAR: {coduri}" + if cl["status"] == "needs_mapping": + return "Cod cu trimitere automata oprita; confirmare manuala inainte de trimitere." + return None + + +def _rezultat_enqueue(submission_id: int | None, cl: dict, **extra) -> SubmissionResult: + """SubmissionResult onest dintr-un rezultat de clasificare (PRD 5.7). + + Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman) + pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None. + """ + return SubmissionResult( + submission_id=submission_id, + status=cl["status"], + erori=list(cl["errors"]), + nemapate=_erori_nemapate(cl["unmapped"]), + motiv=_motiv_clasificare(cl), + **extra, + ) + + +def _rezultat_respins(submission_id: int | None, cl: dict) -> SubmissionResult: + """Rezultat pentru on_unmapped_error=True: status='error', fara enqueue/reactivare. + + `erori` pastreaza COD_NEMAPAT (compat clienti vechi); `nemapate` + `motiv` adaugate. + """ + nem = _erori_nemapate(cl["unmapped"]) + return SubmissionResult( + submission_id=submission_id, status="error", + erori=nem, nemapate=nem, motiv=_motiv_clasificare(cl), + ) + + @router.post("/prezentari", response_model=PrezentariResponse) def create_prezentari( req: PrezentareRequest, @@ -142,10 +190,7 @@ def create_prezentari( cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode) if cl["blocked_error"]: # on_unmapped_error=True: nu reactivam; randul ramane 'error'. - results.append(SubmissionResult( - submission_id=existing["id"], status="error", - erori=_erori_nemapate(cl["unmapped"]), - )) + results.append(_rezultat_respins(existing["id"], cl)) continue cur = conn.execute( "UPDATE submissions SET status=?, payload_json=?, rar_error=?, " @@ -163,9 +208,9 @@ def create_prezentari( "UPDATE accounts SET rar_creds_enc=? WHERE id=?", (encrypt_creds(req.rar_credentials.model_dump()), acct), ) - results.append(SubmissionResult( - submission_id=existing["id"], status=cl["status"], reactivated=True, - )) + # Raspuns onest si la reactivare (PRD 5.7): daca re-clasificarea + # cade pe needs_data/needs_mapping, expune motivul (nu doar status). + results.append(_rezultat_enqueue(existing["id"], cl, reactivated=True)) continue # Cursa: alt POST/requeue a schimbat starea intre SELECT si UPDATE # (rowcount==0) -> raspuns dedup pe starea CURENTA. @@ -188,16 +233,15 @@ def create_prezentari( cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode) if cl["blocked_error"]: # on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat). - results.append(SubmissionResult( - submission_id=None, status="error", erori=_erori_nemapate(cl["unmapped"]), - )) + results.append(_rezultat_respins(None, cl)) continue cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc) " "VALUES (?, ?, ?, ?, ?, ?)", (key, acct, cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], creds_enc), ) - results.append(SubmissionResult(submission_id=int(cur.lastrowid), status=cl["status"])) + # Raspuns onest (PRD 5.7): pe needs_data/needs_mapping expune erori/nemapate/motiv. + results.append(_rezultat_enqueue(int(cur.lastrowid), cl)) # US-004: audit cerere API per cont. Doar metadate (count + distributie status), # NICIUN camp de payload PII integral. Reuse conn (T4 — fara contentie WAL). diff --git a/app/models.py b/app/models.py index 3fec07b..8315450 100644 --- a/app/models.py +++ b/app/models.py @@ -106,9 +106,15 @@ class SubmissionResult(BaseModel): # cheie de continut a fost RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit. # `deduped` pastreaza semantica actuala (clientii vechi care testeaza `deduped` nu se sparg). reactivated: bool = False - # Populat cand status='error' din cauza on_unmapped='error': erori 3 niveluri - # (COD_NEMAPAT) pentru fiecare cod necunoscut/nemapat. Gol altfel. + # Raspuns ONEST pentru randuri blocate (PRD 5.7): orice status != 'queued' isi + # expune motivul, ca integratorul sa nu trateze un needs_data/needs_mapping drept succes. + # erori = validare de continut (needs_data), 3 niveluri [{field, cod, problema, cauza, fix, message}]. + # Pe ramura on_unmapped_error='error' pastreaza COD_NEMAPAT (compat). + # nemapate = coduri fara mapare RAR (needs_mapping / respins), 3 niveluri + cod_op_service/denumire. + # motiv = rezumat uman pe o linie (None cand status='queued'). erori: list[dict] = [] + nemapate: list[dict] = [] + motiv: str | None = None class PrezentariResponse(BaseModel): diff --git a/app/web/routes.py b/app/web/routes.py index bd00333..20c0faa 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -771,10 +771,57 @@ def _payload_form_values(payload_json) -> dict: } +def _nemapate_pentru_submission(row, nomenclator: list[dict]) -> list[dict]: + """Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy (PRD 5.7). + + Echivalentul `pending_unmapped` restrans la un singur rand: parseaza payload_json, + aduna prestatiile fara cod_prestatie (cu cod_op_service) si ataseaza sugestii din + `nomenclator` (pasat de apelant — evita un SELECT redundant in _detaliu_ctx). Goala + daca randul nu e needs_mapping sau nu are operatii nemapate reale (ex. needs_mapping + din auto_send=0 — codul exista deja, doar trimiterea e oprita). + """ + if row["status"] != "needs_mapping": + return [] + try: + content = json.loads(row["payload_json"]) if row["payload_json"] else {} + if not isinstance(content, dict): + content = {} + except (ValueError, TypeError): + content = {} + seen: set[str] = set() + out: list[dict] = [] + for item in content.get("prestatii") or []: + if not isinstance(item, dict) or (item.get("cod_prestatie") or ""): + continue + op = (item.get("cod_op_service") or "").strip() + if not op or op in seen: + continue + seen.add(op) + out.append({ + "cod_op_service": op, + "denumire": item.get("denumire"), + "suggestions": suggest_codes(item.get("denumire"), nomenclator, limit=5), + }) + return out + + def _detaliu_ctx(request: Request, row, *, message: str | None = None, - error: bool = False, corectie_errors: list | None = None) -> dict: - """Context pentru _trimitere_detaliu.html dintr-un rand de submission.""" + error: bool = False, corectie_errors: list | None = None, + conn=None, account_id: int | None = None) -> dict: + """Context pentru _trimitere_detaliu.html dintr-un rand de submission. + + `conn`+`account_id` (optional): cand sunt date si randul e needs_mapping, expune + `nemapate_inline` + `nomenclator` pentru maparea inline din panou (PRD 5.7). + """ eticheta = eticheta_stare(row["status"]) + nemapate_inline: list[dict] = [] + nomenclator: list[dict] = [] + if conn is not None and row["status"] == "needs_mapping": + # Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown. + nomenclator = load_nomenclator(conn) + nemapate_inline = _nemapate_pentru_submission(row, nomenclator) + if not nemapate_inline: + nomenclator = [] # nu expunem dropdown-ul cand nu exista operatii de mapat ctx = { "request": request, "csrf_token": get_csrf_token(request), @@ -797,6 +844,9 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None, "editabil": row["status"] in _CORECTABILE, # US-011: error/needs_data/needs_mapping pot fi sterse / re-puse in coada "gestionabil": row["status"] in _GESTIONABILE_WEB, + # PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator) + "nemapate_inline": nemapate_inline, + "nomenclator": nomenclator, "corectie_msg": message, "corectie_error": error, "corectie_errors": corectie_errors or [], @@ -829,7 +879,64 @@ def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResp row = _fetch_submission_scoped(conn, account_id, submission_id) if not row: raise HTTPException(status_code=404, detail="trimitere inexistenta") - return templates.TemplateResponse("_trimitere_detaliu.html", _detaliu_ctx(request, row)) + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx(request, row, conn=conn, account_id=account_id), + ) + finally: + conn.close() + + +@router.post("/trimitere/{submission_id}/mapeaza", response_class=HTMLResponse) +async def post_mapeaza_inline(request: Request, submission_id: int) -> HTMLResponse: + """Mapare inline din panoul de detaliu (PRD 5.7): alege cod RAR pentru o operatie nemapata. + + Reutilizeaza EXACT save_mapping + reresolve_account (ca tab-ul Mapari) — fara logica + noua de clasificare. Re-rezolva scoped pe batch-ul randului (canal API batch_id IS NULL + SAU import batch), deblocand si randurile-frate cu aceeasi operatie. Scoped pe sesiune + (404 cross-account/inexistent), CSRF obligatoriu, gard pe status needs_mapping. + """ + account_id = require_login(request) + form = await request.form() + verify_csrf(request, str(form.get("csrf_token") or "")) + cod_op_service = str(form.get("cod_op_service") or "").strip() + cod_prestatie = str(form.get("cod_prestatie") or "").strip().upper() + auto_send = str(form.get("auto_send") or "") not in ("", "false", "0", "off") + conn = get_connection() + try: + row = _fetch_submission_scoped(conn, account_id, submission_id) + if not row: + raise HTTPException(status_code=404, detail="trimitere inexistenta") + if row["status"] != "needs_mapping": + raise HTTPException(status_code=403, detail="trimitere fara operatii de mapat") + if not cod_op_service or not cod_prestatie: + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx(request, row, conn=conn, account_id=account_id, error=True, + message="Alege un cod RAR pentru operatie."), + ) + exists = conn.execute( + "SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie,) + ).fetchone() + if not exists: + return templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx(request, row, conn=conn, account_id=account_id, error=True, + message=f"Cod necunoscut in nomenclator: {cod_prestatie}."), + ) + save_mapping(conn, account_id, cod_op_service, cod_prestatie, auto_send) + # Re-rezolva scoped pe canalul randului: batch_id None (API) sau batch import. + reresolve_account(conn, account_id, batch_id=row["batch_id"]) + row2 = _fetch_submission_scoped(conn, account_id, submission_id) + eticheta = eticheta_stare(row2["status"]) + resp = templates.TemplateResponse( + "_trimitere_detaliu.html", + _detaliu_ctx(request, row2, conn=conn, account_id=account_id, + message=f"Mapat {cod_op_service} -> {cod_prestatie}. " + f"Stare noua: {eticheta[0]}."), + ) + resp.headers["HX-Trigger"] = "trimiteriChanged" + return resp finally: conn.close() @@ -895,8 +1002,8 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR row2 = _fetch_submission_scoped(conn, account_id, submission_id) return templates.TemplateResponse( "_trimitere_detaliu.html", - _detaliu_ctx(request, row2, error=True, - message="Lipseste inca un cod RAR — rezolva operatia in tab-ul Mapari."), + _detaliu_ctx(request, row2, conn=conn, account_id=account_id, error=True, + message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."), ) if has_no_auto_send(resolved, mapping_meta): @@ -999,7 +1106,8 @@ async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLRes row = _fetch_submission_scoped(conn, account_id, submission_id) resp = templates.TemplateResponse( "_trimitere_detaliu.html", - _detaliu_ctx(request, row, message="Re-pus in coada — pleaca la urmatoarea trimitere."), + _detaliu_ctx(request, row, conn=conn, account_id=account_id, + message="Re-pus in coada — pleaca la urmatoarea trimitere."), ) resp.headers["HX-Trigger"] = "trimiteriChanged" return resp diff --git a/app/web/templates/_trimitere_detaliu.html b/app/web/templates/_trimitere_detaliu.html index c108884..41692aa 100644 --- a/app/web/templates/_trimitere_detaliu.html +++ b/app/web/templates/_trimitere_detaliu.html @@ -1,4 +1,5 @@ {% from "_eroare.html" import card_erori %} +{% import '_macros.html' as ui %}

Detaliu trimitere #{{ id }}

@@ -65,6 +66,53 @@
{% endif %} + {# === Mapare inline (PRD 5.7): alege cod RAR pentru operatiile nemapate ale acestui rand === #} + {% if nemapate_inline %} +
+

Mapeaza codul operatiei

+

+ Alege codul RAR pentru fiecare operatie. La salvare, randul se re-rezolva pe loc + (si celelalte randuri cu aceeasi operatie). +

+ {% for op in nemapate_inline %} + {% set top = op.suggestions[0] if op.suggestions else None %} + {% set preselect = top.cod_prestatie if (top and top.score >= 60) else '' %} +
+ + +
+ {{ op.cod_op_service }} + {% if op.denumire and op.denumire != op.cod_op_service %} + — {{ op.denumire }} + {% endif %} +
+ {% if op.suggestions %} +
+ Sugestii: + {% for s in op.suggestions[:3] %} + {{ s.cod_prestatie }} ({{ s.score|round|int }}%){% if not loop.last %}, {% endif %} + {% endfor %} +
+ {% endif %} +
+ + {{ ui.autosend_toggle(checked=True) }} + +
+
+ {% endfor %} +
+ {% endif %} + {# === Corectie inline (US-010): doar randuri ne-trimise blocate === #} {% if editabil %} {% set err_map = {} %} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index eb4150d..93cc21e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -48,7 +48,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi > PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata: > schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare". -**Ultima actualizare**: 2026-06-23 — FIX out-of-process (raportat din client VFP): `cod_prestatie` necunoscut in nomenclator era trimis raw la RAR → **HTTP 500** (`ORA-12899`, coloana `COD_PRESTATIE` max 5 car.) + record PARTIAL `FINALIZATA` (RAR ne-tranzactional) pe care reconcilierea il marca fals `sent`. Reparat: validare `cod_prestatie` fata de nomenclator la ingestie (cod necunoscut → tratat ca operatie de mapat, nu se mai trimite raw) + optiune boolean `on_unmapped_error` (`false` default → needs_mapping | `true` → respinge) per-cerere cu default per-cont `accounts.on_unmapped_error_default` (migrare aditiva). Confirmat live raspunsul RAR (500 pe cod intern vs 200 pe `OE-1`). Inclus si in `c842e33`: fix lease orfan worker (nepotrivire format data sending_since vs cutoff → orice rand `sending` parea expirat) + guard anti-dublu-POST + fix UI `hx-confirm` mostenit pe randuri (alerta de stergere la click pe rand). Teste: **748 passed** (cele 2 esecuri pre-existente fara legatura). Contract + CLAUDE.md actualizate. | 5.6 IMPLEMENTAT + VERIFY PASS (asteapta commit). Cele 14 stories din PRD 5.6 livrate TDD (RED->GREEN), `pytest -q` **741 passed, 0 failed**. Lifecycle trimiteri blocate (Val A primul, decizie #18): `app/submissions_admin.py` (sterge/repune scoped, 404-before-409); reactivare dedup peste `error` cu CAS + invalidare sesiune worker la creds noi (T1) + propagare `accounts.rar_creds_enc` (#17) + camp aditiv `reactivated:true` (#19); retentie randuri blocate 30z + `purge_after` curatat la reactivare/requeue (T2); API `DELETE`/`/repune` (200+JSON, #20); UI butoane + bulk + banner "Necesita atentia ta" actionabil cu deep-link. Observabilitate: `app/observ.py log_event` (dublu canal `app_events` DB + `RotatingFileHandler` per-proces, redactare creds/PII la scriere via `app/security.redact_pii`/`vin_partial`), `request_id` middleware + `X-Request-ID` pe toate raspunsurile (T8), handler global excepții -> 500 envelope 6-chei + request_id (T7), audit cerere API (`api_prezentari`/`api_auth_esuat`) + audit worker (`rar_login`/tranzitii), tab "Jurnal" filtrabil scoped (non-admin doar contul sau), retentie jurnal 90z. Live RAR `--send` NEPROBAT in sesiune (recomandat la deploy: confirma `rar_login` ok + `submission_sent` in jurnal). PRD actualizat cu raport VERIFY; contract actualizat cu endpointurile noi (T10). | ISTORIC: HOTFIX livrat + 5.6 APROBAT. Hotfix 500 pe `POST /v1/prezentari` (raportat din client Visual FoxPro): `AUTOPASS_CREDS_KEY` din `.env` nu respecta formatul Fernet (32 bytes url-safe base64) → `ValueError` la primul `encrypt_creds` → 500 brut. Reparat: cheie Fernet valida in `.env` + `crypto.validate_creds_key()` apelata in `main.lifespan` (fail-fast la startup, mesaj clar in loc de 500 la primul POST). Confirmat live: POST VFP → 200 `queued`; trimitere reala pe RAR test → `sent idPrezentare=68818` (verificat independent in finalizate). Corectat si mesajul fals din dashboard pentru starea `error` in `labels.py` ("se reincearca automat" → starea e terminala, NU se reincearca). Investigatia a expus 3 goluri structurale (500 brut fara traducere 3 niveluri; lipsa jurnal de aplicatie la nivel de eveniment; lacune de lifecycle — randuri blocate permanente, dedup blocat de un rand `error`, banner "Necesita atentia ta" neactionabil) → **PRD 5.6 APROBAT** (14 stories; decizii §5 rezolvate cu user). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md). | ISTORIC: 5.5 LIVRAT (uniformizare/standardizare UI/UX: tabele la grila Trimiteri, meniu hamburger + tab-bar redus Acasa/Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk pe model nou `accounts.status`. 9 stories in 3 valuri, UI pur cu o singura exceptie backend = stare cont; stergere soft cu purjare PII imediata GDPR. VERIFY 671 teste + E2E browser (2 bug-uri prinse) + `/code-review high` (2 bug-uri reale reparate). Commit `1fbd894`, vezi randul 5.5). | ISTORIC: 5.4 LIVRAT (Erori pe 3 niveluri problema+cauza+fix pe API si UI: catalog central pur `app/errors.py` ca SINGURA sursa de adevar cod→{problema,fix}, consumat de API+UI+worker — face imposibila divergenta intre canale, acelasi invariant ca 5.2. 8 stories in 5 valuri. Tot ADITIV: `field`/`message`/`error` pastrate la octet, adaugam `cod/problema/cauza/fix`; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare; login/signup/CSRF neatinse. UI progresiv: lista compacta, 3 niveluri complete in detaliu/preview, AA light+dark. VERIFY context curat PASS 628 teste (byte-compat+superset verificate direct, E2E API+web; live RAR neprobat — lipsa creds key). `/code-review high`: 2 bug-uri reale reparate in `labels.py` (`motiv_uman` fara ramura 3-niveluri → 401 creds garbled in coloana Motiv; `parse_erori` element gol pe `{}`). 631 teste. Backend trimitere + schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md)). | ISTORIC: 5.3 LIVRAT (Light/Dark mode: tema light ca bloc `[data-theme="light"]` peste variabilele `:root` — dark NESCHIMBAT la octet; comutator soare/luna in header pe toate paginile, default OS-aware cu fallback dark, persistenta `localStorage` doar la comutare explicita, script anti-FOUC in `` pre-paint; suprafetele de stare hardcodate convertite la `color-mix` in `base.html` + 7 fragmente. Zero backend — pur frontend. VERIFY 2 runde: r1 FAIL a prins literalii dark ramasi in 7 fragmente HTMX (text invizibil in light, test vacuu pe doar base.html) → fix US-003 + test care scaneaza fragmentele; r2 PASS E2E browser (banner light ~13:1 contrast, toggle instant+persista+anti-FOUC, dark identic). `/code-review` high: 1 finding reparat (light `--ok` green sub AA ca text → green-700, ~5.0:1). 584 teste. PRD: [prd-5.3](prd/prd-5.3-light-dark-mode.md)). | ISTORIC: 5.2 LIVRAT (Endpoint dry-run `POST /v1/prezentari/valideaza`: valideaza payload + mapare si intoarce verdictul real — `status_estimat` queued/needs_data/needs_mapping + erori `[{field,message}]` + coduri nemapate + prestatii rezolvate — FARA enqueue, FARA creds, zero scriere DB). 1 story TDD. Cheia de design: helper pur partajat `classify_prezentare` folosit de AMBELE rute, ca dry-run-ul sa nu poata diverge de trimiterea reala (invariant de corectitudine); `create_prezentari` refactorizat pe el cu comportament identic. Scope minim per decizie user: doar validare+mapare (fara idempotency/duplicat, `idempotency.py` neatins), hub `/integrare` amanat ca follow-up (descoperibilitate). VERIFY context curat PASS (577 teste; E2E API cu cele 3 verdicte + COUNT(*)=0 dupa dry-run + fara leak creds in raspuns; regresia de aur verde; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint read-only nu atinge worker/coada/schema). `/code-review` high: 0 findings (refactor faithful, mutable-default Pydantic-safe, import local necesar anti-circular). PRD: [prd-5.2](prd/prd-5.2-dryrun-valideaza.md). | ISTORIC: 5.1 LIVRAT (Hub de integrare `/integrare`: exemple cod multi-limbaj + retetar VFP cu 2 dialecte + `GET /v1/ping` readiness + export Postman/OpenAPI + "Testeaza conexiunea"). 4 stories in 2 valuri (Val 1 = US-001/US-002/US-004 paralel pe fisiere disjuncte via Agent team; Val 2 = US-003 UI). Atentie operationala: US-003 a rulat intr-un worktree branched din ultimul commit (FARA modificarile necomise ale US-004 din working-tree) si la "copiere manuala" a SUPRASCRIS `routes.py`, stergand ruta `POST /integrare/test-cheie` (8 teste 404) — reparat prin re-aplicarea rutei de catre autorul US-004 pe `routes.py` curent. Lectie: stories care ating acelasi fisier in valuri diferite + worktree = clobber daca worktree-ul nu vede working-tree-ul; foloseste fisiere disjuncte SAU merge atent de catre lead. VERIFY context curat PASS (568 teste) + E2E browser Playwright (deep-link server-side, IA pe 2 niveluri, VFP cu 3 niveluri de tab comuta corect, copy, htmx test-cheie → fragment eroare, 0 erori consola) + enqueue live (`POST /v1/prezentari` → queued); live RAR `FINALIZATA` NEPROBAT in sesiune (lipsa `AUTOPASS_CREDS_KEY`/creds RAR test) — risc minim, backend trimitere NEATINS. `/code-review` high a prins 4 bug-uri reale (toate in suprafata noua, reparate + lock-uite cu teste): snippet C# JSON multi-linie nevalid (CS1010), snippet VFP `json.dumps(indent=0)` inca cu newline-uri → string literal rupt in ambele dialecte, snippet Node `node:buffer` nu exporta FormData → TypeError, script `_integrare.html` ne-scoped acumuland event-listeneri pe tab-bar-ul principal la fiecare swap htmx (scoped pe `#integrare-section`). Notat ca cleanup viitor (nereparat): `_render_integrare` dubleaza SQL `are_creds`/`are_cheie`, `ping` cu 2 conexiuni DB + `account_for_key` de 2 ori, `_campuri_obligatorii` necache-uit, panouri limbaj copy-paste (candidat macro Jinja2). Backend trimitere (worker/masina stari/idempotenta/mapping) si schema NEATINSE. PRD: [prd-5.1](prd/prd-5.1-hub-integrare.md). | ISTORIC: 3.6 INCHIS (editare celule in preview + Acasa unificata). CLOSE: `/code-review` high a prins 1 bug real (decriptare `override_json` neprotejata de try/except in ambele cai de preview — 500 pe tot batch-ul la rotatie cheie Fernet vs. `raw_json` care degrada gratios), reparat in `import_router.preview_import` + `routes._web_compute_preview`; duplicarea `_override_of`/canonicalize notata ca cleanup viitor. 523 teste pass. 7 stories in 3 valuri, executate de 2 echipe in paralel (TeamCreate) pe fisiere disjuncte (core: routes/import/templates; mapari: `_mapari.html`) + US-007 secvential. Livrate: tab "Trimiteri" eliminat→sectiune "Trimiterile tale" sub upload pe Acasa (US-003); upload bara slim accentuata cu hero la first-run (US-004); editare de celule in preview prin `import_rows.override_json` (Approach B, Fernet, patch canonic aplicat ULTIMUL in `_resolve_row_for_preview`+`commit_import` — completeaza inclusiv coloane ABSENTE din fisier), mutatie pura cu status rederivat (US-001); buton Editeaza pe rand cu swap pe ``+OOB contoare (nu pe sectiune), form propriu, mutual-exclusion, reuse grila `_trimitere_detaliu.html` (US-002); Mapari + formate de coloane ca tabele `.tablewrap`, H4 auto_send stocat (US-005/006); bifa "auto-send"→comutator etichetat pe COADA ("Pune automat in coada"/"Tine pentru verificare"), scoped pe operatie, `name=auto_send` pastrat (zero backend) (US-007). 523 teste pass. **VERIFY**: E2E browser pe `/` (Acasa unificata, upload slim, editare rand needs_data→ok cu swap pe rand + contoare OOB, Mapari tabelar + comutator) + LIVE pe RAR test — import fara coloana data → editarea completeaza data (override) → commit → worker login RAR test → `postPrezentare` → `sent` cu `idPrezentare=68696`, confirmat independent in lista finalizate RAR. 3 bug-uri JS (htmx 1.9.12) prinse DOAR la E2E in browser (invizibile la TestClient) si reparate: `useTemplateFragments=true` (raspunsul ``+OOB era parsat in context de tabel → `swapError` + contoare pierdute), re-activare `confirm-btn` deferita pe tick (race `editing=true` tranzitoriu), `n-hint` ok-count actualizat de `updateN`. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare) NEATINS — singura atingere de schema: 1 coloana nullable `override_json` cu migrare defensiva. PRD: [prd-3.6](prd/prd-3.6-editare-preview-acasa-unificata.md). | ISTORIC: 3.5 LIVRAT (dashboard compact). 11 stories in 4 valuri, TDD. US-001 bara status compacta pe 2 randuri cu bife accesibile (glife ✓/✗ + text, nu doar culoare) + `format_data_rar` (dd.mm.yyyy hh24:mi:ss, helper pur). US-002 Acasa = ecranul de import (upload dominant inline, tab Import scos, `?tab=import`→Acasa fara 404). US-003 helper pur partajat `app/payload_view.py` (payload→campuri afisabile, defensiv, coercion Excel) refolosit si de `GET /v1/prezentari` (DRY). US-004 "Coada"→"Trimiteri": coloane RO + stare umana + detaliu complet la click in panou dedicat `#trimitere-detaliu` (nu inline — poll 10s), scoped 404 cross-account. US-005/006 CRUD mapari operatii + formate coloane salvate (scoped, re-rezolvare auto la edit cod). US-007 "Mapari" 3 sectiuni (de rezolvat / op salvate / formate coloane), "Cont" doar cheie+creds. US-008 motiv (mesaj validare) pe randuri needs_data in preview. US-009 filtre Trimiteri (stare SQL / vehicul+data Python) scoped + "sterge filtrele". US-010 corectie inline needs_data→queued cu payload+idempotency recalculate, sent read-only (403), coliziune idempotency prinsa pre-UPDATE. US-011 badge contoare pe tab-uri (Mapari/Trimiteri), scoped, aria-label. VERIFY context curat PASS (483 teste; E2E browser/RAR LIVE neprobat — recomandata probare manuala `--send`). `/code-review` high a prins 4 findings reale, toate reparate: corectie needs_mapping re-rezolva prestatiile (nu mai poate trimite cod nul la RAR), filtru fara LIMIT silentios, coliziune idempotency atomica (try/except IntegrityError), comparatie data doar ISO. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare, schema) NEATINS. PRD: [prd-3.5](prd/prd-3.5-dashboard-compact-trimiteri-mapari.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP). +**Ultima actualizare**: 2026-06-23 — 5.7 LIVRAT (raspuns API onest la blocaje + mapare inline din detaliu). Raportat din client VFP: `POST /v1/prezentari` intorcea `submission_id`+`status` FARA motiv pe randuri blocate (`erori` se popula doar pe ramura `on_unmapped_error=True`) → un `needs_data`/`needs_mapping` parea succes ("raspuns fara erori"). Reparat ADITIV: `SubmissionResult` += `nemapate` + `motiv`; `create_prezentari` populeaza `erori` (validare continut, 3 niveluri) / `nemapate` (coduri fara mapare, COD_NEMAPAT) / `motiv` (rezumat uman) pe TOATE caile non-`queued` — enqueue, respins (`on_unmapped_error=True`) si reactivare dedup peste `error`. UI: mapare inline in panoul de detaliu trimitere (`POST /trimitere/{id}/mapeaza`, reuse EXACT `save_mapping`+`reresolve_account`, scoped sesiune + CSRF, re-rezolva pe `batch_id`-ul randului → deblocheaza si randurile-frate; apare doar pe operatii nemapate reale, nu pe auto_send=0) — fara drum prin tab-ul Mapari. `/code-review high`: 2 buguri reale reparate (reactivarea omitea `erori`/`nemapate`/`motiv`; dublu `load_nomenclator` in `_detaliu_ctx`), restul candidatilor infirmati la verify (parse `auto_send` corect via `or ""`; lipsa `conn/account_id` pe ramuri de corectie nereachable cu needs_mapping+unmapped). `pytest -q` **765 passed, 0 failed**. PRD: [prd-5.7](prd/prd-5.7-raspuns-onest-mapare-inline.md). | ISTORIC: FIX out-of-process (raportat din client VFP): `cod_prestatie` necunoscut in nomenclator era trimis raw la RAR → **HTTP 500** (`ORA-12899`, coloana `COD_PRESTATIE` max 5 car.) + record PARTIAL `FINALIZATA` (RAR ne-tranzactional) pe care reconcilierea il marca fals `sent`. Reparat: validare `cod_prestatie` fata de nomenclator la ingestie (cod necunoscut → tratat ca operatie de mapat, nu se mai trimite raw) + optiune boolean `on_unmapped_error` (`false` default → needs_mapping | `true` → respinge) per-cerere cu default per-cont `accounts.on_unmapped_error_default` (migrare aditiva). Confirmat live raspunsul RAR (500 pe cod intern vs 200 pe `OE-1`). Inclus si in `c842e33`: fix lease orfan worker (nepotrivire format data sending_since vs cutoff → orice rand `sending` parea expirat) + guard anti-dublu-POST + fix UI `hx-confirm` mostenit pe randuri (alerta de stergere la click pe rand). Teste: **748 passed** (cele 2 esecuri pre-existente fara legatura). Contract + CLAUDE.md actualizate. | 5.6 IMPLEMENTAT + VERIFY PASS (asteapta commit). Cele 14 stories din PRD 5.6 livrate TDD (RED->GREEN), `pytest -q` **741 passed, 0 failed**. Lifecycle trimiteri blocate (Val A primul, decizie #18): `app/submissions_admin.py` (sterge/repune scoped, 404-before-409); reactivare dedup peste `error` cu CAS + invalidare sesiune worker la creds noi (T1) + propagare `accounts.rar_creds_enc` (#17) + camp aditiv `reactivated:true` (#19); retentie randuri blocate 30z + `purge_after` curatat la reactivare/requeue (T2); API `DELETE`/`/repune` (200+JSON, #20); UI butoane + bulk + banner "Necesita atentia ta" actionabil cu deep-link. Observabilitate: `app/observ.py log_event` (dublu canal `app_events` DB + `RotatingFileHandler` per-proces, redactare creds/PII la scriere via `app/security.redact_pii`/`vin_partial`), `request_id` middleware + `X-Request-ID` pe toate raspunsurile (T8), handler global excepții -> 500 envelope 6-chei + request_id (T7), audit cerere API (`api_prezentari`/`api_auth_esuat`) + audit worker (`rar_login`/tranzitii), tab "Jurnal" filtrabil scoped (non-admin doar contul sau), retentie jurnal 90z. Live RAR `--send` NEPROBAT in sesiune (recomandat la deploy: confirma `rar_login` ok + `submission_sent` in jurnal). PRD actualizat cu raport VERIFY; contract actualizat cu endpointurile noi (T10). | ISTORIC: HOTFIX livrat + 5.6 APROBAT. Hotfix 500 pe `POST /v1/prezentari` (raportat din client Visual FoxPro): `AUTOPASS_CREDS_KEY` din `.env` nu respecta formatul Fernet (32 bytes url-safe base64) → `ValueError` la primul `encrypt_creds` → 500 brut. Reparat: cheie Fernet valida in `.env` + `crypto.validate_creds_key()` apelata in `main.lifespan` (fail-fast la startup, mesaj clar in loc de 500 la primul POST). Confirmat live: POST VFP → 200 `queued`; trimitere reala pe RAR test → `sent idPrezentare=68818` (verificat independent in finalizate). Corectat si mesajul fals din dashboard pentru starea `error` in `labels.py` ("se reincearca automat" → starea e terminala, NU se reincearca). Investigatia a expus 3 goluri structurale (500 brut fara traducere 3 niveluri; lipsa jurnal de aplicatie la nivel de eveniment; lacune de lifecycle — randuri blocate permanente, dedup blocat de un rand `error`, banner "Necesita atentia ta" neactionabil) → **PRD 5.6 APROBAT** (14 stories; decizii §5 rezolvate cu user). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md). | ISTORIC: 5.5 LIVRAT (uniformizare/standardizare UI/UX: tabele la grila Trimiteri, meniu hamburger + tab-bar redus Acasa/Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk pe model nou `accounts.status`. 9 stories in 3 valuri, UI pur cu o singura exceptie backend = stare cont; stergere soft cu purjare PII imediata GDPR. VERIFY 671 teste + E2E browser (2 bug-uri prinse) + `/code-review high` (2 bug-uri reale reparate). Commit `1fbd894`, vezi randul 5.5). | ISTORIC: 5.4 LIVRAT (Erori pe 3 niveluri problema+cauza+fix pe API si UI: catalog central pur `app/errors.py` ca SINGURA sursa de adevar cod→{problema,fix}, consumat de API+UI+worker — face imposibila divergenta intre canale, acelasi invariant ca 5.2. 8 stories in 5 valuri. Tot ADITIV: `field`/`message`/`error` pastrate la octet, adaugam `cod/problema/cauza/fix`; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare; login/signup/CSRF neatinse. UI progresiv: lista compacta, 3 niveluri complete in detaliu/preview, AA light+dark. VERIFY context curat PASS 628 teste (byte-compat+superset verificate direct, E2E API+web; live RAR neprobat — lipsa creds key). `/code-review high`: 2 bug-uri reale reparate in `labels.py` (`motiv_uman` fara ramura 3-niveluri → 401 creds garbled in coloana Motiv; `parse_erori` element gol pe `{}`). 631 teste. Backend trimitere + schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md)). | ISTORIC: 5.3 LIVRAT (Light/Dark mode: tema light ca bloc `[data-theme="light"]` peste variabilele `:root` — dark NESCHIMBAT la octet; comutator soare/luna in header pe toate paginile, default OS-aware cu fallback dark, persistenta `localStorage` doar la comutare explicita, script anti-FOUC in `` pre-paint; suprafetele de stare hardcodate convertite la `color-mix` in `base.html` + 7 fragmente. Zero backend — pur frontend. VERIFY 2 runde: r1 FAIL a prins literalii dark ramasi in 7 fragmente HTMX (text invizibil in light, test vacuu pe doar base.html) → fix US-003 + test care scaneaza fragmentele; r2 PASS E2E browser (banner light ~13:1 contrast, toggle instant+persista+anti-FOUC, dark identic). `/code-review` high: 1 finding reparat (light `--ok` green sub AA ca text → green-700, ~5.0:1). 584 teste. PRD: [prd-5.3](prd/prd-5.3-light-dark-mode.md)). | ISTORIC: 5.2 LIVRAT (Endpoint dry-run `POST /v1/prezentari/valideaza`: valideaza payload + mapare si intoarce verdictul real — `status_estimat` queued/needs_data/needs_mapping + erori `[{field,message}]` + coduri nemapate + prestatii rezolvate — FARA enqueue, FARA creds, zero scriere DB). 1 story TDD. Cheia de design: helper pur partajat `classify_prezentare` folosit de AMBELE rute, ca dry-run-ul sa nu poata diverge de trimiterea reala (invariant de corectitudine); `create_prezentari` refactorizat pe el cu comportament identic. Scope minim per decizie user: doar validare+mapare (fara idempotency/duplicat, `idempotency.py` neatins), hub `/integrare` amanat ca follow-up (descoperibilitate). VERIFY context curat PASS (577 teste; E2E API cu cele 3 verdicte + COUNT(*)=0 dupa dry-run + fara leak creds in raspuns; regresia de aur verde; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint read-only nu atinge worker/coada/schema). `/code-review` high: 0 findings (refactor faithful, mutable-default Pydantic-safe, import local necesar anti-circular). PRD: [prd-5.2](prd/prd-5.2-dryrun-valideaza.md). | ISTORIC: 5.1 LIVRAT (Hub de integrare `/integrare`: exemple cod multi-limbaj + retetar VFP cu 2 dialecte + `GET /v1/ping` readiness + export Postman/OpenAPI + "Testeaza conexiunea"). 4 stories in 2 valuri (Val 1 = US-001/US-002/US-004 paralel pe fisiere disjuncte via Agent team; Val 2 = US-003 UI). Atentie operationala: US-003 a rulat intr-un worktree branched din ultimul commit (FARA modificarile necomise ale US-004 din working-tree) si la "copiere manuala" a SUPRASCRIS `routes.py`, stergand ruta `POST /integrare/test-cheie` (8 teste 404) — reparat prin re-aplicarea rutei de catre autorul US-004 pe `routes.py` curent. Lectie: stories care ating acelasi fisier in valuri diferite + worktree = clobber daca worktree-ul nu vede working-tree-ul; foloseste fisiere disjuncte SAU merge atent de catre lead. VERIFY context curat PASS (568 teste) + E2E browser Playwright (deep-link server-side, IA pe 2 niveluri, VFP cu 3 niveluri de tab comuta corect, copy, htmx test-cheie → fragment eroare, 0 erori consola) + enqueue live (`POST /v1/prezentari` → queued); live RAR `FINALIZATA` NEPROBAT in sesiune (lipsa `AUTOPASS_CREDS_KEY`/creds RAR test) — risc minim, backend trimitere NEATINS. `/code-review` high a prins 4 bug-uri reale (toate in suprafata noua, reparate + lock-uite cu teste): snippet C# JSON multi-linie nevalid (CS1010), snippet VFP `json.dumps(indent=0)` inca cu newline-uri → string literal rupt in ambele dialecte, snippet Node `node:buffer` nu exporta FormData → TypeError, script `_integrare.html` ne-scoped acumuland event-listeneri pe tab-bar-ul principal la fiecare swap htmx (scoped pe `#integrare-section`). Notat ca cleanup viitor (nereparat): `_render_integrare` dubleaza SQL `are_creds`/`are_cheie`, `ping` cu 2 conexiuni DB + `account_for_key` de 2 ori, `_campuri_obligatorii` necache-uit, panouri limbaj copy-paste (candidat macro Jinja2). Backend trimitere (worker/masina stari/idempotenta/mapping) si schema NEATINSE. PRD: [prd-5.1](prd/prd-5.1-hub-integrare.md). | ISTORIC: 3.6 INCHIS (editare celule in preview + Acasa unificata). CLOSE: `/code-review` high a prins 1 bug real (decriptare `override_json` neprotejata de try/except in ambele cai de preview — 500 pe tot batch-ul la rotatie cheie Fernet vs. `raw_json` care degrada gratios), reparat in `import_router.preview_import` + `routes._web_compute_preview`; duplicarea `_override_of`/canonicalize notata ca cleanup viitor. 523 teste pass. 7 stories in 3 valuri, executate de 2 echipe in paralel (TeamCreate) pe fisiere disjuncte (core: routes/import/templates; mapari: `_mapari.html`) + US-007 secvential. Livrate: tab "Trimiteri" eliminat→sectiune "Trimiterile tale" sub upload pe Acasa (US-003); upload bara slim accentuata cu hero la first-run (US-004); editare de celule in preview prin `import_rows.override_json` (Approach B, Fernet, patch canonic aplicat ULTIMUL in `_resolve_row_for_preview`+`commit_import` — completeaza inclusiv coloane ABSENTE din fisier), mutatie pura cu status rederivat (US-001); buton Editeaza pe rand cu swap pe ``+OOB contoare (nu pe sectiune), form propriu, mutual-exclusion, reuse grila `_trimitere_detaliu.html` (US-002); Mapari + formate de coloane ca tabele `.tablewrap`, H4 auto_send stocat (US-005/006); bifa "auto-send"→comutator etichetat pe COADA ("Pune automat in coada"/"Tine pentru verificare"), scoped pe operatie, `name=auto_send` pastrat (zero backend) (US-007). 523 teste pass. **VERIFY**: E2E browser pe `/` (Acasa unificata, upload slim, editare rand needs_data→ok cu swap pe rand + contoare OOB, Mapari tabelar + comutator) + LIVE pe RAR test — import fara coloana data → editarea completeaza data (override) → commit → worker login RAR test → `postPrezentare` → `sent` cu `idPrezentare=68696`, confirmat independent in lista finalizate RAR. 3 bug-uri JS (htmx 1.9.12) prinse DOAR la E2E in browser (invizibile la TestClient) si reparate: `useTemplateFragments=true` (raspunsul ``+OOB era parsat in context de tabel → `swapError` + contoare pierdute), re-activare `confirm-btn` deferita pe tick (race `editing=true` tranzitoriu), `n-hint` ok-count actualizat de `updateN`. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare) NEATINS — singura atingere de schema: 1 coloana nullable `override_json` cu migrare defensiva. PRD: [prd-3.6](prd/prd-3.6-editare-preview-acasa-unificata.md). | ISTORIC: 3.5 LIVRAT (dashboard compact). 11 stories in 4 valuri, TDD. US-001 bara status compacta pe 2 randuri cu bife accesibile (glife ✓/✗ + text, nu doar culoare) + `format_data_rar` (dd.mm.yyyy hh24:mi:ss, helper pur). US-002 Acasa = ecranul de import (upload dominant inline, tab Import scos, `?tab=import`→Acasa fara 404). US-003 helper pur partajat `app/payload_view.py` (payload→campuri afisabile, defensiv, coercion Excel) refolosit si de `GET /v1/prezentari` (DRY). US-004 "Coada"→"Trimiteri": coloane RO + stare umana + detaliu complet la click in panou dedicat `#trimitere-detaliu` (nu inline — poll 10s), scoped 404 cross-account. US-005/006 CRUD mapari operatii + formate coloane salvate (scoped, re-rezolvare auto la edit cod). US-007 "Mapari" 3 sectiuni (de rezolvat / op salvate / formate coloane), "Cont" doar cheie+creds. US-008 motiv (mesaj validare) pe randuri needs_data in preview. US-009 filtre Trimiteri (stare SQL / vehicul+data Python) scoped + "sterge filtrele". US-010 corectie inline needs_data→queued cu payload+idempotency recalculate, sent read-only (403), coliziune idempotency prinsa pre-UPDATE. US-011 badge contoare pe tab-uri (Mapari/Trimiteri), scoped, aria-label. VERIFY context curat PASS (483 teste; E2E browser/RAR LIVE neprobat — recomandata probare manuala `--send`). `/code-review` high a prins 4 findings reale, toate reparate: corectie needs_mapping re-rezolva prestatiile (nu mai poate trimite cod nul la RAR), filtru fara LIMIT silentios, coliziune idempotency atomica (try/except IntegrityError), comparatie data doar ISO. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare, schema) NEATINS. PRD: [prd-3.5](prd/prd-3.5-dashboard-compact-trimiteri-mapari.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP). > 2026-06-18 — 3.4 LIVRAT (interfata web ergonomica: tab-uri + wizard + microcopy). US-001 modul pur `app/web/labels.py` (stari tehnice→text uman + clasa CSS; test parametrizat din CHECK-ul `schema.sql` iese rosu la stare nemapata). US-002 bara status `/_fragments/status` + `_status.html` (etichete umane, defalcare blocate pe motiv, poll 15s, scoped pe cont). US-003 shell 6 tab-uri (Acasa·Import·Coada·Mapari·Cont·Nomenclator) cu deep-link `?tab=`, panou activ randat server-side, fragmente inactive lazy pe click, ARIA real (tablist/tab/tabpanel + aria-selected + navigare cu sageti). US-004 stepper import 4 pasi (PUR vizual, `hx-target="#import-section"` + csrf pastrate). US-005 Acasa onboarding checklist auto-bifat (are_creds/are_trimiteri) + colaps cand totul gata + empty states prietenoase Coada/Mapari. VERIFY lead-driven (TestClient ACs + 434 pytest pass; E2E browser/RAR LIVE neprobat in sesiune — recomandata probare manuala `--send`). Fix izolare teste (reset `ratelimit._hits` in fixturi, 429 la rulare subset). `/code-review` high: regasit avertisment "cont in asteptare de activare" (regresie din scoaterea `/_fragments/banner`) re-introdus in bara status + culori hardcodate→variabile paleta. 434 teste pass. Backend trimitere neatins. PRD: [prd-3.4](prd/prd-3.4-ux-dashboard-web.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP). @@ -102,6 +102,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi | 5.5 | Uniformizare/standardizare UI/UX: tabele la grila Trimiteri (Mapari compact + toggle Auto/Manual + Ajutor; Nomenclator), meniu hamburger (Cont/Integrare/Nomenclator/Admin/logout) cu tab-bar redus la Acasa+Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk (Activeaza/Blocheaza/Arhiveaza/Sterge) pe model nou de stare cont | DONE | 2026-06-23 | 9 stories, 3 valuri. UI pur (reskin+reasezare) cu O SINGURA exceptie backend: `accounts.status` (pending/active/blocked/archived/deleted, migrare defensiva, gate worker `claim_one` pastrand `active=1 ⇔ status='active'`). Macro autosend rescris compact pastrand semantica de prezenta `auto_send` (zero backend). Stergere = SOFT: tombstone, dar PII (creds RAR + chei API + CUI) purjate IMEDIAT la stergere (GDPR/L.142), nu prin retentie. VERIFY 671 teste pass (+40); E2E browser Playwright a prins 2 bug-uri invizibile la TestClient (bara bulk `display:flex` inline invingea `[hidden]`; arhivate cadeau sub "in asteptare" → grupare pe status). `/code-review high`: 2 bug-uri reale reparate (soft delete pastra creds+CUI fara purjare → purjare PII la stergere; apostrof in nume rupea `confirm()` inline kebab) + cleanup `_lifecycle_route`. Debt acceptat: `/admin/deactivate`+`set_active` pastrate legacy (test CLI). Backend trimitere neatins (exceptie: gate cont). Commit `1fbd894`. Design: [5.5-uniformizare-ui](design/5.5-uniformizare-ui.md). PRD: [prd-5.5](prd/prd-5.5-uniformizare-ui.md) | | 5.4 | Erori pe 3 niveluri (problema + cauza + fix) pe API si UI | DONE | 2026-06-22 | Catalog central pur `app/errors.py` (24 coduri, `eroare()` → `{field,cod,problema,cauza,fix,message}`, ridica pe cod necunoscut) ca SINGURA sursa de adevar, consumat de API+UI+worker (acelasi invariant anti-divergenta ca 5.2). 8 stories, 5 valuri. Tot ADITIV (decizie user): `field`/`message`/`error` pastrate la octet, adaugam 3 niveluri + cod stabil; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare (validare continut, RAR 400/401, import, mapare op→cod); login/signup/CSRF NEATINSE. US-001 catalog; US-002 `validation.py` (cod+3n, byte-compat); US-003 `mapping.py`+`/valideaza` (needs_mapping/auto_send superset, `nemapate` 3n); US-004 worker RAR 400→`RAR_VALIDARE` (field_errors passthrough) + 401→`RAR_CREDS_INVALIDE` (clasificare transient neschimbata, fara echo creds); US-005 `import_router` detalii superset; US-006 `labels.parse_erori` (degradeaza gratios, lectia 3.6) + macro `_eroare.html` (progresiv: lista compacta, 3n complete in detaliu/preview; AA light+dark, accent≠rosu); US-007 upload/mapcoloane web prin macro; US-008 contract documentat. VERIFY context curat PASS (628 teste; byte-compat+superset verificate direct; E2E API `/valideaza` 3 niveluri + regresia de aur queued; E2E web fragmente upload+detaliu; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint-urile/UI noi nu ating trimiterea). `/code-review high`: 2 bug-uri reale reparate in `labels.py` — `motiv_uman` nu avea ramura pentru dict-ul 3-niveluri (401 creds → text garbled "field: None; cod:..." in coloana Motiv) + `parse_erori` intorcea element gol pe `{}`/`[{}]` (cutie de eroare goala); cleanup notat ca viitor (dublare `parse_erori`/`motiv_uman`, enrichment COD_NEMAPAT pe 2 straturi, upload handlers copy-paste cross-channel `routes.py`/`import_router`, `json.loads` mort in ramura COLOANE_FORMAT_JSON). 631 teste. Backend trimitere (worker masina stari/idempotenta/mapping-rezolvare) si schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md) | | 5.6 | Observabilitate & jurnal aplicatie + lifecycle trimiteri blocate: jurnal structurat de evenimente (tabela `app_events` + log text) cu vizualizator in dashboard, handler global 500→eroare 3 niveluri + `request_id`, audit cereri API + login RAR + ciclu trimiteri (worker), redactare PII/creds, retentie/purjare; plus stergere/re-pune in coada randuri blocate (UI+API), dedup care nu mai e blocat de un rand `error`, purjare randuri blocate, banner "Necesita atentia ta" actionabil (link + identificare rand) | APROBAT | 2026-06-23 | Nascut din incidentul 500 (client VFP). HOTFIX deja livrat in afara procesului (US-000): cheie Fernet valida in `.env` + `crypto.validate_creds_key()` fail-fast in `main.lifespan` + mesaj `error` corectat in `labels.py`; confirmat live POST→200, send RAR test→`idPrezentare=68818`. 14 stories ramase: observabilitate US-001..008, lifecycle US-009..013, banner US-014. Decizii §5 rezolvate cu user: retentie jurnal 90z (`AUTOPASS_LOG_RETENTION_DAYS`) + `RotatingFileHandler`; tipuri evenimente extensibile; jurnal non-admin scoped pe cont (admin vede tot); la resubmit doar `error` se re-activeaza (needs_* raman deduped); retentie randuri blocate 30z (`AUTOPASS_BLOCKED_RETENTION_DAYS`); stergere UI cu confirmare simpla + bulk pe lista. NEIMPLEMENTAT inca (doar hotfix-ul e in cod). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md) | +| 5.7 | Raspuns API onest la blocaje (`erori`/`nemapate`/`motiv` pe orice status != `queued`) + mapare inline din panoul de detaliu trimitere | DONE | 2026-06-23 | Raportat din client VFP: `POST /v1/prezentari` raspundea `submission_id`+`status` fara motiv pe randuri blocate (`erori` doar pe `on_unmapped_error=True`) → `needs_data`/`needs_mapping` parea succes. 3 stories TDD. US-001 (backend API): `SubmissionResult` += `nemapate`+`motiv` (ADITIV), `create_prezentari` populeaza `erori`/`nemapate`/`motiv` pe enqueue + respins + reactivare via helperele `_rezultat_enqueue`/`_rezultat_respins`/`_motiv_clasificare`; `on_unmapped_error=True` pastreaza `erori`=COD_NEMAPAT (compat). US-002 (web): ruta `POST /trimitere/{id}/mapeaza` (reuse EXACT `save_mapping`+`reresolve_account`, scoped sesiune 404 + CSRF, re-rezolva pe `batch_id`-ul randului) + `_nemapate_pentru_submission` + context in `_detaliu_ctx`. US-003 (UI): sectiune "Mapeaza codul operatiei" in `_trimitere_detaliu.html` (selector cod RAR cu sugestie fuzzy preselectata >=60, `ui.autosend_toggle`), doar pe operatii nemapate reale. `/code-review high`: 2 buguri reale reparate (reactivarea omitea `erori`/`nemapate`/`motiv`; dublu `load_nomenclator`), restul infirmate. `pytest -q` **765 passed, 0 failed**. Live RAR `--send` NEPROBAT (recomandat la deploy: mapare inline → `queued` → worker trimite). Backend trimitere (worker/masina stari/idempotenta) si schema NEATINSE. PRD: [prd-5.7](prd/prd-5.7-raspuns-onest-mapare-inline.md) | ### Etapa 4 — Deprioritizat (post Etapa 5, daca apare nevoia din uz real) diff --git a/docs/prd/prd-5.7-raspuns-onest-mapare-inline.md b/docs/prd/prd-5.7-raspuns-onest-mapare-inline.md new file mode 100644 index 0000000..9a74aa6 --- /dev/null +++ b/docs/prd/prd-5.7-raspuns-onest-mapare-inline.md @@ -0,0 +1,160 @@ +# PRD 5.7 — Raspuns API onest la blocaje + mapare inline din detaliu + +**Stare**: inchis + +> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`. +> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead). + +## 1. Obiectiv + +Doua goluri raportate din clientul Visual FoxPro (2026-06-23): + +1. **Raspunsul `POST /v1/prezentari` minte prin omisiune.** Cand o prezentare e blocata + (`needs_data` — date respinse de RAR; `needs_mapping` — cod fara mapare), API-ul intoarce + `{submission_id, status}` **fara** detalii: campul `erori` ramane gol (se populeaza azi DOAR pe + ramura `on_unmapped_error=True`). Un integrator care verifica "am `submission_id` si `erori` e + gol → e ok" trateaza un rand blocat ca succes. Screenshot live: rand `needs_data` (data in viitor) + pe care clientul l-a primit "fara erori". Reparam: raspunsul devine **onest** — pentru orice status + != `queued` expune `erori` (validare continut), `nemapate` (coduri fara mapare) si un `motiv` + uman pe o linie. Strict **aditiv** (campuri noi + populare a unui camp existent) — clientii vechi + nu se rup. + +2. **Maparea unui cod nemapat cere drum prin tab-ul Mapari.** Din panoul de detaliu al unei + trimiteri `needs_mapping`, userul vede "Lipseste codul prestatiei" dar trebuie sa navigheze in + alt tab ca sa o mapeze. Adaugam **mapare inline**: chiar in panoul de detaliu, fiecare operatie + nemapata primeste un selector de cod RAR (cu sugestii fuzzy preselectate) si un buton care + salveaza maparea + re-rezolva submission-ul pe loc. + +Etapa 5 (ergonomie & integrare). Niciun apel live la RAR. + +## 2. Non-Goals (anti scope-creep) + +- **NU schimba masina de stari, worker-ul, idempotency-ul, schema.** Zero coloane noi. `classify_prezentare` + ramane sursa unica de clasificare (invariantul dry-run din 5.2 se pastreaza). +- **NU schimba comportamentul `on_unmapped_error`** (ramura `error`/respins): `erori` cu `COD_NEMAPAT` + ramane exact ca azi (lock-uit de `test_on_unmapped_error_respinge_fara_enqueue`). Doar **adaugam** + `nemapate` + `motiv` si pe acea ramura. +- **NU imbogateste raspunsul `deduped`** cu erori/motiv: un dedup intoarce starea curenta a randului + existent fara re-clasificare (clientul poate cere `GET /v1/prezentari/{id}` pentru detaliu). Pastram + semantica actuala a caii de dedup. +- **NU atinge `/valideaza`** (dry-run-ul are deja `erori`+`nemapate`; ramane referinta de forma). +- **NU schimba editorul din tab-ul Mapari** — maparea inline il completeaza, nu il inlocuieste; + reutilizeaza `save_mapping` + `reresolve_account` (aceeasi logica, fara ramura noua). +- **NU mapare inline pe `needs_data`** — acolo exista deja corectia de campuri (US-010, 3.5). + Inline-ul de mapare apare DOAR pe `needs_mapping` cu operatii nemapate reale. + +## 3. Stories atomice + +### US-001: Raspuns `POST /v1/prezentari` onest pentru randuri blocate +**Ca** integrator (ROAAUTO / soft propriu / punte VFP) **vreau** ca raspunsul la enqueue sa-mi spuna +**de ce** un rand e blocat (nu doar `status`), **pentru ca** sa nu tratez un `needs_data`/`needs_mapping` +drept succes si sa pot reactiona programatic. + +- **Depinde de**: — +- **Fisiere**: `app/models.py` (`SubmissionResult` += `nemapate`, `motiv`), `app/api/v1/router.py` + (`create_prezentari` populeaza din `cl`), `tests/test_api.py` + `tests/test_mapping.py` (~4 fisiere) +- **Test intai (RED)**: `tests/test_api.py` — + - `test_needs_data_intoarce_erori` (data in viitor → `status="needs_data"`, `erori` negol cu + `cod="DATA_VIITOR"` pe `field="data_prestatie"`, `motiv` negol) + - `test_vin_invalid_intoarce_erori_pe_camp` (VIN cu O/I/Q → `erori` cu `field="vin"`) + - `test_needs_mapping_intoarce_nemapate` (cod necunoscut → `status="needs_mapping"`, `nemapate` + negol cu `cod_op_service`, `erori=[]`, `motiv` negol) + - `test_queued_fara_erori_nemapate` (valid → `erori=[]`, `nemapate=[]`, `motiv` null) + - `tests/test_mapping.py::test_on_unmapped_error_pastreaza_erori` (regresie: `on_unmapped_error=True` + inca da `erori[0].cod=="COD_NEMAPAT"`; + acum `nemapate` negol) +- **Acceptance criteria**: + - [ ] `SubmissionResult` are campurile noi `nemapate: list[dict] = []` si `motiv: str | None = None` + (aditiv; `erori` ramane). + - [ ] Pe enqueue cu `status="needs_data"`: `erori` = exact `validate_prezentare` (3 niveluri), + `nemapate=[]`, `motiv` = rezumat uman pe o linie. + - [ ] Pe enqueue cu `status="needs_mapping"` din coduri nemapate: `nemapate` = + `[{cod_op_service, denumire, cod, problema, cauza, fix, message}]`, `erori=[]`, `motiv` negol. + - [ ] Pe enqueue cu `status="needs_mapping"` din `auto_send=0`: `motiv` explica "confirmare manuala" + (chiar daca `erori`/`nemapate` sunt goale). + - [ ] Pe `status="queued"`: `erori=[]`, `nemapate=[]`, `motiv=None`. + - [ ] Ramura `on_unmapped_error=True` (`status="error"`, `submission_id=None`): `erori` neschimbat + (`COD_NEMAPAT`), `nemapate` negol, `motiv` negol. + - [ ] Raspunsul nu contine niciodata echo de creds (`password`) — validat pe corpul serializat. +- **Verificare E2E**: `POST /v1/prezentari` din client (sau curl) cu data in viitor → JSON cu `erori` + vizibile; cu cod intern → `nemapate`. + +### US-002: Backend mapare inline din panoul de detaliu (ruta + context) +**Ca** operator in dashboard **vreau** sa mapez codul unei operatii direct din detaliul trimiterii +**pentru ca** sa deblochez randul fara sa schimb tab-ul. + +- **Depinde de**: — +- **Fisiere**: `app/web/routes.py` (helper `_nemapate_pentru_submission`, context in `_detaliu_ctx`, + ruta `POST /trimitere/{id}/mapeaza`), `tests/test_web_mapare_inline.py` (~2 fisiere) +- **Test intai (RED)**: `tests/test_web_mapare_inline.py` — + - `test_detaliu_needs_mapping_arata_operatii_nemapate` (GET detaliu pe `needs_mapping` → HTML + contine selectorul de cod RAR + denumirea operatiei + cel putin o sugestie) + - `test_mapeaza_inline_deblocheaza_randul` (POST `/trimitere/{id}/mapeaza` cu cod valid → + submission trece `queued`, maparea apare in `operations_mapping`) + - `test_mapeaza_inline_cod_necunoscut_respins` (cod inexistent in nomenclator → mesaj eroare, + submission ramane `needs_mapping`, nicio mapare salvata) + - `test_mapeaza_inline_scoped_pe_sesiune` (POST pe submission al altui cont → 404, fara mapare) + - `test_mapeaza_inline_csrf_obligatoriu` (fara token → 403) + - `test_mapeaza_inline_respecta_batch` (submission din import cu `batch_id` → re-rezolvarea il atinge) +- **Acceptance criteria**: + - [ ] `_detaliu_ctx` pe `status="needs_mapping"` expune `nemapate_inline` = + `[{cod_op_service, denumire, suggestions:[{cod_prestatie, nume_prestatie, score}]}]` + `nomenclator`. + - [ ] `POST /trimitere/{id}/mapeaza` (Form: `cod_op_service`, `cod_prestatie`, `auto_send`, `csrf_token`): + verifica CSRF, scope pe sesiune (404 cross-account/inexistent ca restul rutelor), respinge cod + absent din nomenclator (re-randeaza detaliul cu mesaj), altfel `save_mapping` + `reresolve_account` + cu `batch_id` al randului, re-randeaza detaliul cu starea noua + mesaj. + - [ ] Re-rezolvarea foloseste EXACT `save_mapping`/`reresolve_account` existente (fara logica noua de + clasificare); randurile-frate cu aceeasi operatie din acelasi batch se deblocheaza si ele. + - [ ] Raspunsul trimite `HX-Trigger: trimiteriChanged` (lista Trimiteri se reimprospateaza). +- **Verificare E2E**: dashboard pe `needs_mapping` → alegi cod RAR inline → randul devine `queued`. + +### US-003: UI mapare inline in `_trimitere_detaliu.html` +**Ca** operator **vreau** un formular clar in detaliu (operatie + sugestii + selector cod + salveaza) +**pentru ca** maparea sa fie evidenta si rapida. + +- **Depinde de**: US-002 +- **Fisiere**: `app/web/templates/_trimitere_detaliu.html`, (eventual reuse din `_macros.html`), + `tests/test_web_mapare_inline.py` (asserts pe HTML) +- **Test intai (RED)**: acoperit de US-002 (`test_detaliu_needs_mapping_arata_operatii_nemapate`) +- **Acceptance criteria**: + - [ ] Sectiune "Mapeaza codul operatiei" vizibila DOAR cand `status="needs_mapping"` si exista + `nemapate_inline`; un `
` per operatie, `hx-target="#trimitere-detaliu"`. + - [ ] Selector cod RAR populat din nomenclator, cu sugestia top (score >= 60) preselectata; sugestiile + afisate cu procent (ca in tab-ul Mapari). + - [ ] Include `csrf_token` + comutatorul auto-send (reuse `ui.autosend_toggle`, default pornit). + - [ ] Mesaj de rezultat (succes/eroare) randat la re-render, fara a pierde restul panoului. +- **Verificare E2E**: idem US-002 (browser HTMX). + +## 4. Riscuri + +- **Divergenta clasificarii**: maparea inline NU trebuie sa reimplementeze rezolvarea. Mitigare: + reutilizeaza `save_mapping` + `reresolve_account` (singura cale, ca tab-ul Mapari) — orice corectie + de comportament ramane intr-un singur loc. +- **Scope batch**: `reresolve_account(batch_id=None)` atinge doar canalul API (`batch_id IS NULL`). Un + submission de import (`batch_id` setat) nu s-ar debloca. Mitigare: ruta paseaza `batch_id`-ul randului + (test dedicat `test_mapeaza_inline_respecta_batch`). +- **Compat raspuns**: noile campuri trebuie sa fie pur aditive. Mitigare: defaulturi goale + test de + regresie pe `on_unmapped_error` si pe forma `deduped`. + +## 5. Intrebari deschise — REZOLVATE (poarta de aprobare, 2026-06-23) + +- **Q1 [DA]**: pastram AMBELE — campuri structurate (`erori`/`nemapate`) pentru masini + `motiv` + (string uman pe o linie) pentru log/UI rapida. Implementat ca atare. +- **Q2 [DA]**: maparea inline apare DOAR pe operatii nemapate reale. Pe `needs_mapping` din + `auto_send=0` (cod deja mapat, doar trimitere oprita) NU se afiseaza — `_nemapate_pentru_submission` + intoarce lista goala in acel caz; confirmarea ramane in tab-ul Mapari. + +## 6. Valuri de executie (graful de dependente) + +``` +Val 1: [US-001] [US-002] ← fisiere disjuncte (router/models vs web routes) → paralel +Val 2: [US-003] ← deblocat de US-002 (template peste contextul nou) +``` + +--- + +## Raport VERIFY + +> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6. +> PASS/FAIL per criteriu, cu dovezi (output pytest citat, E2E pe RAR test). Lipseste pana la VERIFY. + + diff --git a/tests/test_api.py b/tests/test_api.py index 2bff00f..e46e009 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -62,6 +62,61 @@ def test_idempotenta_dedup(client): assert res2["deduped"] is True +# --------------------------------------------------------------------------- # +# PRD 5.7 — raspuns onest: erori/nemapate/motiv pe randuri blocate # +# --------------------------------------------------------------------------- # + +def test_needs_data_intoarce_erori(client): + """Data in viitor -> needs_data + erori 3 niveluri (DATA_VIITOR) + motiv negol.""" + r = client.post("/v1/prezentari", json=_body(data_prestatie="2099-01-01")) + res = r.json()["results"][0] + assert res["status"] == "needs_data" + assert res["nemapate"] == [] + coduri = [e["cod"] for e in res["erori"]] + assert "DATA_VIITOR" in coduri + assert res["erori"][0]["field"] == "data_prestatie" + assert res["motiv"] # rezumat uman negol + + +def test_vin_invalid_intoarce_erori_pe_camp(client): + """VIN cu O/I/Q -> erori cu field='vin'.""" + r = client.post("/v1/prezentari", json=_body(vin="WVWZZZ1OZIQ45678")) + res = r.json()["results"][0] + assert res["status"] == "needs_data" + fields = [e["field"] for e in res["erori"]] + assert "vin" in fields + + +def test_needs_mapping_intoarce_nemapate(client): + """Cod necunoscut in nomenclator -> needs_mapping + nemapate negol, erori gol, motiv negol.""" + r = client.post("/v1/prezentari", json=_body(prestatii=[{"cod_prestatie": "VERIFICARE NECUNOSCUTA 999"}])) + res = r.json()["results"][0] + assert res["status"] == "needs_mapping" + assert res["erori"] == [] + assert res["nemapate"] + assert res["nemapate"][0]["cod_op_service"] == "VERIFICARE NECUNOSCUTA 999" + assert res["nemapate"][0]["cod"] == "COD_NEMAPAT" + assert res["motiv"] + + +def test_queued_fara_erori_nemapate_motiv(client): + """Prezentare valida -> queued cu erori/nemapate goale si motiv null.""" + r = client.post("/v1/prezentari", json=_body()) + res = r.json()["results"][0] + assert res["status"] == "queued" + assert res["erori"] == [] and res["nemapate"] == [] + assert res["motiv"] is None + + +def test_raspuns_nu_reflecta_parola(client): + """Raspunsul onest nu trebuie sa contina niciodata creds RAR (echo de parola).""" + body = _body(data_prestatie="2099-01-01") + body["rar_credentials"] = {"email": "x@y.ro", "password": "PAROLA_SECRETA_123"} + r = client.post("/v1/prezentari", json=body) + assert "PAROLA_SECRETA_123" not in r.text + assert "password" not in r.text + + def test_json_malformat_422(client): # Lipseste vin -> validare de shape Pydantic -> 422 (NU needs_data). bad = {"rar_credentials": {"email": "x", "password": "y"}, diff --git a/tests/test_dedup_error.py b/tests/test_dedup_error.py index 36d2a83..ece28a1 100644 --- a/tests/test_dedup_error.py +++ b/tests/test_dedup_error.py @@ -117,6 +117,40 @@ def test_resubmit_peste_queued_ramane_deduped(client): assert res.get("reactivated", False) is False +def test_reactivare_pe_needs_data_expune_erori(client): + """PRD 5.7: reactivarea unui rand error care re-clasifica needs_data trebuie sa + expuna erori/motiv (raspuns onest), nu doar status + reactivated.""" + # rand initial valid -> queued; il fortam error + r1 = client.post("/v1/prezentari", json=_body()) + sid = r1.json()["results"][0]["submission_id"] + _force_status(sid, "error") + # resubmit cu aceeasi cheie de continut DAR data in viitor -> reactivare pe needs_data + r2 = client.post("/v1/prezentari", json=_body(data_prestatie="2099-01-01")) + res = r2.json()["results"][0] + # cheia de continut difera (alta data) -> NU dedup; e un rand nou. Verificam ca + # oricum raspunsul onest e populat pentru needs_data (calea de enqueue). + assert res["status"] == "needs_data" + assert any(e["cod"] == "DATA_VIITOR" for e in res["erori"]) + assert res["motiv"] + + +def test_reactivare_acelasi_continut_pastreaza_onest(client): + """Reactivare cu EXACT acelasi continut peste un rand error -> reactivated=True; + daca starea noua e blocata, erori/motiv sunt populate (altfel goale pe queued).""" + r1 = client.post("/v1/prezentari", json=_body(vin="WVWZZZ1OZIQ45678")) # VIN invalid -> needs_data + sid = r1.json()["results"][0]["submission_id"] + assert r1.json()["results"][0]["status"] == "needs_data" + _force_status(sid, "error") + r2 = client.post("/v1/prezentari", json=_body(vin="WVWZZZ1OZIQ45678")) + res = r2.json()["results"][0] + assert res["submission_id"] == sid + assert res["reactivated"] is True + assert res["status"] == "needs_data" + # raspuns onest la reactivare: erori populate + assert any(e["field"] == "vin" for e in res["erori"]) + assert res["motiv"] + + def test_resubmit_peste_needs_data_ramane_deduped(client): r1 = client.post("/v1/prezentari", json=_body()) sid = r1.json()["results"][0]["submission_id"] diff --git a/tests/test_mapping.py b/tests/test_mapping.py index 37b6527..7a62708 100644 --- a/tests/test_mapping.py +++ b/tests/test_mapping.py @@ -241,6 +241,9 @@ def test_on_unmapped_error_respinge_fara_enqueue(client): assert res["status"] == "error" assert res["submission_id"] is None assert res["erori"] and res["erori"][0]["cod"] == "COD_NEMAPAT" + # PRD 5.7: raspuns onest si pe ramura respinsa — nemapate + motiv populate (aditiv). + assert res["nemapate"] and res["nemapate"][0]["cod_op_service"] == _COD_INTERN + assert res["motiv"] # Nu s-a creat nimic in coada. assert client.get("/v1/prezentari").json()["submissions"] == [] diff --git a/tests/test_web_mapare_inline.py b/tests/test_web_mapare_inline.py new file mode 100644 index 0000000..114e3ac --- /dev/null +++ b/tests/test_web_mapare_inline.py @@ -0,0 +1,208 @@ +"""Teste PRD 5.7 — mapare inline din panoul de detaliu trimitere. + +needs_mapping cu operatie nemapata -> panoul arata selectorul de cod RAR + sugestii; +POST /trimitere/{id}/mapeaza salveaza maparea (save_mapping) si re-rezolva submission-ul +(reresolve_account) pe loc. Scoped pe sesiune (404 cross-account), CSRF obligatoriu, +respinge cod absent din nomenclator, respecta batch_id-ul randului. +""" + +from __future__ import annotations + +import json +import os +import re +import tempfile + +import pytest +from starlette.testclient import TestClient + + +def _create_account_user(email: str, name: str = "Service", 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, name, 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) + assert m + 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=acasa") + m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) + assert m + return m.group(1) + + +def _insert_needs_mapping(acct: int, *, op: str = "DIVERSE VERIFICARI 159004", + denumire: str | None = None, batch_id: int | None = None, + vin: str = "WVWZZZ1JZXW0NM001") -> int: + """Insereaza un submission needs_mapping cu o prestatie nemapata (cod_prestatie null).""" + from app.db import get_connection + payload = { + "vin": vin, "nr_inmatriculare": "B123ABC", "data_prestatie": "2026-06-10", + "odometru_final": "159004", + "prestatii": [{"cod_prestatie": None, "cod_op_service": op, "denumire": denumire or op}], + } + conn = get_connection() + try: + k = f"k-{os.urandom(6).hex()}" + cur = conn.execute( + "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, batch_id) " + "VALUES (?, ?, 'needs_mapping', ?, ?, ?)", + (k, acct, json.dumps(payload), json.dumps({"unmapped": [{"cod_op_service": op}]}), batch_id), + ) + conn.commit() + return int(cur.lastrowid) + finally: + conn.close() + + +def _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 _mapping_count(acct: int) -> int: + from app.db import get_connection + conn = get_connection() + try: + return conn.execute( + "SELECT COUNT(*) AS n FROM operations_mapping WHERE account_id=?", (acct,) + ).fetchone()["n"] + finally: + conn.close() + + +@pytest.fixture() +def client(monkeypatch): + tmp = tempfile.mkdtemp() + monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "inline.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 test_detaliu_needs_mapping_arata_operatii_nemapate(client): + """GET detaliu pe needs_mapping -> HTML cu selectorul de cod RAR + operatia + actiunea inline.""" + acct = _create_account_user("nm@test.com") + sid = _insert_needs_mapping(acct, op="REPARATIE MOTOR X") + _login(client, "nm@test.com") + resp = client.get(f"/_fragments/trimitere/{sid}") + assert resp.status_code == 200 + assert "Mapeaza codul operatiei" in resp.text + assert "REPARATIE MOTOR X" in resp.text + assert f"/trimitere/{sid}/mapeaza" in resp.text + # nomenclatorul seed e in selector + assert "OE-1" in resp.text + + +def test_mapeaza_inline_deblocheaza_randul(client): + """POST mapeaza cu cod valid -> submission queued + mapare salvata.""" + acct = _create_account_user("mi@test.com") + sid = _insert_needs_mapping(acct, op="VERIF SPECIALA 1") + _login(client, "mi@test.com") + csrf = _csrf(client) + resp = client.post(f"/trimitere/{sid}/mapeaza", data={ + "cod_op_service": "VERIF SPECIALA 1", "cod_prestatie": "OE-1", + "auto_send": "true", "csrf_token": csrf, + }) + assert resp.status_code == 200 + assert resp.headers.get("HX-Trigger") == "trimiteriChanged" + r = _row(sid) + assert r["status"] == "queued" + # codul s-a umplut in payload + pres = json.loads(r["payload_json"])["prestatii"][0] + assert pres["cod_prestatie"] == "OE-1" + assert _mapping_count(acct) == 1 + + +def test_mapeaza_inline_cod_necunoscut_respins(client): + """Cod inexistent in nomenclator -> mesaj eroare, ramane needs_mapping, fara mapare.""" + acct = _create_account_user("cn@test.com") + sid = _insert_needs_mapping(acct, op="VERIF SPECIALA 2") + _login(client, "cn@test.com") + csrf = _csrf(client) + resp = client.post(f"/trimitere/{sid}/mapeaza", data={ + "cod_op_service": "VERIF SPECIALA 2", "cod_prestatie": "COD-INEXISTENT", + "csrf_token": csrf, + }) + assert resp.status_code == 200 + assert "necunoscut" in resp.text.lower() + assert _row(sid)["status"] == "needs_mapping" + assert _mapping_count(acct) == 0 + + +def test_mapeaza_inline_scoped_pe_sesiune(client): + """POST pe submission al altui cont -> 404, fara mapare salvata.""" + acct_a = _create_account_user("a@test.com") + acct_b = _create_account_user("b@test.com") + sid = _insert_needs_mapping(acct_a, op="VERIF A") + _login(client, "b@test.com") + csrf = _csrf(client) + resp = client.post(f"/trimitere/{sid}/mapeaza", data={ + "cod_op_service": "VERIF A", "cod_prestatie": "OE-1", "csrf_token": csrf, + }) + assert resp.status_code == 404 + assert _row(sid)["status"] == "needs_mapping" + assert _mapping_count(acct_b) == 0 + + +def test_mapeaza_inline_csrf_obligatoriu(client): + """Fara token CSRF valid -> 403, fara efect.""" + acct = _create_account_user("cf@test.com") + sid = _insert_needs_mapping(acct, op="VERIF CSRF") + _login(client, "cf@test.com") + resp = client.post(f"/trimitere/{sid}/mapeaza", data={ + "cod_op_service": "VERIF CSRF", "cod_prestatie": "OE-1", "csrf_token": "gresit", + }) + assert resp.status_code == 403 + assert _row(sid)["status"] == "needs_mapping" + + +def test_mapeaza_inline_respecta_batch(client): + """Submission din import (batch_id setat) -> re-rezolvarea il atinge (queued).""" + acct = _create_account_user("ba@test.com") + from app.db import get_connection + conn = get_connection() + try: + cur = conn.execute( + "INSERT INTO import_batches (account_id, filename, status) VALUES (?, 'f.xlsx', 'committed')", + (acct,), + ) + conn.commit() + batch_id = int(cur.lastrowid) + finally: + conn.close() + sid = _insert_needs_mapping(acct, op="VERIF BATCH", batch_id=batch_id) + _login(client, "ba@test.com") + csrf = _csrf(client) + resp = client.post(f"/trimitere/{sid}/mapeaza", data={ + "cod_op_service": "VERIF BATCH", "cod_prestatie": "OE-2", + "auto_send": "true", "csrf_token": csrf, + }) + assert resp.status_code == 200 + assert _row(sid)["status"] == "queued"