# Contract RAR AUTOPASS — sursa de adevăr (verificat live)

> **Acesta este documentul autoritativ pentru contractul API RAR AUTOPASS.**
> Înlocuiește presupunerile din planurile vechi acolo unde diferă. Dacă un plan
> contrazice acest fișier, **acest fișier are dreptate**.
>
> Surse:
> - `docs/api-rar-documentatie-oficiala.md` — răspuns oficial de la programatorii RAR.
> - Verificare live pe endpoint-ul de **test** (`/public/login` + `getNomenclatorPrestatii`), **2026-06-15**.
> - Cod VFP testat (`rar_autopass.prg`) — confirmat ca URL-uri corecte.

## Endpoint-uri

| Mediu | Bază |
|---|---|
| TEST (integrare — doar aici se testează) | `https://apps.rarom.ro/test-rar-autopass` |
| PRODUCȚIE | `https://apps.rarom.ro/rar-autopass` |
| Swagger test | `https://apps.rarom.ro/test-rar-autopass/swagger-ui/index.html` |

⚠️ `…/v3/api-docs`, `…/v2/api-docs`, `…/swagger-resources` întorc **403** (nu se pot
descărca programatic). Swagger-ul nu permite autentificare directă — testarea reală
se face din cod / Postman cu credențialele de test.

### Rute confirmate

| Operație | Metodă + cale | Stare |
|---|---|---|
| Login | `POST /public/login` | ✅ verificat live |
| Nomenclator prestații | `GET /nomenclator/getNomenclatorPrestatii` | ✅ verificat live (200) |
| Adăugare prezentare | `POST /prezentari/postPrezentare` | din VFP testat + doc oficial |

> Nota: doc-ul oficial citează operationId-ul Swagger `getPrestatiiNomUsingGET`, dar
> **calea reală e `/nomenclator/getNomenclatorPrestatii`** (cea din VFP). `/nomenclator/getPrestatiiNom`
> întoarce 403 — nu o folosi. operationId ≠ path.

## ⚠️ Header obligatoriu pe TOATE apelurile: `User-Agent`

WAF-ul RAR întoarce **`403 Forbidden` („Request forbidden by administrative rules")**
la orice request **fără header `User-Agent` de browser** — inclusiv `/public/login` și
chiar `swagger-ui`. curl/clienți fără UA = blocați la WAF înainte de a ajunge la aplicație.
MSXML2.XMLHTTP din VFP trimite un UA implicit, de aceea VFP-ul merge.

→ Gateway-ul (`app/rar_client.py`) **trebuie** să seteze `User-Agent` pe fiecare apel
(ex. `Mozilla/5.0`). Confirmat live 2026-06-15: fără UA → 403; cu UA + creds greșite → 401;
cu UA + creds corecte → 200.

## Autentificare

`POST /public/login`  body `{"email": "...", "password": "..."}` → 200, JSON cu câmpuri:

```
idUser, idAgent, cui, nume, prenume, email, token, activationToken,
activ, dataActivarii, dataSfarsit, blocat, expirat, tokenExists, authorities
```

- Tokenul JWT se atașează la apelurile securizate: `Authorization: Bearer {token}`.
- **JWT claims** (decodate live): `{ jti: "mobileJWT", sub: <email>, authorities: ["ADMIN"], iat, exp }`.
- **⚠️ JWT TTL = 108000 secunde = 30 de ORE** (`exp - iat`). **NU e un token scurt.**
  Vezi „Corecții față de planuri" #1 — schimbă strategia de robustețe a worker-ului.
- `idUser` vine din răspunsul de login (ex. live: `6766`); `idAgent` poate fi `null` la login,
  dar serverul atribuie `idAgent` pe prezentare (vezi exemplul de răspuns oficial: `idAgent: 1587`).

## postPrezentare — payload (request)

Câmpurile marcate OPTIONAL pot lipsi; restul sunt obligatorii. `tipPrestatie` **NU se trimite**
(îl generează serverul).

```json
{
  "vin": "XXXXXXXXXXXXXXXXX",
  "nrInmatriculare": "B999GEN",
  "dataPrestatie": "2024-07-25",
  "odometruFinal": "9999999",
  "odometruInitial": null,
  "prestatii": [
    { "codPrestatie": "OE-1", "idPrezentare": null },
    { "codPrestatie": "OE-2", "idPrezentare": null }
  ],
  "sistemReparat": "null",
  "status": "FINALIZATA",
  "obs": "TEST",
  "b64Image": "UklGR...."
}
```

| Câmp | Obligatoriu | Note |
|---|---|---|
| `vin` | DA | 17 caractere, MAJUSCULE, fără spații/caractere speciale, **fără literele O, I, Q** |
| `nrInmatriculare` | DA | max 10 caractere, litere + cifre, MAJUSCULE, fără spații/caractere speciale |
| `dataPrestatie` | DA | format `YYYY-MM-DD`; **nu mai devreme de 2024-12-01**, **nu mai târziu de azi** |
| `odometruFinal` | DA | indicația km la final (exemplul oficial îl trimite ca string `"9999999"`) |
| `odometruInitial` | condiționat | `null` normal; **OBLIGATORIU dacă `prestatii` conține `R-ODO` sau `I-ODO`** (indicația dinainte de reparație/schimbare) |
| `prestatii` | DA | listă `{codPrestatie, idPrezentare:null}`; codurile din nomenclator |
| `status` | DA | **întotdeauna `"FINALIZATA"`** prin API |
| `sistemReparat` | DA (poate fi `"null"`) | exemplul oficial trimite string-ul `"null"`; valori reale nedocumentate (vezi Open Q) |
| `obs` | NU (OPTIONAL) | text liber |
| `b64Image` | NU (OPTIONAL) | poza odometrului; **nu mai e obligatorie**; dacă se atașează, trebuie format base64 valid |

### postPrezentare — răspuns (success) — VERIFICAT LIVE 2026-06-15

Răspuns real pe contul nostru de test (record creat `data.id=68514`):

```json
{
  "statusCode": 200,
  "message": "Prezentare adaugata cu succes",
  "data": {
    "id": 68514,
    "dataPrestatie": "2026-06-15",
    "vin": "WVWZZZ1KZAW000123",
    "odometruFinal": 123456,
    "idAgent": 40,
    "tipPrestatie": "GENERIC",
    "odometruInitial": null,
    "idUser": 6766,
    "sistemReparat": "null",
    "obs": "TEST GATEWAY",
    "nrInmatriculare": "B999TST",
    "listaPrestatii": null,
    "status": "FINALIZATA",
    "prestatii": [
      { "idPrezentare": 68514, "codPrestatie": "OE-1" },
      { "idPrezentare": 68514, "codPrestatie": "OE-2" }
    ],
    "b64Image": null
  }
}
```

- `data.id` = ID-ul prezentării la RAR (de reținut ca `idPrezentare` în submission).
- **`prestatii[].idPrezentare == data.id`** (live: ambele 68514). Exemplul vechi sugera un
  număr separat mai mare (599950 vs 59950) — **fals**, e același id.
- `idAgent` = **atribuit de server** (login a întors `null`, răspunsul are `40`).
- `odometruFinal` trimis ca string `"123456"` → **întors ca număr** `123456` (server normalizează).
- `sistemReparat:"null"` acceptat; `b64Image` omis → `null`; `odometruInitial:null` OK.
- Câmp extra nedocumentat în răspuns: **`listaPrestatii: null`** (prezent lângă `prestatii`).
- `tipPrestatie` = `"GENERIC"` — **generat de server**, nu input client.

### postPrezentare — răspuns (eroare validare) — VERIFICAT LIVE 2026-06-15

HTTP **400**. `data` este un **ARRAY** de `{field, message}` (NU string), un element per eroare:

```json
{
  "statusCode": 400,
  "message": "Validare eșuată pentru cererea de prezentare.",
  "data": [
    { "field": "vin", "message": "VIN trebuie să aibă exact 17 caractere, fara spatii sau caractere speciale, litere invalide :  O, I, Q." },
    { "field": "vin", "message": "VIN conține caractere invalide, spatii, sau  O, I, Q." }
  ]
}
```

Mesaje exacte capturate live (de mapat în UI/loguri gateway):

| Constrângere încălcată | field | message (exact) |
|---|---|---|
| VIN cu O/I/Q | `vin` | `VIN trebuie să aibă exact 17 caractere, fara spatii sau caractere speciale, litere invalide :  O, I, Q.` + `VIN conține caractere invalide, spatii, sau  O, I, Q.` |
| dataPrestatie < 2024-12-01 | `dataPrestatie` | `Data prestatiei nu poate fi anterioara datei de 01.12.2024.` |
| dataPrestatie în viitor | `dataPrestatie` | `Data prestatiei nu poate fi în viitor.` |

→ Gateway: parsează `data` ca listă de erori de câmp (nu citi `data.message`). Pe success `data`
e obiect; pe 400 `data` e array — discriminează după `statusCode`/HTTP code.

## Reguli de validare (server-side RAR, de aplicat și în gateway)

Aplicate deja pe ambele medii (test + producție):

1. **dataPrestatie**: `>= 2024-12-01` și `<= azi`.
2. **VIN**: exact 17 caractere, majuscule, fără spații/caractere speciale, **fără O, I, Q**.
3. **nrInmatriculare**: max 10 caractere, litere + cifre, majuscule, fără spații/caractere speciale.
4. **b64Image**: dacă e prezent, trebuie base64 valid.
5. **odometruInitial**: obligatoriu dacă `prestatii` conține `R-ODO` sau `I-ODO`.

→ Acestea devin reguli Pydantic exacte în `app/api`. Validează la gateway înainte de enqueue
(stare `needs_data`) ca să 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_EROARE_SERVER` | RAR a esuat la inregistrarea prezentarii | RAR a raspuns cu o eroare de server (vezi cauza). Trimiterea NU se reincearca automat si NU a fost confirmata — verifica datele (in special codul prestatiei) si re-trimite dupa corectare. |
| `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. |

> **Clasificarea esecurilor RAR la `postPrezentare` (worker).** Un **400** -> `needs_data`
> (validare continut). Un **500 cu corp de eroare** (`{statusCode,message}`, ex. `ORA-12899`)
> e un esec DEFINITIV: RAR a raspuns „am esuat", deci NU e o pierdere de raspuns ambigua
> -> worker-ul marcheaza `error` (`RAR_EROARE_SERVER`), **fara reconciliere si fara retry**
> (altfel ar marca fals `sent` pe un record PARTIAL pe care RAR, ne-tranzactional, il lasa
> la esec). Doar erorile **ambigue** — timeout / TransportError / 502/503/504 / 429 / 408 —
> declanseaza reconcilierea anti-duplicat + retry cu backoff.

#### 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 |
|---|---|
| OE-1 | REPARAȚIE |
| OE-2 | INTRETINERE |
| OE-3 | REVIZIE PERIODICA |
| OE-4 | REGLARE FUNCTIONALA |
| OE-5 | MODIFICARE CONSTRUCTIVA |
| OE-6 | RECONSTRUCTIE |
| OE-7 | ACTUALIZARE SOFTWARE |
| OE-8 | INLOCUIRE SEZONIERA A ANVELOPELOR |
| OE-D | AVARII GRAVE LA SISTEMUL DE DIRECTIE |
| OE-F | AVARII GRAVE LA SISTEMUL DE FRANARE |
| OE-C | AVARII GRAVE LA STRUCTURA DE REZISTENTA A CAROSERIEI |
| OE-S | AVARII GRAVE LA STRUCTURA DE REZISTENTA A SASIULUI |
| OE-R | AVARII GRAVE LA UN SISTEM DE RETINERE SI PROTECTIE IN CAZ DE ACCIDENT |
| OE-A | AVARII GRAVE LA UN SISTEM AVANSAT DE ASISTENTA A CONDUCATORULUI AUTO (ADAS) |
| OE-I | ISTORICUL INDICATIEI ODOMETRULUI (vehicule anterior inmatriculate in alte tari) |
| AITLV | INREGISTRARE ATELIER INSPECTIE TAHOGRAFE / LIMITATOARE DE VITEZA |
| **R-ODO** | **REPARATIE ODOMETRU** → declanșează `odometruInitial` obligatoriu |
| **I-ODO** | **INLOCUIRE ODOMETRU** → declanșează `odometruInitial` obligatoriu |

> `numePrestatie` redat prescurtat pentru OE-D…OE-A (în API e textul lung complet).
> Nomenclatorul se ia live din API; nu hard-coda — folosește acest tabel doar ca referință.

## Ciclu de viață prezentare (la RAR) — IMPORTANT

- API-ul are **un singur scop: INSERT prezentări cu status `FINALIZATA`.** Toate celelalte
  operațiuni CRUD se fac din interfața web.
- **Prezentările `FINALIZATA` NU se pot anula prin API** (nici din web). Doar cele `SALVATA`
  sunt anulabile/editabile, dar API nu produce `SALVATA`.
  - Încercarea de anulare a unei `FINALIZATA` întoarce: `EROARE_STATUS_ANULARE("... Id not found.")`.
- Corecția datelor eronate (după FINALIZATA) = solicitare la **suport.autopass@rarom.ro**
  (pe test nu e cazul). **Nu există flux API de corecție/anulare pentru records-urile noastre.**

## Monitorizare (citire prezentări) — VERIFICAT LIVE 2026-06-15

**Rută:** `GET /prezentari/getAllPrezentariFinalizate` (Bearer). Confirmat live.
**Răspuns:** `{statusCode, message, data: {totalCount, content: [...]}}` — listă în `data.content`.

Fiecare item din `content` (live):
```json
{
  "id": 68514, "dataPrestatie": "2026-06-15", "vin": "WVWZZZ1KZAW000123",
  "odometruFinal": 123456, "idAgent": 40, "tipPrestatie": null,
  "odometruInitial": null, "idUser": 6766, "sistemReparat": null, "obs": "...",
  "nrInmatriculare": "B999TST", "listaPrestatii": null, "status": "FINALIZATA",
  "prestatii": null, "b64Image": null
}
```

- **`odometruFinal` e NUMĂR** (int) în listare (deși la `postPrezentare` se trimite string). Reconcilierea
  compară ca int.
- Pe **test**: `prestatii` vine `null` (confirmă: nu te baza pe `prestatii` din listă — le ai local în `submissions`).
- **Filtrele NU funcționează pe test**: `?vin=`, `?search=`, `?keyword=`, `?dataPrestatie=` sunt IGNORATE
  (întorc tot setul). `?page=&size=` rup răspunsul (non-JSON). → **fetch tot setul, filtrează client-side.**
  Pe prod doc-ul promite filtrare keyword/dată + export Excel (de re-verificat pe prod).
- **RAR acceptă DUPLICATE**: live există 2 perechi de records identice pe `vin+dataPrestatie+odometruFinal`
  (id 63622≡63625, 63623≡63626). De aceea reconcilierea pe răspuns pierdut e necesară, iar matcher-ul
  alege **id-ul maxim** când există mai multe potriviri.

> Reconciliere (T2): înainte de re-send pe un rând `sending`, GET finalizate, match pe
> `vin + dataPrestatie + odometruFinal(int)`; dacă există → marchează `sent` cu id-ul găsit, NU re-trimite.

## Corecții față de planurile inițiale (context istoric)

1. **JWT „scurt" → de fapt 30 de ORE.** Planurile (`plan-design-review` §„Gestiunea credențialelor",
   `plan-eng-review` §worker) presupun JWT scurt și mută durabilitatea pe re-push din ROAAUTO
   („coada acoperă minutele, ROAAUTO acoperă orele"). **Fals.** Cu 30h, **worker-ul singur poate
   relua peste orice pană RAR realistă** în fereastra tokenului. Re-push-ul din ROAAUTO devine
   plasă de siguranță secundară, nu mecanismul principal. Re-evaluează: poate nu mai e nevoie de
   re-push în treapta 1, sau credențialele se pot reține în memorie pe durata penei.
2. **b64Image (poza odometrului) NU mai e obligatorie.** Planurile o tratau ca posibil gap de
   conformitate / posibil obligatorie. **Rezolvat: opțională.** → Open Question „sursa pozei în
   ROAAUTO" **se închide** (nu mai e blocantă). Dacă e atașată, doar trebuie base64 valid.
3. **`tipPrestatie` NU e input client** — îl generează serverul (`"GENERIC"`). Open Question #2
   (valori acceptate `tipPrestatie`) **se închide pentru request** — nu-l trimite.
4. **`sistemReparat`**: planul presupunea că e derivabil server-side din coduri și nu input liber.
   **Parțial fals** — apare în request (exemplul oficial trimite string-ul `"null"`). Rămâne open
   ce valori reale acceptă; sigur: poți trimite `"null"` când nu se aplică.
5. **Anulare / corecție prin API: NU există** pentru records-urile noastre (sunt `FINALIZATA`).
   Scoate din scope endpoint-urile gateway `PATCH /v1/prezentari/{id}/anulare` și `/corectie`
   (proxy peste `markPrezentareAnulataById` / `patchPrezentare`) — nu se aplică `FINALIZATA`.
   Maparea stărilor + „Error & rescue map" din `plan-eng-review` trebuie ajustate.
6. **Regula `needs_data` e acum deterministă:** `odometruInitial` lipsă **doar** când `prestatii`
   conține `R-ODO` sau `I-ODO`. (Planul vorbea generic de „repair odometru".)
7. **URL nomenclator confirmat** = `/nomenclator/getNomenclatorPrestatii` (nu varianta din
   operationId Swagger). Constatarea #5 din `plan-eng-review` (URL-uri din VFP, nu din spec) — confirmată.

## API gateway (ROAAUTO -> gateway): mapare operatii (hibrid, 2026-06-15)

Aceasta e suprafata **gateway-ului**, nu RAR. Un item din `prestatii` la
`POST /v1/prezentari` poate veni in doua forme (cel putin una obligatorie):

| Camp item | Note |
|---|---|
| `cod_prestatie` | cod RAR direct (ex. `OE-1`). **Validat fata de nomenclator** -> validare T3 -> coada. Cod NECUNOSCUT in nomenclator e tratat ca operatie de mapat (vezi mai jos). |
| `cod_op_service` | cod intern ROAAUTO. Gateway-ul il traduce in cod RAR prin `operations_mapping`. |
| `denumire` | denumirea operatiei ROAAUTO; folosita pentru fuzzy lookup in editor. |

Daca lipsesc **ambele** coduri -> `422` (shape). Op cu `cod_op_service` necunoscut
(fara mapare) -> submission `needs_mapping` (NU se trimite la RAR), apare in editorul
web. La salvarea maparii, submission-urile blocate pe acel cod se re-rezolva automat
(-> `queued`, sau `needs_data` daca regula odometru/continut cere asta). Codul RAR
rezolvat se scrie inapoi in `payload_json`, deci payload builder + worker raman
code-driven.

**Validare `cod_prestatie` la ingestie (2026-06-23).** RAR accepta NUMAI coduri din
nomenclator: coloana `COD_PRESTATIE` are max 5 caractere si un cod necunoscut
intoarce **HTTP 500** (`ORA-12899`) — confirmat live. Periculos: RAR NU e tranzactional
si lasa un **record partial** (`FINALIZATA`, terminal) chiar cand apelul esueaza, iar
reconcilierea worker-ului il poate marca fals `sent`. De aceea gateway-ul NU mai trimite
un `cod_prestatie` care nu e in nomenclator: il promoveaza la `cod_op_service` (cu
`denumire`=cod, pentru fuzzy) si il trateaza ca operatie de mapat.

**Optiunea `on_unmapped_error`** (camp boolean top-level optional pe `POST /v1/prezentari`
si `/v1/prezentari/valideaza`) controleaza ce se intampla la cod necunoscut/nemapat:
- `false` (default) — submission `needs_mapping`, apare in editor (non-distructiv);
- `true` — respinge fara enqueue: `SubmissionResult` cu `status="error"`,
  `submission_id=null`, `erori=[COD_NEMAPAT...]`.

Cand campul lipseste se aplica default-ul contului (`accounts.on_unmapped_error_default`,
implicit `false`/`0`). Override per-cerere > default cont > `false`.

Endpointuri noi:
- `GET /v1/mapari/pending` — operatii nemapate distincte + sugestii fuzzy (`{cod_prestatie, nume_prestatie, score}`).
- `POST /v1/mapari` `{account_id?, cod_op_service, cod_prestatie, auto_send}` — upsert mapare + re-rezolvare. Respinge `cod_prestatie` inexistent in nomenclator (422).
- Web: `GET /_fragments/mapari` (editor HTMX), `POST /mapari` (form, salveaza + re-randeaza).

### Lifecycle trimiteri blocate (PRD 5.6)

`POST /v1/prezentari` — camp **aditiv** in fiecare `SubmissionResult`: `reactivated: bool`.
La resubmit cu aceeasi cheie de continut peste un rand `error` (ex. parola RAR corectata),
randul se RE-ACTIVEAZA (re-clasificat + creds actualizate) si raspunsul poarta
`reactivated: true` + starea noua. `deduped` pastreaza semantica actuala (clientii vechi
care testeaza `deduped` nu se sparg). Pentru `sent`/`queued`/`sending`/`needs_*` ->
`deduped: true` (neschimbat).

- `DELETE /v1/prezentari/{id}` — sterge o trimitere blocata a contului cheii API.
  **200 + body JSON** `{ok, submission_id, status_anterior}` (NU 204 — clienti VFP string-parse).
  Scope evaluat INAINTEA starii: cross-account / inexistent -> **404** (acelasi mesaj, B3);
  own-account `sent`/`sending` -> **409** (conflict de stare).
- `POST /v1/prezentari/{id}/repune` — re-pune in coada (`error -> queued`, re-ruleaza classify).
  **200 + body JSON** `{ok, submission_id, status_anterior, status_nou}`. Acelasi oracol scope/stare.
- `GET /v1/prezentari/{id}` expune ACUM si `rar_error` (T9) — recovery observabil prin API
  (de ce a esuat); contine doar coduri/mesaje de validare RAR, niciodata creds.

Web (dashboard, scoped pe sesiune + CSRF): `POST /trimitere/{id}/sterge`,
`POST /trimitere/{id}/repune`, `POST /trimiteri/sterge-bulk` (selectie multipla, doar blocate).

Fuzzy: `rapidfuzz.token_sort_ratio` pe denumire normalizata (fara diacritice, upper).
Nomenclatorul se ia **live** din RAR (worker upsert la fiecare login); seed fallback
de 18 coduri la boot (`app/nomenclator_seed.py`) ca editorul sa mearga offline.
Auth API-key (CORE) inca neimplementat -> `account_id` curge ca `NULL` si e atribuit
contului default `id=1` (seed in schema); cand auth livreaza, account_id real curge natural.

## Regula de scope pe cont (B8, PRD 3.2)

Orice GET nou pe `/v1/*` care atinge `submissions` sau `operations_mapping` **PORNESTE**
cu `account_id: int = Depends(resolve_account_id)` si clauza de scope pe cont in SQL.
Varianta globala (fara scope) e exceptie justificata explicit — singurul exemplu actual
este `GET /v1/nomenclator` (cache de referinta RAR fara PII, partajat intre conturi).

Pentru `submissions` (account_id nullable): foloseste `account_scope_clause(account_id)`
din `app/mapping.py` care produce `(account_id = ? OR (account_id IS NULL AND ? = 1))`.
Randurile legacy cu `account_id IS NULL` apartin contului 1 (OV-2, back-compat).

Pentru `operations_mapping` (account_id NOT NULL): `WHERE account_id = ?` simplu.

## Open questions rămase (actualizat)

1. ~~Sursa pozei odometrului~~ — **închis** (poză opțională).
2. `sistemReparat` — ce valori reale acceptă (în afară de `"null"`). De probat la postPrezentare test.
3. Un singur user RAR per agent economic sau mai mulți (`idUser`/`idAgent` — afectează filtrarea monitorizării).
4. ~~JWT TTL~~ — **închis: 30h.** Decizie nouă: simplifică worker-ul; reconsideră necesitatea re-push ROAAUTO.
5. ~~Comportamentul răspunsului de eroare~~ — **închis** (format `data:[{field,message}]`, mesaje capturate mai sus).
6. WAF cere `User-Agent` — **închis/confirmat** (vezi secțiunea de sus).

## ~~Rămas de verificat live (postPrezentare real pe test)~~ — ✅ FĂCUT 2026-06-15

Login + nomenclator + JWT TTL + **postPrezentare** = ✅ toate verificate live.
Record de test creat: `data.id = 68514` (FINALIZATA, permanent pe test). Confirmat:
- mesajele de eroare exacte (VIN O/I/Q, dată prea veche, dată viitoare) — vezi tabelul de erori;
- forma răspunsului success pe contul nostru + `data.id`;
- `sistemReparat:"null"` acceptat, `b64Image`/`odometruInitial` omise OK;
- header `User-Agent` obligatoriu (altfel 403 WAF).

Rămas neprobat: ce alte valori `sistemReparat` (în afară de `"null"`) acceptă (Open Q #2).

## Note integrare — planuri de cont (PRD 5.17)

**Poți dezvolta și testa pe planul Gratuit** fără niciun upgrade — `POST /v1/prezentari/valideaza`
(dry-run) e permis pe orice plan, nu face enqueue și nu consumă cotă lunară. Primești același
răspuns de validare (câmpuri, cod_prestatie, rezolvare operație) ca la trimiterea reală.

**Trimiterea reală cere planul Pro** (sau trial Pro activ): rutele `POST /v1/prezentari`,
`POST /v1/import` și `POST /v1/import/{id}/commit` sunt gate-uite pe `api_access=True`
(Pro/Premium). Un cont Free/Standard primește `403 PLAN_FARA_API`. Contactează-ne pentru upgrade.

Planul Gratuit are limită de **60 prezentări/lună** (indiferent de canal). La depășire: `422 PLAN_LIMITA_LUNARA`.
Planul Pro nu are limită de volum. `GET /v1/nomenclator` rămâne public pe orice plan (exploatare pre-upgrade).

## Creds RAR durabile per-mediu (PRD 5.20 US-013)

`POST /v1/conturi/rar-creds` — setează credențialele RAR durabile per-cont, cu scriere în slotul per-mediu.

**Body JSON:**
```json
{
  "email": "user@firma.ro",
  "password": "parola",
  "rar_target": "test"
}
```

`rar_target`: `"test"` | `"prod"` | absent. Absent → ancora globală (`AUTOPASS_RAR_ENV`), implicit `"test"`.
Schimbare **aditivă** (back-compat): clienții care nu trimit `rar_target` continuă să funcționeze.

Efecte: scrie în `accounts.rar_creds_{env}_enc` + activează `rar_{env}_enabled=1`.

**Răspuns:** `{"ok": true, "account_id": N, "rar_env": "test"}`

---

`DELETE /v1/conturi/rar-creds` — șterge credențialele RAR durabile per-cont.

**Query param** `env`: `test` | `prod` | absent.
- `?env=test` → NULL doar slotul Testare + `rar_test_enabled=0`.
- `?env=prod` → NULL doar slotul Producție + `rar_prod_enabled=0`.
- Absent → NULL ambele sloturi (revenire completă la modelul efemer Treapta 1).

Schimbare **aditivă** (back-compat).

---

`GET /v1/ping` — răspuns include acum și câmpurile per-mediu:
```json
{
  "are_creds_rar": true,
  "are_creds_test": true,
  "are_creds_prod": false
}
```
`are_creds_rar` rămâne (back-compat) = OR logic al celor două.
</content>
</invoke>
