feat(errors): erori pe 3 niveluri (problema+cauza+fix) pe API si UI (PRD 5.4)

Catalog central pur app/errors.py ca sursa unica cod->{problema,fix},
consumat de API+UI+worker. Aditiv (field/message pastrate la octet) +
rar_error stocat superset. Scope: fluxul de declarare; login/signup/CSRF
neatinse. labels.parse_erori degradeaza gratios; UI progresiv AA light+dark.
631 teste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-23 10:28:09 +00:00
parent b48501d8e4
commit 14e1c463f0
25 changed files with 2440 additions and 44 deletions

File diff suppressed because one or more lines are too long

View File

@@ -175,6 +175,127 @@ Aplicate deja pe ambele medii (test + producție):
Acestea devin reguli Pydantic exacte în `app/api`. Validează la gateway înainte de enqueue
(stare `needs_data`) ca nu primești 4xx de la RAR.
## Envelope de eroare imbogatit (PRD 5.4)
### Forma unui obiect de eroare
Incepand cu PRD 5.4, fiecare obiect de eroare returnat de gateway contine **6 chei**:
| Cheie | Tip | Rol | Back-compat |
|---|---|---|---|
| `field` | string \| null | Campul care a generat eroarea (null daca eroarea e globala) | DA existent anterior |
| `message` | string | Mesajul scurt (identic cu `cauza` cand e disponibila, altfel `problema`) | DA existent anterior |
| `cod` | string | Identificator stabil de tip eroare (ex. `VIN_FORMAT`). Camp nou. | NU adaugat 5.4 |
| `problema` | string | Ce s-a intamplat descriere scurta, inteligibila pentru utilizator | NU adaugat 5.4 |
| `cauza` | string | De ce a aparut eroarea concret; pentru erorile RAR 400, mesajul exact de la RAR (passthrough) | NU adaugat 5.4 |
| `fix` | string | Ce trebuie facut pentru remediere | NU adaugat 5.4 |
**Exemplu JSON concret** (eroare VIN invalid, returnat de `POST /v1/prezentari/valideaza`):
```json
{
"field": "vin",
"message": "VIN trebuie sa aiba exact 17 caractere majuscule, fara spatii/caractere speciale si fara O, I, Q.",
"cod": "VIN_FORMAT",
"problema": "VIN invalid",
"cauza": "VIN trebuie sa aiba exact 17 caractere majuscule, fara spatii/caractere speciale si fara O, I, Q.",
"fix": "Verifica VIN-ul pe talon (pozitia E) sau pe caroserie: exact 17 caractere majuscule, fara spatii si fara literele O, I, Q."
}
```
### Nota de back-compat
Cheile `field` si `message` sunt **pastrate neschimbate** pe toate raspunsurile. Cheile `cod`, `problema`, `cauza`, `fix` sunt **aditive** (camp nou in plus). Clientii care citesc doar `field`/`message` (sau `error`/`message` la import) continua sa functioneze fara modificare.
### Unde apare envelope-ul imbogatit
**1. `POST /v1/prezentari/valideaza` (dry-run)**
Campul `erori` (array) si campul `nemapate` (array) din raspuns contin obiecte cu toate cele 6 chei.
**2. `submissions.rar_error` (stocat in DB, vizibil prin `GET /v1/prezentari/{id}` si in dashboard)**
Campul `rar_error` e superset al formei de mai sus si variaza cu starea submission-ului:
- `needs_data` array de obiecte `{field, message, cod, problema, cauza, fix}`:
```json
[
{
"field": "dataPrestatie",
"message": "Data prestatiei nu poate fi anterioara datei de 01.12.2024.",
"cod": "RAR_VALIDARE",
"problema": "RAR a respins prezentarea",
"cauza": "Data prestatiei nu poate fi anterioara datei de 01.12.2024.",
"fix": "Corecteaza campul semnalat de RAR (vezi cauza) si reincearca; detaliile exacte sunt in mesajul tehnic RAR."
}
]
```
- `needs_mapping` (cod nemapat): obiect cu cheile `unmapped` (array), `cod`, `problema`, `cauza`, `fix`:
```json
{
"unmapped": ["SCHIMB_ULEI_COMPLET"],
"cod": "COD_NEMAPAT",
"problema": "Lipseste codul RAR al operatiei",
"cauza": "Operatia SCHIMB_ULEI_COMPLET nu are un cod RAR mapat.",
"fix": "Alege codul RAR pentru aceasta operatie in tab-ul Mapari (ai sugestii automate)."
}
```
- `needs_mapping` cu `auto_send` oprit: obiect cu `auto_send`, `cod: "AUTO_SEND_OPRIT"`, `problema`, `cauza`, `fix`.
- Eroare RAR 400: array imbogatit cu `cod: "RAR_VALIDARE"` pe fiecare element.
- Eroare RAR 401 (creds invalide): obiect cu `cod: "RAR_CREDS_INVALIDE"`, `problema`, `cauza`, `fix`.
**3. Erori de import (`POST /v1/import`, preview, commit)**
Campul `detail` din raspunsurile de eroare este superset: contine cheile vechi `error`/`message` plus `cod`, `problema`, `cauza`, `fix`.
**Exceptii din scope 5.4**: erorile de login/signup si CSRF raman mesaje plate (fara envelope imbogatit).
### Tabel cod → problema / fix (toate codurile din `app/errors.CATALOG`)
#### Validare date prestatie
| Cod | Problema | Fix |
|---|---|---|
| `VIN_FORMAT` | VIN invalid | Verifica VIN-ul pe talon (pozitia E) sau pe caroserie: exact 17 caractere majuscule, fara spatii si fara literele O, I, Q. |
| `NR_INMATRICULARE_FORMAT` | Numar de inmatriculare invalid | Foloseste doar litere si cifre majuscule, maxim 10 caractere, fara spatii sau cratima (ex. B123ABC). |
| `DATA_FORMAT` | Data prestatiei in format gresit | Scrie data ca AAAA-LL-ZZ (ex. 2026-06-22). |
| `DATA_PREA_VECHE` | Data prestatiei prea veche | RAR accepta prestatii doar incepand cu 01.12.2024; verifica data prestatiei. |
| `DATA_VIITOR` | Data prestatiei in viitor | Data prestatiei nu poate fi dupa ziua de azi; corecteaza data. |
| `ODOMETRU_FINAL_FORMAT` | Odometru final invalid | Scrie kilometrajul final ca numar intreg, fara zecimale sau text (ex. 145000). |
| `ODOMETRU_INITIAL_LIPSA` | Lipseste odometrul initial | Prestatiile R-ODO / I-ODO cer kilometrajul initial; completeaza-l. |
| `ODOMETRU_INITIAL_FORMAT` | Odometru initial invalid | Scrie kilometrajul initial ca numar intreg, fara zecimale sau text. |
| `ODOMETRU_INITIAL_ORDINE` | Odometru initial mai mare decat finalul | Kilometrajul initial trebuie sa fie mai mic sau egal cu cel final; verifica cele doua valori. |
| `PRESTATII_GOALE` | Nicio prestatie | Adauga cel putin o prestatie cu cod RAR valid. |
| `B64_INVALID` | Imaginea nu este base64 valid | Trimite imaginea codata base64 corect, sau omite campul daca nu ai imagine. |
#### Mapare operatie
| Cod | Problema | Fix |
|---|---|---|
| `COD_NEMAPAT` | Lipseste codul RAR al operatiei | Alege codul RAR pentru aceasta operatie in tab-ul Mapari (ai sugestii automate). |
| `AUTO_SEND_OPRIT` | Necesita confirmare manuala | Codul e mapat cu trimitere automata oprita; verifica randul si pune-l manual in coada. |
#### Erori RAR (raspuns live de la RAR)
| Cod | Problema | Fix |
|---|---|---|
| `RAR_VALIDARE` | RAR a respins prezentarea | Corecteaza campul semnalat de RAR (vezi cauza) si reincearca; detaliile exacte sunt in mesajul tehnic RAR. |
| `RAR_CREDS_INVALIDE` | Credentiale RAR invalide | Verifica email-ul si parola contului RAR in tab-ul Cont; trimiterea nu se reincearca automat la credentiale gresite. |
#### Import fisier
| Cod | Problema | Fix |
|---|---|---|
| `IMPORT_FISIER_PREA_MARE` | Fisier prea mare | Imparte fisierul in bucati de maxim 5000 de randuri si incarca-le pe rand. |
| `IMPORT_ANTET_NECLAR` | Antet de coloane neclar | Asigura-te ca primul rand contine numele coloanelor (ex. VIN, Numar, Data). |
| `IMPORT_ENCODING` | Codare de caractere nesuportata | Salveaza fisierul ca CSV UTF-8 (sau xlsx) si reincarca. |
| `IMPORT_FISIER_NERECUNOSCUT` | Fisier nerecunoscut | Incarca un fisier .xlsx sau .csv valid. |
| `IMPORT_MULTIPLE_SHEETS` | Mai multe foi in fisier | Pastreaza datele intr-o singura foaie sau alege foaia de import. |
| `IMPORT_FARA_MAPARE_COLOANE` | Coloanele nu sunt mapate | Mapeaza intai coloanele fisierului la campurile cerute, apoi continua. |
| `IMPORT_CONFIRMARE_GRESITA` | Numar confirmat gresit | Numarul confirmat difera de randurile gata de trimis; verifica preview-ul si reconfirma. |
| `IMPORT_OVERRIDE_ILIZIBIL` | Editarea anterioara nu se poate citi | Editarea salvata este ilizibila (probabil cheia s-a schimbat); reediteaza randul. |
| `COLOANE_FORMAT_JSON` | Format de coloane (JSON) invalid | Verifica sintaxa JSON a maparii de coloane (ghilimele duble, acolade inchise corect). |
## Nomenclator prestații (18 coduri, verificat live 2026-06-15)
| cod | nume |

View File

@@ -0,0 +1,395 @@
# PRD 5.4 — Erori pe 3 niveluri (problema + cauza + fix) pe API si UI
**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
Fiecare eroare pe care o vede un integrator (canal API) sau un service-auto (UI web) sa raspunda la
trei intrebari, in loc de una singura:
1. **Problema** — ce s-a intamplat (categorie umana, scurta). *"VIN invalid."*
2. **Cauza** — de ce, specific. *"VIN-ul are 16 caractere; RAR cere exact 17."*
3. **Fix** — ce sa faci acum. *"Verifica VIN-ul pe talon (pozitia E); 17 caractere majuscule, fara O/I/Q."*
Motivul (lentila DX, Etapa 5): erorile plate ("Fisier nerecunoscut", "cheie API invalida", "VIN invalid")
**transfera incertitudinea catre utilizator** — care fie ghiceste, fie deschide un tichet de suport. Cele
trei niveluri inchid bucla la sursa: mai putine tichete, integrare self-service.
**Invariant de corectitudine (motivul cheie de design):** cele trei niveluri pentru un anumit cod de
eroare se definesc **o singura data**, intr-un **catalog central pur** (`app/errors.py`), consumat de
**toate** suprafetele (API + UI + worker). Daca textul s-ar duplica pe canale, API si UI ar putea
diverge — un cod ar spune un lucru in JSON si altul in dashboard. Catalogul unic face imposibila
divergenta (acelasi pattern care a facut 5.2 corect: o singura sursa partajata, nu doua copii).
## 2. Non-Goals (anti scope-creep)
- **NU acoperim login / signup / CSRF / auth 401** (decizie utilizator 2026-06-22: focus pe fluxul de
declarare). Aceste suprafete sunt edge / dev si raman mesaje plate. `auth_routes.py`, `csrf.py`,
handler-ele `LoginRequired`/`AdminRequired`/`CsrfError` din `main.py` — NEATINSE.
- **NU breaking change pe API** (decizie utilizator 2026-06-22: aditiv). Pastram campurile existente
(`field`, `message`, `error`, `type`/`loc`/`msg`) si **ADAUGAM** `cod`, `problema`, `cauza`, `fix`.
Clientii vechi (ROAAUTO / soft propriu integrat la 5.1/5.2) nu se strica; cei noi pot afisa 3 niveluri.
- **NU schema noua** — `submissions.rar_error` e deja TEXT si stocheaza JSON; doar imbogatim continutul.
Zero migrare.
- **NU apel live nou la RAR** — pentru erorile RAR 400 imbracam mesajul RAR existent (passthrough ca
`cauza`) intr-un invelis 3-niveluri; nu schimbam clasificarea transient/terminal a worker-ului.
- **NU schimbam regulile de validare** — `validate_prezentare` valideaza exact aceleasi conditii;
doar ataseaza `cod` + nivelele la fiecare eroare existenta.
- **NU schimbam masina de stari / idempotenta / mapping-rezolvarea / nomenclatorul.**
- **NU traducere i18n** — romana, ca tot proiectul.
## 3. Stories atomice
### US-001: Catalog central de erori (`app/errors.py`) — DONE (588 teste)
**Ca** dezvoltator al gateway-ului **vreau** o singura sursa de adevar care mapeaza fiecare cod de
eroare la (problema, fix), cu un helper care construieste obiectul de eroare 3-niveluri, **pentru ca**
API-ul, UI-ul si worker-ul sa nu poata diverge in ce explica utilizatorului.
- **Depinde de**: —
- **Fisiere**: `app/errors.py` (modul pur nou), `tests/test_errors.py` (~2 fisiere)
- **Test intai (RED)**: `tests/test_errors.py`
- `test_catalog_complet` — fiecare intrare are `problema` + `fix` ne-goale (string).
- `test_eroare_construieste_3niveluri``eroare("VIN_FORMAT", field="vin", cauza="...")` intoarce
dict cu cheile `{field, cod, problema, cauza, fix, message}`, `cod=="VIN_FORMAT"`, `problema`/`fix`
luate din catalog, `cauza` cea data.
- `test_message_back_compat` — cand `cauza` e dat, `message == cauza` (alias pentru clientii vechi).
- `test_cod_necunoscut_ridica``eroare("INEXISTENT")` ridica `KeyError`/`ValueError` (nu inventeaza
text gol — drift prins la dezvoltare).
- **Acceptance criteria**:
- [ ] `app/errors.py` pur (fara import DB/HTTP), expune `CATALOG: dict[str, dict]` (cod → {problema, fix})
si `eroare(cod, *, field=None, cauza=None) -> dict`.
- [ ] Obiectul de eroare are exact cheile `{field, cod, problema, cauza, fix, message}`; `message`
(back-compat) `== cauza` cand `cauza` e dat, altfel `== problema`.
- [ ] CATALOG contine codurile pentru toate suprafetele in scop (validare continut, mapare op→cod,
RAR, import) — vezi lista din §4. Fiecare intrare are `problema` + `fix` ne-goale.
- [ ] **`fix` e specific si actionabil** (finding CEO): numeste un loc/o actiune concreta (ex. "talon,
pozitia E", "tab-ul Mapari", "salveaza ca .csv UTF-8"), NU boilerplate generic ("verifica datele").
Un fix generic = eroare plata mai lunga; testul de calitate al livrabilei. (Verificat la review uman.)
- [ ] `eroare` cu cod absent din CATALOG ridica eroare (nu intoarce text gol).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: unitar (modul pur) — fara canal.
### US-002: Validarea de continut emite 3 niveluri (`validation.py`) — DONE
**Ca** integrator API **vreau** ca fiecare eroare de validare (VIN/nr/data/odometru/prestatii/b64) sa
spuna problema + cauza + fix, **pentru ca** sa corectez payload-ul fara sa ghicesc formatul cerut.
- **Depinde de**: US-001
- **Fisiere**: `app/validation.py`, `tests/test_validation.py` (~2 fisiere)
- **Test intai (RED)**: `tests/test_validation.py`
- `test_vin_invalid_are_3niveluri` — VIN cu O/I/Q → eroare cu `cod=="VIN_FORMAT"`, `problema`,
`fix` ne-goale, `field=="vin"`, `message` pastrat (back-compat).
- `test_data_prea_veche_cod` / `test_data_viitor_cod` / `test_data_format_cod` — coduri distincte.
- `test_odometru_initial_lipsa_cod` / `test_odometru_ordine_cod` — coduri distincte.
- `test_prestatii_goale_cod`, `test_b64_invalid_cod`.
- `test_back_compat_field_message` — fiecare eroare are inca `field` + `message` (forma veche
pastrata pentru clientii existenti).
- `test_toate_codurile_in_catalog` — fiecare `cod` emis de `validate_prezentare` exista in `CATALOG`.
- **Acceptance criteria**:
- [ ] `validate_prezentare` intoarce erori cu `{field, message, cod, problema, cauza, fix}` (aditiv —
`field` + `message` neschimbate la octet fata de azi).
- [ ] Fiecare regula are un `cod` stabil (vezi lista §4); textul (problema/fix) vine din `errors.eroare`.
- [ ] **Byte-compat** (finding Eng): `cauza` = mesajul existent verbatim (eventual + context specific
precum lungimea VIN gasita); `message` ramane EXACT string-ul de azi → testele existente care
compara `message` raman verzi fara modificari.
- [ ] Toate testele existente (`test_validation.py`, `test_api.py`, `test_validare_dryrun.py`) raman
verzi (forma veche `field`/`message` intacta).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: canal API — `POST /v1/prezentari/valideaza` cu VIN invalid → `erori[0]` are cele
3 niveluri + `field`/`message` vechi.
### US-003: Propagare 3 niveluri prin mapare + raspuns API (`mapping.py`, `router.py`) — DONE
**Ca** integrator API **vreau** ca raspunsul `/valideaza` si motivul stocat (`rar_error`) sa transporte
cele 3 niveluri pentru validare SI pentru coduri nemapate / auto-send oprit, **pentru ca** verdictul sa
fie la fel de explicit indiferent de ramura (needs_data / needs_mapping).
- **Depinde de**: US-001, US-002
- **Fisiere**: `app/mapping.py`, `app/api/v1/router.py`, `app/models.py`, `tests/test_mapping.py`,
`tests/test_validare_dryrun.py` (~5 fisiere)
- **Test intai (RED)**:
- `test_mapping.py::test_unmapped_are_3niveluri` — cod_op_service necunoscut → `classify_prezentare`
produce `needs_mapping` cu `cod=="COD_NEMAPAT"` + problema/cauza (codul concret)/fix in structura.
- `test_mapping.py::test_auto_send_oprit_3niveluri` — mapare cu `auto_send=0``cod=="AUTO_SEND_OPRIT"`
+ 3 niveluri.
- `test_mapping.py::test_needs_data_pass_through` — erorile de validare imbogatite trec neatinse prin
`classify_prezentare` in `rar_error`.
- `test_validare_dryrun.py::test_erori_au_3niveluri``/valideaza` cu VIN invalid → `erori[i]` are
`cod/problema/cauza/fix`; cu cod_op nemapat → `nemapate` carry 3 niveluri.
- **Acceptance criteria**:
- [ ] `classify_prezentare` pastreaza erorile de validare imbogatite (pass-through) in `rar_error`.
- [ ] Ramura `needs_mapping` (cod nemapat) si nota `auto_send=0` se construiesc prin `errors.eroare`
(3 niveluri), nu string-uri ad-hoc.
- [ ] **`rar_error` stocat = SUPERSET al formei de azi** (finding Eng critic): pastreaza structura
veche (`needs_data` → array `[{field,message,...}]`; `needs_mapping``{unmapped:[...], ...}`),
ADAUGA cheile 3-niveluri. Asa `labels.motiv_uman` actual ramane functional intre Val 3 si Val 4
(nu se strica pana e actualizat in US-006) si nu e nevoie de migrare. Aplica principiul aditiv si
la datele stocate, nu doar la API.
- [ ] Raspunsul `/valideaza` (`erori`, `nemapate`) include `cod/problema/cauza/fix` aditiv; modelele
din `models.py` accepta cheile noi fara a respinge (verifica: tipul `erori`/`nemapate` e permisiv
sau extins; nu schema stricta care respinge chei in plus).
- [ ] **Teste subset, nu egalitate exacta** (finding Eng): testele existente care comparau `==` un dict
de eroare se actualizeaza la asertii de subset (comportament identic, doar chei aditive).
- [ ] `POST /v1/prezentari` (calea reala) ramane cu comportament identic — `test_api.py` verde;
`rar_error` stocat e JSON 3-niveluri pentru needs_data/needs_mapping.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: canal API — `/valideaza` cu (a) VIN invalid → needs_data + 3 niveluri, (b) cod_op
nemapat → needs_mapping + 3 niveluri. Regresia de aur: `POST /v1/prezentari` enqueue neschimbat.
### US-004: Erorile RAR (400/401) imbracate pe 3 niveluri in worker (`worker`, `rar_client.py`) — DONE
**Ca** service-auto **vreau** ca o respingere de la RAR sa fie tradusa in problema + cauza (mesajul RAR
exact) + fix, in loc de un JSON brut, **pentru ca** sa inteleg ce a respins RAR fara sa citesc JSON.
- **Depinde de**: US-001
- **Fisiere**: `app/worker/__main__.py`, `app/rar_client.py` (eventual), `tests/test_worker_*.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_worker_rar_errors.py` (nou) —
- `test_rar_400_stocheaza_3niveluri``RarError(status=400, field_errors=[{field,message}])`
worker stocheaza `rar_error` JSON cu `cod=="RAR_VALIDARE"`, `problema`, `cauza` continand mesajul
RAR exact (passthrough), `fix` cu indrumare; pastreaza si `field_errors` originale.
- `test_rar_401_creds_3niveluri``RarAuthError``cod=="RAR_CREDS_INVALIDE"` + 3 niveluri, stare
`error` (fara retry, neschimbat).
- `test_clasificare_transient_neschimbata` — 5xx/timeout raman transient (retry), comportament identic.
- **Acceptance criteria**:
- [ ] La RAR 400, `rar_error` stocat = SUPERSET (finding Eng): pastreaza array-ul `field_errors`
original `[{field,message}]` (ca `labels.py` actual sa-l randeze per-camp) + ADAUGA invelisul
3-niveluri (`cod=RAR_VALIDARE`, `cauza`=mesajul RAR exact passthrough, `fix`=indrumare).
- [ ] La RAR 401, `rar_error` = 3-niveluri (`cod=RAR_CREDS_INVALIDE`), stare `error` (fara retry).
- [ ] Clasificarea transient vs terminal NESCHIMBATA (5xx/408/429 retry; 4xx terminal); reconcilierea
anti-duplicat neatinsa.
- [ ] Fara echo de creds in `rar_error` (mesajul RAR nu contine parola; verificat in test).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: worker pe RAR test (daca exista creds) — prezentare cu VIN invalid → RAR 400 →
`needs_data` cu `rar_error` 3-niveluri vizibil in dashboard. (Live optional — vezi riscuri.)
### US-005: Erorile de import imbracate pe 3 niveluri (`import_router.py`) — DONE
**Ca** service-auto care incarca un fisier **vreau** ca erorile de upload / mapare coloane / commit sa
spuna ce e gresit, de ce si cum repar, **pentru ca** sa pot incarca singur fara suport.
- **Depinde de**: US-001
- **Fisiere**: `app/api/v1/import_router.py`, `tests/test_import_*.py` (~2 fisiere)
- **Test intai (RED)**: `tests/test_import_errors.py` (nou) —
- `test_fisier_prea_mare_3niveluri` — 413 → detail contine `cod=="IMPORT_FISIER_PREA_MARE"` +
problema/cauza (nr randuri vs max)/fix; pastreaza `error`/`message` vechi.
- `test_antet_neclar_3niveluri` — HeaderError → `cod=="IMPORT_ANTET_NECLAR"` + 3 niveluri + `found`.
- `test_encoding_3niveluri`, `test_fisier_nerecunoscut_3niveluri`, `test_multiple_sheets_3niveluri`.
- `test_fara_mapare_coloane_3niveluri` — preview fara mapare → `IMPORT_FARA_MAPARE_COLOANE`.
- `test_confirmare_gresita_3niveluri` — commit cu n gresit → `IMPORT_CONFIRMARE_GRESITA` + `n_ok`.
- `test_override_ilizibil_3niveluri` — editare cu override corupt → `IMPORT_OVERRIDE_ILIZIBIL`.
- **Acceptance criteria**:
- [ ] Fiecare `HTTPException` de import in scop are `detail` SUPERSET: pastreaza `error`/`message`/
campurile contextuale existente (`sheets`/`found`/`n_ok`) si ADAUGA `cod/problema/cauza/fix`.
- [ ] Codurile vin din `errors.eroare` (catalog), nu string-uri ad-hoc.
- [ ] Toate testele de import existente raman verzi (forma veche `error`/`message` intacta; asertii de
subset unde comparau exact — finding Eng).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: canal API — `POST /v1/import` cu fisier prea mare / antet neclar → detail 3-niveluri.
### US-006: Componenta UI de eroare pe 3 niveluri + stari submission (`labels.py`, templates core) — DONE
**Ca** service-auto **vreau** ca dashboard-ul sa afiseze problema (bold) + cauza + fix (linie de actiune)
pentru randurile needs_data / needs_mapping / error si per-camp in preview, **pentru ca** sa stiu ce sa
fac fara sa deschid un tichet.
- **Depinde de**: US-001, US-002, US-003, US-004 (codurile/structura trebuie sa existe)
- **Fisiere**: `app/web/labels.py`, `app/web/templates/_eroare.html` (macro nou), `app/web/templates/base.html`
(CSS), `app/web/templates/_trimitere_detaliu.html`, `app/web/templates/_status.html`,
`app/web/templates/_preview_rand.html`, `tests/test_web_*.py` (~7 fisiere)
- **Test intai (RED)**: `tests/test_web_erori.py` (nou) —
- `test_motiv_uman_3niveluri``labels.motiv_uman` pe `rar_error` 3-niveluri intoarce
problema/cauza/fix (nu doar un string plat); fallback gratios la rar_error vechi/string/corupt.
- `test_detaliu_afiseaza_fix``/_fragments/...` pe submission needs_data → HTML contine textul `fix`.
- `test_preview_rand_per_camp_fix` — preview rand needs_data → fiecare camp invalid arata `fix`-ul.
- **Acceptance criteria**:
- [ ] **Progresiv, dashboard compact pastrat** (finding Design — nu regresa 3.5/3.6): in lista/rand
se vede problema (eticheta umana existenta) + fix-ul ca o singura linie de actiune; cauza +
mesajul tehnic RAR integral stau in detaliu / `<details>` (nu 3 linii per rand → zid de text).
Cele 3 niveluri complete apar in panoul de detaliu si in preview-ul de rand, nu in fiecare rand din lista.
- [ ] **Scannabil** (finding Design): nivelele au tratament vizual / etichete care le fac parcurgibile
fara citire integrala (ex. "Problema" / "De ce" / "Cum repari", sau ierarhie vizuala clara).
- [ ] Macro Jinja `_eroare.html` randeaza consistent; mesajul tehnic RAR integral ramane in `<details>`
(pattern existent in `_trimitere_detaliu.html`).
- [ ] `labels.py` citeste catalogul / parseaza `rar_error` 3-niveluri; degradeaza gratios pe forma
veche (string / `[{field,message}]` / JSON corupt) — fara 500 (lectia 3.6 cu decriptarea).
- [ ] Reutilizeaza, NU inlocuieste, pattern-ul bun din `_status.html` (problema + subtext-hint deja
~ problema + fix); `_trimitere_detaliu.html`, `_preview_rand.html` folosesc macro-ul.
- [ ] CSS in paleta light+dark din 5.3 — fara culori hardcodate; accentul de "fix/actiune" trece AA
in AMBELE teme (lectia 5.3: `--ok` pica AA ca text); distinct de rosul de eroare.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser HTMX pe `/` — rand needs_data arata problema+cauza+fix; preview rand
invalid arata fix per-camp; dark + light (5.3) ambele lizibile.
### US-007: 3 niveluri in import / upload / preview UI + rute web (`routes.py`, templates import) — DONE
**Ca** service-auto **vreau** ca erorile de la upload, mapare coloane si preview import sa apara cu cele
3 niveluri in interfata, **pentru ca** sa rezolv singur problemele de fisier.
- **Depinde de**: US-006 (macro `_eroare.html`), US-005 (codurile de import)
- **Fisiere**: `app/web/routes.py`, `app/web/templates/_upload.html`, `app/web/templates/_mapcoloane.html`,
`app/web/templates/_preview_import.html`, `tests/test_web_*.py` (~5 fisiere)
- **Test intai (RED)**: `tests/test_web_import_erori.py` (nou) —
- `test_upload_eroare_3niveluri` — upload fisier invalid prin ruta web → fragment contine problema+fix.
- `test_mapcoloane_format_json_3niveluri` — format coloane JSON invalid → `COLOANE_FORMAT_JSON` 3 niveluri.
- `test_cod_rar_necunoscut_3niveluri` — mapare operatie cu cod RAR inexistent → 3 niveluri + sugestie.
- **Acceptance criteria**:
- [ ] Caile web de eroare din `routes.py` (upload, mapare coloane, format JSON, cod RAR necunoscut,
corectie) trec context 3-niveluri catre template (din catalog), nu string plat.
- [ ] `_upload.html`, `_mapcoloane.html`, `_preview_import.html` folosesc macro-ul `_eroare.html`.
- [ ] Forma veche (mesaj plat) inca functioneaza unde nu exista cod (fara regresie); toate testele web
existente verzi.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser HTMX — upload fisier prea mare → 3 niveluri; mapare coloane JSON invalid → fix.
### US-008: Documentare envelope de eroare imbogatit (`api-rar-contract.md`) — DONE
**Ca** integrator nou **vreau** sa stiu forma exacta a erorilor (campuri vechi + cele 3 niveluri noi) si
lista de coduri, **pentru ca** sa-mi construiesc gestionarea de erori fara reverse-engineering.
- **Depinde de**: US-001, US-002, US-003, US-005 (codurile finale)
- **Fisiere**: `docs/api-rar-contract.md` (~1 fisier)
- **Test intai (RED)**: — (doc; verificare manuala). Optional `tests/test_errors.py::test_doc_acopera_codurile`
daca e fezabil ieftin (verifica ca fiecare cod din CATALOG apare in doc).
- **Acceptance criteria**:
- [ ] Sectiune noua in `api-rar-contract.md`: forma erorii (`{field, message, cod, problema, cauza, fix}`),
nota de back-compat (campurile vechi raman), tabel cod → problema/fix.
- [ ] Mentioneaza ca `/valideaza` si `rar_error` stocat folosesc aceeasi forma.
- **Verificare E2E**: review uman al documentului.
## 4. Catalog de coduri (referinta — definit in US-001)
| Domeniu | Cod | problema (nivel 1) | unde |
|---|---|---|---|
| Validare | `VIN_FORMAT` | VIN invalid | US-002 |
| Validare | `NR_INMATRICULARE_FORMAT` | Numar de inmatriculare invalid | US-002 |
| Validare | `DATA_FORMAT` | Data prestatiei in format gresit | US-002 |
| Validare | `DATA_PREA_VECHE` | Data prestatiei prea veche | US-002 |
| Validare | `DATA_VIITOR` | Data prestatiei in viitor | US-002 |
| Validare | `ODOMETRU_FINAL_FORMAT` | Odometru final invalid | US-002 |
| Validare | `ODOMETRU_INITIAL_LIPSA` | Lipseste odometrul initial | US-002 |
| Validare | `ODOMETRU_INITIAL_FORMAT` | Odometru initial invalid | US-002 |
| Validare | `ODOMETRU_INITIAL_ORDINE` | Odometru initial > final | US-002 |
| Validare | `PRESTATII_GOALE` | Nicio prestatie | US-002 |
| Validare | `B64_INVALID` | Imaginea nu e base64 valid | US-002 |
| Mapare | `COD_NEMAPAT` | Lipseste codul RAR al operatiei | US-003 |
| Mapare | `AUTO_SEND_OPRIT` | Necesita confirmare manuala | US-003 |
| RAR | `RAR_VALIDARE` | RAR a respins prezentarea | US-004 |
| RAR | `RAR_CREDS_INVALIDE` | Credentiale RAR invalide | US-004 |
| Import | `IMPORT_FISIER_PREA_MARE` | Fisier prea mare | US-005 |
| Import | `IMPORT_ANTET_NECLAR` | Antet de coloane neclar | US-005 |
| Import | `IMPORT_ENCODING` | Codare de caractere nesuportata | US-005 |
| Import | `IMPORT_FISIER_NERECUNOSCUT` | Fisier nerecunoscut | US-005 |
| Import | `IMPORT_MULTIPLE_SHEETS` | Mai multe foi in fisier | US-005 |
| Import | `IMPORT_FARA_MAPARE_COLOANE` | Coloanele nu sunt mapate | US-005 |
| Import | `IMPORT_CONFIRMARE_GRESITA` | Numar confirmat gresit | US-005 |
| Import | `IMPORT_OVERRIDE_ILIZIBIL` | Editarea anterioara nu se poate citi | US-005 |
| Coloane | `COLOANE_FORMAT_JSON` | Format de coloane (JSON) invalid | US-007 |
> Lista finala se fixeaza in US-001 (catalog) + drift-test (`test_toate_codurile_in_catalog`). Codurile
> nu sunt parte din contractul de back-compat (campuri noi); mesajele RAR exacte raman in `cauza`.
## 4b. Intrebari deschise (rezolvate inainte de executie)
- **Latimea scope-ului** — REZOLVAT: focus pe fluxul de declarare (validare continut, RAR 400, import,
mapare op→cod); login/signup/CSRF/auth raman plate. [user 2026-06-22]
- **Compatibilitate API** — REZOLVAT: aditiv, fara breaking change (campuri vechi pastrate, adaugam
`cod/problema/cauza/fix`); documentat in `api-rar-contract.md`. [user 2026-06-22]
## 5. Riscuri
- **Drift catalog** (cod folosit dar absent din CATALOG, sau text gol) → `errors.eroare` ridica pe cod
necunoscut (US-001) + `test_toate_codurile_in_catalog` (US-002) — drift prins la dezvoltare, nu in prod.
- **`fix` generic, fara valoare** (finding CEO) → criteriu de calitate explicit (US-001): fiecare `fix`
numeste loc/actiune concreta; verificat la review uman + in design-review-ul UI.
- **`rar_error` stocat schimbat rupe `labels.py` intre valuri** (finding Eng) → stocam SUPERSET (old keys
intacte) in US-003/US-004; `labels.py` actual ramane functional pana la US-006; zero migrare.
- **Teste cu egalitate exacta de dict** (finding Eng) → se trec la asertii de subset (acelasi comportament,
chei aditive); contractul de back-compat e pe campurile vechi, nu pe absenta celor noi.
- **Breaking change accidental pe API** → aditiv prin constructie + testele existente (`test_api.py`,
`test_validare_dryrun.py`, `test_import_*.py`) sunt contractul de back-compat: raman verzi = forma
veche `field`/`message`/`error` intacta. AC explicit in fiecare story backend.
- **500 la afisare UI pe `rar_error` vechi/corupt** (lectia 3.6: decriptare neprotejata) → `labels.py`
degradeaza gratios pe forma veche (string / `[{field,message}]` / JSON corupt), test dedicat (US-006).
- **Scurgere de creds prin `cauza`** (mesaj RAR passthrough) → mesajele RAR de validare nu contin parola
(field/message pe campuri de prezentare); test no-echo (US-004). Handler-ul 422 din `main.py` deja
dropeaza `input`/`ctx`.
- **Suprafata mare** → 8 stories pe valuri cu fisiere disjuncte (vezi §6); backend (US-002/004/005)
paralel, UI secvential dupa backend, docs paralel.
- **Verbozitate UI** (3 niveluri = zgomot) → progresiv: problema + fix vizibile, mesajul tehnic RAR
integral ramane in `<details>` (pattern existent in `_trimitere_detaliu.html`).
- **Conflict pe fisiere comune** (lectia 5.1: clobber la worktree/merge) → mapping.py atins doar de
US-003; routes.py doar de US-007; templates partitionate intre US-006 (detaliu/status/preview_rand) si
US-007 (upload/mapcoloane/preview_import); macro `_eroare.html` creat in US-006, consumat in US-007
(dependenta de val, nu paralel).
## 6. Valuri de executie (graful de dependente)
```
Val 1: [US-001] backbone catalog (singur — toti depind de el)
Val 2: [US-002] [US-004] [US-005] backend paralel, fisiere disjuncte
validation.py worker/rar import_router.py
Val 3: [US-003] mapping.py + router.py + models.py (depinde US-002)
Val 4: [US-006] [US-008] UI core (labels+templates) || docs (api-rar-contract.md) — disjuncte
Val 5: [US-007] UI import/web (depinde US-006 pt macro)
```
- **Val 2**: max 2-3 teammates simultan (ROADMAP §5.5). validation.py / worker+rar_client / import_router.py
sunt disjuncte → 3 teammates paraleli OK.
- **Val 4**: US-006 (templates+labels) si US-008 (doc) ating fisiere disjuncte → paralel.
- Dupa fiecare val: lead-ul ruleaza `python3 -m pytest -q` (regresie) si bifeaza stories in PRD.
## 7. Review-uri de plan (aplicate inainte de cod — ROADMAP §5.3)
> Obligatorii: `/plan-ceo-review` (valoare/scope) + `/plan-eng-review` (fezabilitate/teste).
> `/plan-design-review` — DA (atinge UI: US-006, US-007). Rezultatele se aplica IN acest PRD inainte de cod.
**CEO (valoare/scope) — PASS.** Problema corecta (DX: erorile plate transfera incertitudinea la user →
tichete de suport), aliniata cu directia Etapa 5. Scope-ul (declaration flow) e cel mai direct la valoare;
login/signup/CSRF taiate corect (decizie user). **Inversiune ("ce-l face sa esueze?"):** un `fix` generic
("verifica datele") face dintr-o eroare 3-niveluri doar o eroare plata mai lunga — valoarea traieste sau
moare in specificitatea fix-ului. → Aplicat ca criteriu de calitate explicit (US-001 AC + risc): fiecare
`fix` numeste loc/actiune concreta. **Deferare constienta:** `POST /v1/prezentari` real intoarce doar
`status`, nu `erori` inline — integratorul afla "de ce" printr-un GET sau prin `/valideaza`; a adauga
`erori` inline pe ruta reala ar fi aditiv + util, dar e scope creep peste 5.4 → notat ca oportunitate
viitoare, nu in scope acum.
**Eng (fezabilitate/teste) — PASS cu 3 conditii (aplicate in PRD).** Catalogul pur + helper = backbone
fezabil, drift prins la dev. **(1) Critic — `rar_error` stocat trebuie SUPERSET** (old keys intacte): altfel
`labels.motiv_uman` se strica intre Val 3 (backend schimba forma) si Val 4 (UI o citeste); superset =
zero migrare + degradare gratioasa (lectia 3.6). Aplicat in US-003/US-004 AC + risc. **(2) Byte-compat**:
validarea pune mesajul existent verbatim ca `cauza`, `message` ramane identic → testele pe `message`
raman verzi (US-002). **(3) Teste subset, nu egalitate exacta** de dict (chei aditive) — aplicat in
US-002/003/005. `models.py` trebuie sa accepte chei in plus pe `erori`/`nemapate` (verificat in US-003).
Worker testabil cu `rar_client` mock-uit (fara live RAR).
**Design (UI — US-006/US-007) — PASS cu 3 conditii (aplicate in PRD).** **(1) Progresiv, nu regresa
dashboard-ul compact din 3.5/3.6**: in lista/rand → problema + fix pe o linie; cauza + tehnic RAR in
detaliu/`<details>`; cele 3 niveluri complete doar in panoul de detaliu + preview-ul de rand. **(2)
Scannabil**: etichete/ierarhie vizuala ("Problema"/"De ce"/"Cum repari") ca user-ul sa parcurga fara
citire integrala. **(3) AA in ambele teme** (lectia 5.3): accentul de "fix/actiune" trece AA light+dark,
fara culori hardcodate, distinct de rosul de eroare. Reutilizeaza pattern-ul bun din `_status.html`
(problema + subtext = ~ problema + fix), nu il inlocui.
---
## Raport VERIFY
Verificator independent (context curat, rol qa-only), 2026-06-22. **VERDICT GLOBAL: PASS.**
**1. Suita — PASS.** `python3 -m pytest -q` → 628 passed, 234 warnings.
**2. Acceptance criteria US-001..US-008 — toate PASS.** Verificate direct pe cod + probe live (TestClient + SQLite temp):
- US-001: catalog pur, 24 coduri (`MISSING:[]`, `EMPTY:[]`), `eroare('INEXISTENT')`→KeyError, chei `{field,cod,problema,cauza,fix,message}`, `message==cauza`/`==problema` corect.
- US-002: **byte-compat confirmat**`message` VIN identic la octet cu `git show HEAD:app/validation.py` (textul vechi pus ca `cauza`); erori complete pe 6 chei.
- US-003: **`rar_error` SUPERSET confirmat** — needs_mapping pastreaza `unmapped`; auto_send pastreaza `auto_send`; needs_data ramane array cu `field`+`message`; `nemapate` din `/valideaza` poarta 3 niveluri; `models.py:113-114` permisiv (`list[dict]`).
- US-004: RAR 400→`RAR_VALIDARE` (`field`/`message` pastrate, cauza=mesaj RAR passthrough), RAR 401→`RAR_CREDS_INVALIDE` fara retry fara echo creds; `_is_transient` neschimbat.
- US-005: detalii import superset (`error`/`message`/`sheets`/`found`/`n_ok` + 3 niveluri).
- US-006: `parse_erori` degradeaza gratios (string plat / `[{field,message}]` fara cod / JSON invalid / None — fara exceptie); detaliu randeaza Problema/De ce/Cum repari + `<details>` tehnic; CSS doar variabile paleta, AA in ambele teme (accent 5.17/5.33, err 4.83/5.06), accent ≠ rosu.
- US-007: upload/mapcoloane prin macro; per-camp `camp-fix`.
- US-008: contract documentat (forma 6 chei, back-compat, tabel cod→problema/fix complet).
- **Calitate `fix` (finding CEO) — PASS**: specifice/actionabile ("talon pozitia E", "tab-ul Mapari", "CSV UTF-8", "maxim 5000 randuri"); niciun fix generic.
- **Non-Goal — PASS**: `auth_routes.py`/`csrf.py`/`main.py` NEATINSE (confirmat `git status`).
**3. E2E canal API — PASS.** `/valideaza`: (a) VIN invalid → `erori[0]` cu `cod/problema/cauza/fix` + `field`/`message` vechi; (b) cod_op nemapat → `nemapate[0]` cu `cod_op_service`/`denumire` + 3 niveluri. `POST /v1/prezentari` real → `200 {status:queued}`.
**4. E2E canal web (UI, TestClient pe fragmente) — PASS.** Upload invalid → `_upload.html` cu `eroare-3n` + "Cum repari" + fix; submission needs_data → `_trimitere_detaliu.html` cu 3 niveluri + `<details>` tehnic. (Browser Playwright neutilizat — fragmentele TestClient acopera criteriile.)
**5. Regresia de aur — PASS (live neprobat, conform asteptarii).** `POST /v1/prezentari`→queued + `test_api.py` verde. Flux LIVE RAR (worker→FINALIZATA pe RAR test) NEPROBAT — lipsesc `AUTOPASS_CREDS_KEY`+creds test+`--send` in mediu; NU e FAIL al 5.4 (endpoint-urile/UI noi nu ating trimiterea; worker-ul doar imbogateste `rar_error`).
**Observatie minora (ne-blocanta), REPARATA la CLOSE:** exemplul JSON din `api-rar-contract.md` avea `message`/`cauza` cu un text VIN usor diferit de cel real emis de `validation.py:72`. Corectat (ambele linii) sa coincida verbatim cu codul.