Files
rar-autopass/docs/api-rar-contract.md
Claude Agent 748ab8b289 feat(api): scope pe cont la GET-urile de listare /v1/* (PRD 3.2)
Inchide scurgerea cross-account pe GET /v1/prezentari(/{id}),
/v1/mapari(/pending) si /v1/audit/export. Toate primesc
Depends(resolve_account_id) + account_scope_clause (regula NULL->cont 1,
OV-2). Nomenclatorul ramane global (referinta partajata, fara PII).

- B3: 404 cross-account byte-identic cu 404 inexistent (fara oracol enumerare)
- B4: get_prezentare cu allowlist de campuri (nu mai expune rar_creds_enc/
  payload_json/idempotency_key/rar_error)
- B1: pending_unmapped filtreaza in SQL; default None = global doar pentru web
- B2: helper account_scope_clause (DRY, doar pe submissions nullable)
- B5: index idx_submissions_account_status
- B8: regula de scope documentata in api-rar-contract.md
- TD-3.2: ?account_id != contul cheii -> 400

14 teste noi (cross-account, legacy NULL, B3, B4); suita 313 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:50 +00:00

17 KiB

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).

{
  "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):

{
  "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:

{
  "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.

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):

{
  "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). Trece neatins -> validare T3 -> coada.
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.

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).

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).