# PRD 5.2 — Endpoint dry-run `POST /v1/prezentari/valideaza` **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 Un endpoint care valideaza un payload de prezentari **exact ca** `POST /v1/prezentari`, dar **fara enqueue si fara efecte secundare**: intoarce verdictul (queued / needs_data / needs_mapping), erorile reale `[{field, message}]` si codurile nemapate, ca integratorul (ROAAUTO / soft propriu / punte VFP) sa-si verifice integrarea inainte sa trimita ceva real. "Magical moment" de onboarding (Etapa 5 — ergonomie & integrare). **Invariant de corectitudine (motivul cheie de design):** dry-run-ul trebuie sa produca **acelasi verdict** pe care l-ar produce `POST /v1/prezentari` pe acelasi payload. Daca cele doua cai diverg, dry-run-ul minte — mai rau decat sa nu existe. De aceea logica de clasificare se **extrage intr-un helper pur partajat** folosit de AMBELE endpoint-uri (nu se duplica). ## 2. Non-Goals (anti scope-creep) - **NU atinge coada `submissions`** — zero INSERT/UPDATE/DELETE; endpoint read-only (citeste doar `operations_mapping` pentru rezolvarea codurilor, scoped pe cont). - **NU detectie duplicat / idempotency** — nu raporteaza `idempotency_key` si nu cauta submission-uri identice existente (decizie utilizator 2026-06-22: doar validare + mapare). `idempotency.py` neatins. - **NU stocheaza si NU foloseste creds RAR** — `rar_credentials` devine optional si, daca e prezent, e ignorat (nu se cripteaza, nu se logheaza, nu se trimite nicaieri). - **NU UI / hub** — documentarea in `/integrare` (5.1) + snippet-uri ramane follow-up (decizie utilizator 2026-06-22). Acest PRD = strict endpoint + teste. - **NU atinge worker, masina de stari, schema, validation.py (regulile), nomenclator.** - **NU apel live la RAR** — validarea e 100% locala (replica regulilor RAR din `validation.py`). ## 3. Stories atomice ### US-001: Endpoint dry-run `POST /v1/prezentari/valideaza` **Ca** integrator (ROAAUTO / soft propriu / VFP) **vreau** sa trimit un payload de prezentari si sa primesc inapoi exact erorile + verdictul pe care le-ar produce trimiterea reala, **fara** sa enqueue ceva sau sa am nevoie de creds RAR, **pentru ca** sa-mi validez integrarea sigur, repetabil, inainte de prima trimitere reala. - **Depinde de**: — - **Fisiere**: `app/mapping.py` (helper pur nou `classify_prezentare`), `app/api/v1/router.py` (ruta + refactor `create_prezentari` sa foloseasca helper-ul), `app/models.py` (modele request/response), `tests/test_validare_dryrun.py` (~4 fisiere) - **Test intai (RED)**: `tests/test_validare_dryrun.py` — - `test_payload_valid_returneaza_queued` (valid → `status_estimat="queued"`, `valid=True`, `erori=[]`) - `test_vin_invalid_returneaza_needs_data` (VIN cu O/I/Q → `needs_data` + eroare pe `field="vin"`) - `test_data_viitoare_needs_data` (data in viitor → `needs_data`) - `test_cod_op_nemapat_returneaza_needs_mapping` (cod_op_service necunoscut → `needs_mapping` + `nemapate=[{cod_op_service, denumire}]`) - `test_mapare_existenta_rezolva_codul` (cu mapare salvata pe cont → cod_op_service rezolvat in `prestatii_rezolvate`, `status_estimat="queued"`) - `test_fara_creds_merge` (body FARA `rar_credentials` → 200) - `test_nu_scrie_in_coada` (`COUNT(*)` submissions inainte == dupa; zero efecte secundare) - `test_multi_prezentari_rezultate_per_index` (lista [valid, invalid] → 2 rezultate, `index` 0/1, statusuri diferite) - `test_shape_invalid_422` (prestatie fara `cod_prestatie` SI fara `cod_op_service` → 422 de shape, ca endpoint-ul real; raspunsul NU contine echo de `input`/parola) - **Acceptance criteria**: - [ ] `POST /v1/prezentari/valideaza` exista, accepta `{prezentari:[...], rar_credentials?}`, intoarce `{results:[{index, valid, status_estimat, erori, nemapate, prestatii_rezolvate}]}`. - [ ] `status_estimat` ∈ {`queued`, `needs_data`, `needs_mapping`} si coincide cu statusul pe care `POST /v1/prezentari` l-ar atribui pe acelasi payload + aceeasi mapare de cont (verificat prin helper partajat `classify_prezentare`, folosit de ambele rute). - [ ] `valid == (status_estimat == "queued")`. - [ ] `erori` = exact lista `validate_prezentare` (forma `[{field, message}]`), goala cand e curat. - [ ] `nemapate` = `[{cod_op_service, denumire}]` cand exista coduri interne nerezolvate; altfel `[]`. - [ ] `prestatii_rezolvate` arata codurile RAR rezolvate (cod_op_service mapat → cod_prestatie umplut). - [ ] `rar_credentials` optional; daca lipseste → 200; daca e prezent → ignorat (necriptat, nelogat). - [ ] Zero scriere in DB: `COUNT(*) FROM submissions` neschimbat dupa apel (read-only pe mapping). - [ ] Scope pe cont prin `resolve_account_id` (ca endpoint-ul real): maparea folosita la rezolvare e a contului cheii API (dev fara cheie → cont 1; prod → cheie obligatorie). - [ ] `create_prezentari` (endpoint-ul real) ramane cu comportament identic — toate testele existente `tests/test_api.py` raman verzi dupa refactor-ul catre helper-ul partajat. - [ ] `python3 -m pytest -q` verde. - **Verificare E2E**: canal API — `POST /v1/prezentari/valideaza` pe instanta locala cu (a) payload valid → `queued`, (b) VIN invalid → `needs_data` + mesaj real, (c) cod_op nemapat → `needs_mapping`; apoi `COUNT(*)` submissions neschimbat. **Regresia de aur:** `POST /v1/prezentari` real → worker → `FINALIZATA` la RAR test (neatins de endpoint-ul nou). ## 4. Riscuri - **Divergenta dry-run vs. real** (risc principal) → mitigat prin helper pur partajat `classify_prezentare` folosit de ambele rute + AC explicit + testele `test_api.py` care lock-uiesc calea reala dupa refactor. - **Refactor al caii reale** (`create_prezentari` adopta helper-ul) ar putea schimba subtil comportamentul → mitigat: testele existente `test_api.py` sunt contractul; raman verzi = comportament identic. Helper-ul intoarce exact aceleasi `status` + `rar_error` ca azi (queued / needs_data + erori JSON / needs_mapping + unmapped JSON / needs_mapping + auto_send note). - **Scurgere de creds** prin noul model → mitigat: `rar_credentials` optional, ignorat, `repr=False` pastrat; handler-ul global 422 din `main.py` deja dropeaza `input`/`ctx` (no-echo parola) — acoperit de `test_shape_invalid_422`. - **Asteptare gresita: "valideaza" = duplicat-check** → documentat ca Non-Goal in raspuns/docstring; fara `idempotency_key` in raspuns ca sa nu sugereze dedup. ## 5. Intrebari deschise > Rezolvate cu utilizatorul la poarta de aprobare PRD (2026-06-22). - **Continut raspuns** — REZOLVAT: doar validare + mapare (fara idempotency/duplicat). [user 2026-06-22] - **Hub /integrare** — REZOLVAT: amanat; 5.2 = endpoint + teste. [user 2026-06-22] - `auto_send=0` pe un cod mapat: pe calea reala devine `needs_mapping` cu nota "review manual". Dry-run-ul raporteaza acelasi `status_estimat="needs_mapping"` (consistenta cu real). Confirmat ca acceptabil — fara camp separat pentru cazul auto_send (ar fi scope creep). [decizie de plan, acceptata implicit] ## 6. Valuri de executie (graful de dependente) ``` Val 1: [US-001] ← singur story, fara dependente. Un worker (sau lead direct, livrabila mica — ROADMAP §5.5). ``` Livrabila mica: poate rula fara `TeamCreate` (un singur worker Sonnet TDD), dar VERIFY in context curat + writeback raman obligatorii (ROADMAP §5.5). ## 7. Review-uri de plan (aplicate inainte de cod — ROADMAP §5.3) **CEO (valoare/scope) — PASS.** Problema corecta, calea cea mai directa (reuse pur `validation.py`+`mapping.py`, zero logica de domeniu noua). Inversiune ("ce-l face sa esueze?"): divergenta dry-run vs. real — neutralizata prin helper-ul partajat `classify_prezentare` + lock-ul `test_api.py`. Scope minim corect; singura "expansiune" (extragerea clasificatorului) e ceruta de corectitudine, nu scope creep. **Risc flagat (deferare constienta):** descoperibilitate — un endpoint pe care integratorii nu-l gasesc nu-si livreaza valoarea pana cand un follow-up il expune in `/integrare`. Acceptat de utilizator (amanat); valoarea 5.2 se realizeaza la follow-up. **Eng (fezabilitate/teste) — PASS.** Fezabilitate triviala, fara atingere schema/worker/idempotenta. Lista de teste e completa (happy path + fiecare ramura de eroare + fara-creds + multi-prezentare cu index + shape-422 fara echo + aserctia critica zero-efecte `COUNT(*)` inainte==dupa). Singurul risc pe calea de aur = refactor-ul `create_prezentari` pe helper-ul partajat; contractul de blocare = suita `test_api.py` existenta (ramane verde = comportament identic). Read-only → fara logging nou (consistent cu GET-urile existente); in prod cere cheie prin `resolve_account_id` (fara suprafata noua de abuz). --- ## 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 canal API). Lipseste pana la VERIFY. Verificator independent (context curat, rol qa-only), 2026-06-22. **VERDICT GLOBAL: PASS.** **1. Suita — PASS.** `python3 -m pytest -q` → 577 passed (226 warnings preexistente Starlette/templating). `pytest tests/test_validare_dryrun.py tests/test_api.py -q` → 14 passed (9 dry-run TDD + 5 regresia caii reale). **2. Acceptance criteria US-001 — toate PASS.** Endpoint exista cu raspunsul `{results:[{index,valid, status_estimat,erori,nemapate,prestatii_rezolvate}]}`. `status_estimat` coincide cu `POST /v1/prezentari` prin helper-ul partajat `classify_prezentare` apelat de AMBELE rute (`router.py` create + valideaza, acelasi `load_mapping_meta`). `valid == (status=="queued")`. `erori`=lista `validate_prezentare`. `nemapate`= `[{cod_op_service,denumire}]`. `rar_credentials` optional + ignorat (`encrypt_creds` absent din ruta; parola din body NU apare in raspuns). Zero scriere DB (COUNT(*) submissions=0 dupa 3 apeluri). Scope prin `resolve_account_id`. `create_prezentari` identic (test_api.py verde). **3. E2E canal API (TestClient + DB temp, verificare directa SQLite) — PASS.** (a) valid+creds → queued, valid=true, erori=[], fara leak creds; (b) VIN cu O/I/Q → needs_data + eroare reala pe `vin`; (c) cod_op nemapat → needs_mapping + nemapate populat. Dupa toate: `GET /v1/prezentari`=0, DB COUNT(*)=0. **4. Regresia de aur — PASS (live neprobat, conform asteptarii).** `POST /v1/prezentari` enqueue-aza corect (200, queued, COUNT(*)=1) + test_api.py verde. Flux live RAR (worker→FINALIZATA pe RAR test) NEPROBAT — lipsesc secretele in mediu (`AUTOPASS_CREDS_KEY` nesetat, fara creds RAR test/`--send`). NU e FAIL al 5.2: endpoint-ul nou e read-only, nu atinge worker/coada/schema. Documentat, nu inventat.