diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index d88907c..508c7d8 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -1,7 +1,10 @@ # Context proiect — Gateway RAR AutoPass (migrare ROAAUTO din VFP în Web API) > Fișier de continuitate între sesiuni. Citește-l înainte de a relua lucrul. -> Ultima actualizare: 2026-06-14. +> Ultima actualizare: 2026-06-15. +> +> ⚠️ **SURSA DE ADEVĂR pentru contractul RAR = `docs/api-rar-contract.md`** (verificat live). +> Acolo unde planurile (`docs/plans/*`) diferă, contractul are dreptate. Vezi „Corecții față de planuri". ## Reluare pe alt calculator (portabil) @@ -46,18 +49,20 @@ versiunea web. Nu se mai dezvoltă; se portează. | `rar_advanced.prg` | export Excel (oglindă pentru treapta 2) | referință import xlsx/csv | | `mapare_prestatii.DBF` | cod_op_service → codPrestatie | `operations_mapping` (via `tools/import_dbf.py`) | | `prestatii_rar.DBF` | nomenclator {codPrestatie, numePrestatie} | `nomenclator_rar` (via `tools/import_dbf.py`) | -| `Documentatie Serviciu AutoPass_Final.txt`, `Document informativ RAR- Autopass.txt` | spec oficial RAR | contract API | +| `Documentatie Serviciu AutoPass_Final.txt`, `Document informativ RAR- Autopass.txt` | spec oficial RAR (vechi, are typo-uri) | înlocuit de `docs/api-rar-contract.md` | +| `docs/api-rar-documentatie-oficiala.md` | răspuns oficial programatori RAR | sintetizat în `docs/api-rar-contract.md` | +| `docs/api-rar-contract.md` | **contract verificat live — sursa de adevăr** | referință pentru `app/` | -## Planurile (în `docs/plans/`) +## Planul (în `docs/plans/`) -1. **`plan-design-review.md`** — designul produsului/arhitecturii (output `/office-hours`). - Problemă ISV, topologie gateway central pass-through credențiale, zero stocare parole, - privacy-first, teză SaaS pe trepte. -2. **`plan-eng-review.md`** — planul de implementare (review CEO, SELECTIVE EXPANSION peste design). - Decizii blocate, constatări din spec, mașina de stări submission, securitate, verificare E2E. +**`plan.md`** — **planul unic executabil** (sursă unică). Consolidează designul de produs + +implementarea, aliniat la contractul verificat live. Conține: arhitectură, reguli contract, +validare, mașina de stări, componente, securitate, failure modes, **Roadmap de execuție (T1-T7 + +pașii rămași)**, verificare E2E, NOT in scope, decizii blocate, și anexa de produs/SaaS. -> Continuă cu **plan-eng-review** și **plan-design-review** — acestea sunt cele două -> documente de reluat în următoarea sesiune. +> Continuă cu **`docs/plans/plan.md`** → secțiunea „Roadmap de execuție". Pasul blocant următor = **T1** +> (un `postPrezentare` real pe test). Fostele `plan-eng-review.md` + `plan-design-review.md` au fost +> consolidate în `plan.md` (review-urile eng + design au intervenit pe el). ## Arhitectura țintă (rezumat) @@ -73,18 +78,20 @@ ROAAUTO (VFP, client subțire) ──HTTPS──▶ Gateway FastAPI (central, 1 Stack: Python/FastAPI + SQLite (WAL) + httpx. Deploy: LXC Proxmox + Cloudflare Tunnel (start) → VPS (~5€/lună). Open-source pe github.com/romfast, AGPL-3.0 (⚠️ decide CLA din ziua 1 dacă vrei dual-license). -## ⚠️ Următorul pas BLOCANT — „The Assignment" (spike, ~1h, ÎNAINTE de cod) +## „The Assignment" (spike) — REZOLVAT în mare parte (2026-06-15) -Pe endpoint-ul de **test RAR**, măsoară: -1. **Durata de viață a JWT-ului** (`/public/login` → `postPrezentare` la intervale crescătoare până la 401) - → dimensionează fereastra de retry autonom din worker. -2. **Dacă `postPrezentare` trece fără `b64Image`** (poză odometru) și fără `odometruInitial` - → decide dacă poza e obligatorie în prod și dacă ROAAUTO trebuie s-o atașeze. -3. **Valorile acceptate pentru `tipPrestatie` / `sistemReparat`** (enum nedocumentat). +Detalii complete în `docs/api-rar-contract.md`. Rezumat: +1. **JWT TTL = 108000s = 30 ORE** (nu „scurt"). → worker-ul singur poate relua peste pene lungi; + re-push ROAAUTO devine secundar. Reconsideră arhitectura de robustețe din planuri. +2. **`b64Image` (poza) = OPȚIONALĂ** (confirmat oficial). Open question „sursa pozei" închisă. +3. **`tipPrestatie` = generat de server** (`GENERIC`), nu se trimite. `sistemReparat` se trimite + (poate fi `"null"`); valorile reale rămân de probat. +4. **`needs_data` determinist:** `odometruInitial` obligatoriu doar dacă `prestatii` conține + `R-ODO` sau `I-ODO`. -Rezultatul decide robustețea cozii și scopul real al ROAAUTO. Nu porni worker-ul înainte. +Rămas: **un singur `postPrezentare` real pe test** (mesaje de eroare exacte + `data.id`). Vezi contract. -## De făcut după spike (din plan-eng-review, secțiunea Verificare) +## De făcut după spike (din `plan.md`, Roadmap de execuție + Verificare) 1. `tools/import_dbf.py --dry-run` pe `mapare_prestatii.DBF` + `prestatii_rar.DBF` (raport întâi, apoi import). 2. Schelet repo: `app/api/v1`, `app/rar_client.py`, `app/worker`, `app/web`, SQLite (WAL), `docker compose up`, `/healthz` verde. @@ -102,9 +109,12 @@ Rezultatul decide robustețea cozii și scopul real al ROAAUTO. Nu porni worker- - Cherry-picks în v1: alertă submission-uri blocate, `/healthz`+`/metrics`, sugestie fuzzy mapare, export audit CSV. - URL-urile RAR: **sursa de adevăr = VFP testat**, NU spec-ul (are typo-uri de copy/paste). -## Open questions rămase +## Open questions rămase (actualizat 2026-06-15) -1. Sursa pozei odometrului în fluxul ROAAUTO (dacă spike confirmă `b64Image` obligatoriu). -2. `tipPrestatie` — valori acceptate (de probat la spike). -3. Un singur user RAR per agent economic sau mai mulți (afectează `idUser` / filtrare monitorizare). +1. ~~Sursa pozei odometrului~~ — **închis** (poză opțională, vezi contract). +2. ~~`tipPrestatie` valori~~ — **închis pentru request** (generat de server). Rămâne: ce valori + reale acceptă `sistemReparat` (în afară de `"null"`). +3. Un singur user RAR per agent economic sau mai mulți (afectează `idUser`/`idAgent` / filtrare monitorizare). 4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client. +5. Anulare/corecție: **nu există flux API** (records `FINALIZATA`); corecția = email suport RAR. De + reflectat în UX dashboard (nu promite anulare). diff --git a/Document informativ RAR- Autopass.txt b/docs/Document informativ RAR- Autopass.txt similarity index 100% rename from Document informativ RAR- Autopass.txt rename to docs/Document informativ RAR- Autopass.txt diff --git a/Documentatie Serviciu AutoPass_Final.txt b/docs/Documentatie Serviciu AutoPass_Final.txt similarity index 100% rename from Documentatie Serviciu AutoPass_Final.txt rename to docs/Documentatie Serviciu AutoPass_Final.txt diff --git a/docs/api-rar-contract.md b/docs/api-rar-contract.md new file mode 100644 index 0000000..c84b70c --- /dev/null +++ b/docs/api-rar-contract.md @@ -0,0 +1,214 @@ +# Contract RAR AUTOPASS — sursa de adevăr (verificat live) + +> **Acesta este documentul autoritativ pentru contractul API RAR AUTOPASS.** +> Înlocuiește presupunerile din `docs/plans/*` 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. + +## 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: , 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) + +```json +{ + "statusCode": 200, + "message": "Prezentare adaugata cu succes", + "data": { + "id": 59950, + "dataPrestatie": "2024-07-25", + "vin": "...", + "odometruFinal": 9999999, + "idAgent": 1587, + "tipPrestatie": "GENERIC", + "odometruInitial": null, + "idUser": 22, + "sistemReparat": "null", + "obs": "TEST", + "nrInmatriculare": "B999GEN", + "status": "FINALIZATA", + "prestatii": [ { "idPrezentare": 599950, "codPrestatie": "OE-1" }, ... ], + "b64Image": "..." + } +} +``` + +- `data.id` = ID-ul prezentării la RAR (de reținut ca `idPrezentare` în submission). +- `tipPrestatie` = `"GENERIC"` — **generat de server**, nu input client. +- Validările eșuate sunt raportate **în răspunsul API** (mesaj de eroare) când nu sunt respectate constrângerile. + +## 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) + +- Pe **mediul de test**: la interogarea listei de prezentări finalizate **NU primești și `prestatii`** în răspuns. +- Pe **producție**: prestațiile sunt disponibile; lista poate fi filtrată după keyword / interval + de date și exportată în Excel. +- Implicație dashboard: nu te baza pe `prestatii` din listă pe test; le ai în `submissions` local. + +## Corecții față de `docs/plans/*` (citește înainte de a refolosi planurile) + +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ă. + +## 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 exact al răspunsului de eroare la fiecare constrângere (de capturat la primul postPrezentare test). + +## Rămas de verificat live (postPrezentare real pe test) + +Login + nomenclator + JWT TTL = ✅ făcute. Mai rămâne **un singur POST real pe test** ca să confirmi: +- mesajele de eroare exacte pentru fiecare constrângere (VIN cu O/I/Q, dată în afara intervalului etc.); +- valoarea `data.id` întoarsă și forma exactă a răspunsului pe contul nostru; +- acceptarea `sistemReparat: "null"` și omiterea `b64Image`/`odometruInitial`. +(Creează un record pe mediul de test — nu pe producție.) + + diff --git a/docs/api-rar-documentatie-oficiala.md b/docs/api-rar-documentatie-oficiala.md new file mode 100644 index 0000000..dd515b2 --- /dev/null +++ b/docs/api-rar-documentatie-oficiala.md @@ -0,0 +1,189 @@ +Aveti mai jos linkul de Swagger ce descrie fiecare endpoint al mediului de TESTARE. + + +https://apps.rarom.ro/test-rar-autopass/swagger-ui/index.html + + + +Acest Swagger nu permite autentificarea pentru testare directa, insa aici gasiti API si payload/parametri, modele de date ce pot fi testate prin tool`uri specifice (PostMan) cu credentialele pe care vi le`am creat: + + +Am atasat documentatia si un screenshot ca exemplu de login. + + +Ca sa poata fi integrate in PRODUCTIE, API se modifica astfel : + + +https://apps.rarom.ro/rar-autopass/swagger-ui/index.html folosind credentialele societatii/reprezentantei societatii. + + + +Inainte de a trece in productie am rugamintea sa reveniti pentru a verifica corectitudinea datelor din mediul de test. + + + +Imaginea odometrului nu mai este obligatorie. + +Prezentarile prin API se vor trimite cu status “FINALIZATA”. + +- este necesar intai login https://apps.rarom.ro/test-rar-autopass/public/login apoi folosirea Bearer token in apelul pentru POST Prezentare + + +- pentru POST Prezentare modelul este urmatorul, spre exemplu, restul de date fiind asociate/generate de server: + +{ + + "b64Image":"UklGRn5FAgBXRUJQVlA4WAoAAAAoAAAA/wMA/wMASUNDUAobAAAAABsKbGNtc.... etc stringul base 64", --OPTIONAL + + "dataPrestatie": "2024-07-25", + + "nrInmatriculare": "B999GEN", + + "obs": "TEST", --OPTIONAL + + "odometruFinal": "9999999", + + "odometruInitial": null, - se completeaza doar daca se schimba/repara instrumentele care afiseaza nr km + + "prestatii": [ + + { + + "codPrestatie": "OE-1", + + "idPrezentare": null + + }, + + { + + "codPrestatie": "OE-2", + + "idPrezentare": null + + }, + + + + { + + Se adauga prestatii in continuare daca este cazul + + } + + ], + + "sistemReparat": "null", + + "status": "FINALIZATA", + + "vin": "XXXXXXXX888888888" + +} + + Si vom primi ca raspuns: + +{ + + "statusCode": 200, + + "message": "Prezentare adaugata cu succes", + + "data": { + + "id": 59950, + + "dataPrestatie": "2024-07-25", + + "vin": "XXXXXXXX888888888", + + "odometruFinal": 9999999, + + "idAgent": 1587, + + "tipPrestatie": "GENERIC", + + "odometruInitial": null, + + "idUser": 22, + + "sistemReparat": "null", + + "obs": "TEST", + + "nrInmatriculare": "B999GEN", + + "status": "FINALIZATA", + + "prestatii": [ + + { + + "idPrezentare": 599950, + + "codPrestatie": "OE-1" + + }, + + { + + "idPrezentare": 599950, + + "codPrestatie": "OE-2" + + } + + ], + + "b64Image":"UklGRn5FAgBXRUJQVlA4WAoAAAAoAAAA/wMA/wMASUNDUAobAAAAABsKbGNtc.... etc stringul base 64" + +} + + + +- Daca se intervine asupra instrumentelor care afiseaza kilometrajul se va introduce odometru initial(indicatia inainte de reparatie/schimbat) si odometru final (indicatia dupa reparatie/ schimbat) + + + +- Codurile de prestatie pot fi interogate prin API pentru a le putea asocia dvs cu operatiunile ce se executa de catre societate. + +https://apps.rarom.ro/test-rar-autopass/swagger-ui/index.html#/nomenclator-controller/getPrestatiiNomUsingGET + + + + +Testele pentru integrare se vor face NUMAI pe mediul de test: + +https://apps.rarom.ro/test-rar-autopass/swagger-ui/index.html + +----------------------------------------- +In urma constatarii unor date introduse eronat, API RAR Auto-Pass (atat mediul de test cat si cel de productie) a suferit unele modificari legate de constrangerile campurilor transmise, unele fiind deja existente, iar altele adaptate, dupa cum urmeaza: + +Data – nu poate fi mai devreme de 01.12.2024 sau mai tarziu de “astazi” + +VIN – 17 caractere, majuscule, fara spatii, caractere speciale sau literele O,I,Q + +Imagine odometru, daca este atasata, trebuie sa fie format base64. + +Daca in lista de prestatii se regaseste R-ODO SAU I-ODO, campul odometru initial trebuie sa fie deasemenea transmis. + +Numarul de inmatriculare – fara spatii sau caractere speciale, max 10 caractere, litere + cifre, majuscule. + + +Acestea sunt deja aplicate si detalii se regasesc in raspunsul API, in cazul in care nu sunt respectate. + +---------------------------- +Doar prezentarile cu status SALVATA pot fi anulate, nu si cele cu status FINALIZATA. Raspunsul este : EROARE_STATUS_ANULARE("Eroare la schimbarea statusului prezentarii. Id not found."), de acolo confuzia. + + + +Anularea Prezentarilor se poate face din interfata web atat timp cat acestea sunt inca editabile(SALVATA). Odata marcate ca FINALIZATA , prezentarile nu se mai pot anula. + + +Scopul API este strict de INSERT prezentari ca si FINALIZATE. Toate celelalte operatiuni CRUD se vor face din web unde este cazul. + + +Daca s-au introdus date eronate acestea pot fi rectificate de catre Registrul Auto, in urma unei solicitari la suport.autopass@rarom.ro , dar acum nu este cazul fiindca sunteti pe mediul de test. + +Inca un aspect de mentionat pe mediul de test , la interogarea listei de prezentari finalizate nu veti primi in raspuns si prestatiile, insa pe mediul de productie acestea sunt disponibile pentru vizualizare, pot fi filtrate by keyword sau date range, si apoi pot fi exportate in Excel. +-------------------------------- diff --git a/docs/plans/plan-design-review.md b/docs/plans/plan-design-review.md deleted file mode 100644 index fa71a58..0000000 --- a/docs/plans/plan-design-review.md +++ /dev/null @@ -1,271 +0,0 @@ -# Design: Gateway RAR AUTOPASS (migrare ROAAUTO din VFP în web) - -Generat de /office-hours pe 2026-06-14 -Mod: Startup (proiect intern / intrapreneurship ISV) -Status: DRAFT - -## Problem Statement - -ROA (ERP) are nevoie să declare la **RAR AUTOPASS** prestațiile de service ale clienților săi -(service-uri auto care rulează **ROAAUTO**, Visual FoxPro + Oracle), conform **Legii 142/2023** și -**OM 210/2024**. Integrarea există azi în VFP (clasa `RarAutoPass`), dar **nu e încă pusă la clienți** — -doar testată pe endpoint-ul de test RAR. - -Problema reală a lui Mihai NU e a unui service, ci a unui **ISV**: nu vrea să redistribuie un `.exe` VFP -la fiecare client la fiecare corecție. Vrea **logica pe un server central, depanabilă central**, cu ROAAUTO -ca simplu client subțire care trimite comenzile. - -## Demand Evidence (validat: client real + lege) - -**Cel mai tare semnal:** un **client real a cerut automatizarea** introducerii prezentărilor în AUTOPASS — de -aici a pornit tot proiectul. Primul plătitor a cerut-o, nu e ipoteză. **Status quo înlocuit:** interfața web -oficială AUTOPASS, unde service-urile introduc **manual, prezentare cu prezentare**, operația principală — tedios. - -Obligație legală reală, nu ipotetică — **Legea 142/2023** (registrul electronic al istoricului vehiculelor) -obligă operatorii economici autorizați să transmită la RAR: -- la fiecare prestație: **VIN + indicația odometrului**; -- repararea/înlocuirea odometrului; -- operațiunile principale de reparație la **direcție, frânare, structura caroseriei/șasiului** și alte - sisteme de siguranță. -- Amenzi: informații eronate de la service 1.000–2.000 lei; manipulare odometru până la 5.000 lei / penal. - -Implică: **toți** clienții ROA + **mii de service-uri non-ROA** au aceeași obligație → există piață dincolo -de ROA pentru un canal de import (xlsx/csv) ulterior. - -## Status Quo - -Integrare VFP funcțională dar nedistribuită: `RarAutoPass` (`rar_autopass.prg`) vorbește direct cu RAR prin -`MSXML2.ServerXMLHTTP`; mapare în DBF (`mapare_prestatii`), nomenclator în `prestatii_rar`, jurnal în -`rar_log`; UI desktop cu tab-uri. Credențiale RAR în `settings.xml` pe fiecare stație (text clar). - -## Constraints - -- Stack: **Python / FastAPI** (ales). Hosting: **hibrid** — instanță centrală always-on operată de ROA + - Proxmox LXC pentru dev/staging. `romfast.ro`/hosting.com (doar PHP) nu găzduiește core-ul. -- Open-source pe **github.com/romfast** (licență recomandată **AGPL-3.0**). -- ROAAUTO rămâne client subțire (refolosim pattern-ul `MSXML2.ServerXMLHTTP`). - -## Premises (confirmate) - -1. Migrarea în web se justifică prin nevoia ISV: deploy central, fără redistribuire de exe-uri. ✅ -2. Cererea e reală și legală (L.142/2023) — clienți ROA + service-uri non-ROA. ✅ -3. Maparea operație→`codPrestatie` e în **core** (API-ul cere `prestatii` în `postPrezentare`). ✅ (corectat de utilizator pe baza spec-ului) -4. ROAAUTO = client subțire; mapare + retry + jurnal pe server. ✅ -5. **Topologie: gateway central, pass-through credențiale, ZERO stocare de parole.** ✅ - -## Contract API RAR AUTOPASS (din spec oficial v0.0.1, baza `/rar-autopass`) - -- Auth: `POST /public/login` {email, password} → `GetUtilizatoriDTO{ token, idUser, ... }`. - Token JWT atașat ca `Authorization: Bearer {token}` la toate apelurile securizate. -- Nomenclator: `GET /nomenclator/getNomenclatorPrestatii` → listă `{codPrestatie, numePrestatie}`; - `GET /nomenclator/getPrestatieByCodPrestatie/{cod}`. -- Prezentări: `POST /prezentari/postPrezentare` (payload `Prezentari`); `GET /prezentari/getAllPrezentari`; - `GET /prezentari/getPrezentare/{id}`; `PATCH /prezentari/markPrezentareAnulataById/{id}`; - `PATCH /prezentari/patchPrezentare/{id}`. Răspuns: `CustomResponseForMapping{ data, message, statusCode }`. -- Payload `Prezentari`: `vin`, `nrInmatriculare`, `dataPrestatie(date)`, `odometruInitial`, `odometruFinal`, - `obs`, `b64Image`, `sistemReparat`, `tipPrestatie`, `status`∈{SALVATA,FINALIZATA,ANULATA,UNDEFINED}, - `prestatii: [{codPrestatie, idPrezentare}]`. -- Cont/roluri (per agent economic): ADMIN creează CLIENT/ADMIN_CLIENT. **Nu replicăm** asta în gateway. - -## Recommended Approach — B: gateway central + coadă + token JWT scurt - -### Flux - -``` -ROAAUTO (VFP) ──POST /v1/prezentari {comanda + RAR creds + idempotency_key}──▶ Gateway FastAPI - (citește creds din Oracle clientului) │ - ├─ rezolvă maparea op→codPrestatie - ├─ INSERT submission (PII TRANZITORIU, status=queued) - ◀── răspuns imediat: {submissionId, status:queued|needs_mapping} ───────────┘ (dedup pe idempotency_key) - Worker (daemon/task fundal, poll SQLite) ── login RAR → JWT ──▶ postPrezentare ──▶ RAR - │ retry cu backoff în fereastra JWT - └─ la succes: PURJEAZĂ PII, reține doar hash+status+idPrezentare -Browser ──▶ Dashboard ── monitorizare CITITĂ LIVE din RAR (getAllPrezentari), nu din PII local ── + mapări, nomenclator -``` - -### Gestiunea credențialelor (cheia deciziei #5) - -- ROAAUTO trimite `email`+`password` RAR la fiecare apel (din Oracle-ul clientului, peste **HTTPS**). - Creds-urile trăiesc **doar în itemul de coadă** (în memorie/rândul de lucru), folosite de worker pentru `login`, - apoi **șterse**. Parola **nu se persistă** și se **scrubează** din loguri ȘI din capturile de excepție/APM. -- Worker-ul face `login` (nu API-ul) → POST-ul răspunde imediat fără latența RAR. Worker: login → JWT → postPrezentare, - retry cu backoff **în fereastra JWT-ului**. -- **Onestitate despre robustețe:** coada NU aduce reziliență la indisponibilitate RAR de durată — JWT-ul e scurt și - nu ținem parola ca să reluăm peste expirare. Ce aduce coada: răspuns asincron rapid pentru ROAAUTO + jurnal central + - retry pe erori tranzitorii scurte. **Durabilitatea reală pe pene lungi stă în ROAAUTO**, prin job-ul periodic de - **re-push** al submission-urilor rămase `error/pending` (retrimite cu creds proaspete). Coada acoperă minutele, - ROAAUTO acoperă orele. (Dacă măsurarea TTL-ului arată JWT lung, reevaluezi — vezi „The Assignment".) - -### Idempotență (critic — record legal, fără dubluri) - -`postPrezentare` NU e idempotent, iar avem două bucle de retry (worker + re-push ROAAUTO) → risc de **prezentări -duplicate la RAR** pentru un record urmărit legal. Soluție: -- ROAAUTO trimite un `idempotency_key` = hash(cont + VIN + dataPrestatie + set(codPrestatie)). -- Gateway: `UNIQUE(idempotency_key)` pe `submissions`. Re-trimiterea aceleiași chei NU creează submission nou. -- Worker, înainte de a retrimite: dacă submission-ul are deja `idPrezentare` (răspuns RAR) → marchează `sent`, nu reapelează. - -### Mașina de stări a unui submission - -`queued → sending → { sent | needs_mapping | error }` -- `needs_mapping`: operație fără `codPrestatie` mapat → **se ține gateway-side, NU se trimite incomplet** (API-ul cere - `prestatii`); după ce mapezi, trece în `queued`. (≠ VFP-ul de azi care o arunca silențios.) -- `error`: eligibil pentru re-push din ROAAUTO (`GET /v1/prezentari?status=error`). -- `sent`: are `idPrezentare` de la RAR; terminal. - -### Privacy-first / stateless (mentenanță & răspundere minime) - -Decizie: gateway-ul e **pur tranzit + interfață cu RAR**, NU depozit de date. -- PII-ul prezentării (VIN, km, date) trăiește în SQLite **doar tranzitoriu** cât e în coadă; **la `sent` se purjează**, - rămân doar `idempotency_key` (hash ireversibil) + status + `idPrezentare`. Nu se poate reconstrui VIN-ul din hash. -- **Monitorizarea se citește LIVE din RAR** (`getAllPrezentari`/`getPrezentare`) — RAR e sursa de adevăr, nu un jurnal local. -- Durabilitatea pe pene lungi stă **la margine** (Oracle ROAAUTO / fișierul încărcat), nu la tine → re-push din client. -- **Fără agregare de date.** Datele service-urilor NU se folosesc pentru alte produse. (Eventual, în viitor, doar - produs separat cu **opt-in explicit + anonimizare**, lawyered — niciodată default.) Privacy = argument de adopție, nu doar conformitate. - -### Componente (un repo, `docker compose up`) - -1. **API (`app/api/v1`)** — FastAPI: - - `POST /v1/prezentari` (una/mai multe) → validare Pydantic, enqueue, răspuns cu `submissionId`. - - `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică pentru ROAAUTO + re-push. - - `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`. - - `GET/PUT /v1/mapari` — CRUD mapare per cont. - - `PATCH /v1/prezentari/{id}/anulare`, `/corectie` — proxy peste markPrezentareAnulataById / patchPrezentare. - - Auth gateway: **API key per cont ROA** (separată de credențialele RAR ale clientului); cu emitere/rotire/revocare. - - *(Amânat, NU în v1: `POST /v1/import` xlsx/csv — strat 2 / piață non-ROA.)* -2. **Client RAR (`app/rar_client.py`)** — portare din `rar_autopass.prg`: login+JWT, getNomenclatorPrestatii, - postPrezentare, getAllPrezentari, getPrezentare, markPrezentareAnulataById, patchPrezentare. `httpx` + retry/backoff. -3. **Worker (daemon / task de fundal) + coadă pe SQLite (`app/worker`)** — proces pornit non-stop (sau task - `asyncio` în aplicația FastAPI), sub Docker `restart: always`. Buclă: ia rândurile `status='queued'`, - revendică atomic (`BEGIN IMMEDIATE; UPDATE … SET status='sending' WHERE id=? AND status='queued'`), - login RAR, trimite, retry cu backoff, scrie status + `idPrezentare`. Reacție **instant**, fără întârziere. - *Fără Redis, fără arq, fără Postgres.* ROAAUTO oferă durabilitatea pe pene lungi (re-push). - Atenție `b64Image`: poate fi mare → stocat ca BLOB sau path pe disc, nu în RAM. -4. **Dashboard (`app/web`)** — **Jinja2 + HTMX** (server-rendered, zero build): Monitorizare **citită live din RAR** - (`getAllPrezentari`) + starea cozii curente (din `submissions`), Editor mapări, Browser nomenclator. API-first. -5. **SQLite** (mod WAL) — înlocuiește DBF, un singur fișier `.db`: - - `accounts`, `api_keys` (conturi ROA + chei gateway). - - `operations_mapping` (cod_op_service → codPrestatie, `auto_send`=trimite automat dacă e mapat) ← `mapare_prestatii`. - - `nomenclator_rar` (cache {codPrestatie, numePrestatie}) ← `prestatii_rar`. - - `submissions` (coadă + dedup): `idempotency_key` UNIQUE, status, statusCode RAR, eroare, `idPrezentare`, retry, timestamps. - Câmpurile PII (vin, km, dataPrestatie, prestatii) sunt **tranzitorii** — populate cât e `queued/sending`, - **purjate la `sent`**. (≠ `rar_log` care era jurnal permanent.) - - **Notă: niciun câmp pentru parole RAR; niciun PII reținut după trimitere.** (`import_jobs` — doar la xlsx/csv, amânat.) - -### Client ROAAUTO (VFP) — refactor minim - -- `settings.xml` păstrează doar **URL gateway + API key** (nu mai ține mapări/nomenclator). -- Credențialele RAR ale clientului se citesc din **Oracle (ROA)** și se trimit în payload la gateway. -- `export_comenzi.prg` rămâne (citește `comenzi_service`/`operatii`), dar construiește JSON și face - `POST /v1/prezentari` în loc de XML + apel direct RAR. -- Dispar din VFP: `Login`, `UpdateNomenclator`, `GetCodRarPentruOperatie`, maparea, `rar_log` → trec în web. -- Se adaugă un job periodic „re-push pending" (timer existent din `rar-forms.prg` se poate reutiliza). - -## Approaches Considered - -- **A — sincron, fără coadă** (S, risc mic): ships rapid, dar fără retry autonom. Bun ca prim pas, respins ca țintă. -- **B — coadă + token JWT scurt** (M, recomandat ✅): robust la indisponibilitate RAR, nu pierzi prestații obligatorii legal. -- **C — outbox în Oracle** (M/L): cel mai decuplat, dar cere acces gateway→Oracle client (VPN/rețea). Reținut ca opțiune pentru clienți non-ROA / viitor. - -## Open Questions - -1. Durata reală a JWT-ului RAR (decide fereastra de retry autonom). De măsurat pe endpoint-ul de test. -2. `sistemReparat` / `tipPrestatie` — valori acceptate (enum nedocumentat în spec). De clarificat cu RAR. -3. Modelul de cont RAR per client: un singur user RAR per agent economic sau mai mulți (afectează cum mapezi `idUser`). -4. Monetizare/direcție (nedecisă): vezi mai jos — de reluat după ce A→B merge la primul client. - -## Arhitectura în mare (modelul mental) - -Azi VFP face 3 treburi pe FIECARE PC client: (a) ia comanda, (b) mapează + se loghează la RAR, (c) ține jurnalul → -de-aia trebuie redistribuit la fiecare corecție. Migrarea = muți (b)+(c) pe un server central al tău: -- **ROAAUTO (VFP, la client) = expeditorul** — citește comanda + creds RAR din Oracle și le trimite la gateway. Atât. -- **Gateway (Python, central) = creierul** — primește, mapează, login RAR, trimite, retry, jurnal. Aici faci corecțiile o dată, pentru toți. -- **Dashboard web = panoul de control** — vezi ce s-a trimis/eșuat, editezi maparea. - -## Opțiuni de deploy (unde rulează gateway-ul) - -| Opțiune | Cum | Cost | Când | -|---|---|---|---| -| **LXC Proxmox + Cloudflare Tunnel** | container la birou, expus public HTTPS fără IP static / porturi deschise | 0 € | **Start + teste** (risc: netul/curentul biroului) | -| **VPS mic always-on** (ex. Hetzner) | același container, mașină care nu cade | ~5 €/lună | **Clienți reali / producție** (recomandat) | -| romfast.ro / hosting.com | Python via cPanel/Passenger (WSGI) | inclus | ⚠️ FastAPI e ASGI + worker-ul e daemon → shared hosting nepotrivit (shim fragil, fără procese persistente). Doar landing | - -Recomandare: start pe **LXC + Cloudflare Tunnel** (cost 0), mutare pe **VPS** la clienți. Mutarea = copiezi containerul + fișierul `.db`. -romfast.ro rămâne doar pentru landing/prezentare, nu țintă de producție (ASGI + worker daemon nu merg pe shared hosting). - -- **Cod:** open-source pe github.com/romfast, **AGPL-3.0**. Deploy = **un container** cu FastAPI (uvicorn) + worker-ul ca task de fundal/al doilea proces, sub Docker `restart: always` + un volum SQLite. -- **Dev/staging:** LXC Proxmox. **Migrare date:** `tools/import_dbf.py` (`mapare_prestatii.DBF` + `prestatii_rar.DBF` cu `dbfread`). - -## Teza de produs & direcție SaaS - -**Teza:** *cel mai ușor mod de a băga operațiile de service în AUTOPASS* — din ROAAUTO (API), din alte aplicații, -sau din fișiere — în loc de tastarea manuală din interfața oficială. Câștigi prin **efort minim cerut service-ului**, -nu prin features. (Funnel-ul „read public gratuit" — RESPINS: nimeni nu vrea să *citească* nomenclatorul; valoarea -e 100% în **trimitere**.) - -**Lecția GTM de la demoanaf.ro:** a devenit viral nu prin model plătit, ci oferind o variantă **reimaginată, simplă, -plăcută** a unui serviciu oficial greoi/instabil; s-a răspândit în grupurile de Facebook. Echivalentul tău: o cale -**dramatic mai simplă decât tastarea manuală AUTOPASS**, construită rapid cu AI dar pe arhitectură solidă. - -**Wedge validat:** automatizezi tastarea manuală AUTOPASS, **începând cu clientul care a cerut-o** (cale ROAAUTO/API). - -**Trepte (același motor — mapare + coadă + trimitere + monitorizare — fără rescriere):** -- **Treapta 1 (acum):** core-ul pentru clientul care a cerut + clienții ROA, prin ROAAUTO. Îți rezolvi nevoia. -- **Treapta 2 (non-ROA, web upload):** import xlsx/csv cu **mapare reținută** (vezi mai jos) + dashboard. Login web, - fără instalare. **Primul venit** — freemium **pe volum** (gratis sub N prezentări/lună pt. service mici, plată peste; - metrica de preț = prezentări/lună = fix unitatea obligatorie legal). -- **Treapta 3 (diferențiere):** integrări mai adânci + sugestii AI de mapare (eventual conector MCP). - -**Moat:** (1) mapările reținute per service cresc costul de plecare; (2) lățimea integrării (mergi indiferent ce -software are service-ul); (3) fiabilitatea de conformitate (retry, monitorizare din RAR — nu pierzi o declarație legală); -(4) **privacy** (nu reținem datele lor) — el însuși argument de adopție. Saltul fără rescriere: API key + mapare per cont, -**zero parole stocate, zero PII reținut**. - -### Adopție în masă & praguri (calibrat pe cifre reale) - -Declanșatorul trecerii de la tastarea manuală în RAR la upload = **timp salvat / efort de trecere**: -~2-4 min/prezentare manual × volum lunar. Clienți reali cunoscuți: **60-80** și **80-100 prezentări/lună** → -**3-6 ore/lună** de tastare = durerea care îi convertește. -- Prima folosire trebuie trivială (upload Excel existent → mapare reținută → trimite, sub 5 min). -- Gratis la volumul lor = fără decizie de achiziție, doar încearcă. -- **Freemium pe volum:** gratis **~30-40 prezentări/lună** (service mici = bază virală, cost ~0 la tine); - plată peste prag (banda 1 ≈ 50-150/lună prinde fix clienții actuali — primii bani de la cine a cerut serviciul). - Metrica de preț = prezentări/lună = unitatea obligatorie legal. -- Ironie de reținut: service-urile mici au cea mai mică durere (greu de convins, dar virale); volumele mari au cea - mai mare durere (cei mai dispuși să plătească). Gratis = achiziție; venit = volume mari. - -## Import xlsx/csv — UX (stratul SaaS, treapta 2) - -Două straturi de mapare, **ambele reținute per cont** (cheia produsului — „map once, reuse forever"): -1. **Mapare coloane** (schema fișierului → câmpuri canonice): ex. „«Serie șasiu»→VIN, «Index km»→odometruFinal". - Reluată automat dacă headerele se repetă. -2. **Mapare operații** (etichetele/codurile service-ului → `codPrestatie` AUTOPASS), cu **sugestie fuzzy** pe denumire. - -Flux: upload → recunoaște coloanele (reia maparea) → propune maparea operațiilor (reținută + sugestii) → -**preview** (ce se trimite, rânduri nemapate flag-uite) → „Trimite la RAR" → monitorizare. A 2-a oară: upload → preview → trimite. - -**Spectru de integrare (același backend):** API (POST prezentări, ca ROAAUTO) → drop fișier programat (folder/SFTP/email-to-import) → upload manual în browser (zero instalare). Cine poate, integrează API; cine nu, dă fișier. - -## Success Criteria - -- O prezentare reală trimisă din ROAAUTO prin gateway apare `FINALIZATA` la RAR (test), vizibilă în dashboard. -- Paritate cu VFP: același `codPrestatie` rezultat din mapare pe aceleași comenzi. -- Reziliență: RAR indisponibil → submission `queued/error` cu retry, ROAAUTO nu se blochează; re-push recuperează. -- Securitate: niciun credențial RAR în client/`settings.xml` și niciun câmp de parolă în SQLite. -- Privacy: după `sent`, în SQLite nu rămâne PII de vehicul (doar hash+status+idPrezentare); monitorizarea vine din RAR. - -## The Assignment (următorul pas concret) - -Pe endpoint-ul de **test RAR**, măsoară **durata de viață a JWT-ului** întors de `/public/login` (fă un login, -apoi `postPrezentare` la intervale crescătoare până la 401). Numărul ăsta dimensionează fereastra de retry -autonom din worker și decide dacă ai nevoie de job-ul de re-push în ROAAUTO. E o oră de muncă și deblochează -toată decizia de robustețe din B. - -## What I noticed about how you think - -- Ai corectat insight-ul meu „wedge = doar VIN+km" cu „API-ul cere și operațiile de manopelă" — și aveai - dreptate, ai citit contractul, nu l-ai presupus. Asta e exact instinctul care face diferența: sursa, nu pitch-ul. -- Ai pus singur problema de custodie a parolelor („nu vreau să salvez parole") înainte să ți-o ridic eu — - gândești în termeni de răspundere, nu doar de „merge/nu merge". -- Vezi proiectul ca ISV, nu ca utilizator final: „nu vreau să redistribui la fiecare corecție" e fix - raționamentul care justifică web-ul. Mulți ar fi rescris fără să poată articula de ce. -``` diff --git a/docs/plans/plan-eng-review.md b/docs/plans/plan-eng-review.md deleted file mode 100644 index 1d9043e..0000000 --- a/docs/plans/plan-eng-review.md +++ /dev/null @@ -1,241 +0,0 @@ -# Plan implementare: Gateway RAR AUTOPASS (migrare ROAAUTO din VFP în web) - -Sursă: review CEO (SELECTIVE EXPANSION) peste design-ul `vreau-sa-migrez-acest-precious-mochi.md`. -Grounded pe codul VFP existent + spec-ul oficial RAR (`Documentatie Serviciu AutoPass_Final.txt`, -`Document informativ RAR- Autopass.txt`). - -## Context - -ROAAUTO (Visual FoxPro + Oracle, la fiecare service client) declară azi prestațiile la RAR AUTOPASS -direct din clasa `RarAutoPass` (`rar_autopass.prg`), prin `MSXML2.ServerXMLHTTP`. Obligație legală -(L.142/2023, OM 210/2024). Integrarea e testată doar pe endpoint-ul de test RAR, nepusă la clienți. - -Problema reală e de **ISV**: nu vrei să redistribui un `.exe` VFP la fiecare corecție. Muți logica -(mapare + login RAR + jurnal + retry) pe un **gateway central depanabil o dată pentru toți**, iar ROAAUTO -rămâne client subțire. Un client real a cerut automatizarea — primul plătitor, nu ipoteză. - -Rezultat țintă (treapta 1): o prezentare reală trimisă din ROAAUTO prin gateway apare la RAR, vizibilă -în dashboard, cu retry pe erori tranzitorii și fără a stoca parole. - -## Decizii blocate în review - -| # | Decizie | Alegere | -|---|---|---| -| Mod | Postura review | **SELECTIVE EXPANSION** — bulletproof treapta 1 + 4 cherry-picks acceptate | -| Idempotency | Anti-dublură | **Hash de conținut pe server**, UNIQUE; "nu se acceptă 2 prezentări identice". `nr_comanda` NU e cerut (RAR n-are câmpul; SaaS n-are comenzi) | -| Defensibilitate | Dovadă vs privacy | **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare | -| Poză odometru | b64Image obligatoriu? | **Se validează întâi la „The Assignment"** pe endpoint test; nu construim orbește | -| Odometru repair | Validare | **Strict + stare `needs_data`** (nu trimite incomplet) | -| Cherry-picks | Adăugate în v1 | Alertă submission-uri blocate; `/healthz`+`/metrics`; sugestie fuzzy mapare; export audit CSV | -| Import DBF | Migrare date | `import_dbf.py` cu **dry-run + raport** înainte de scriere | - -## Constatări din spec-ul oficial (corecturi față de design) - -1. **`Prezentari` n-are câmp de număr comandă** (spec model #11, l.387-391). RAR acceptă duplicate (fără - constrângere unică). → cheia de idempotență e doar pentru retry-urile *tale*, hash pe conținut. -2. **Toate câmpurile sunt obligatorii except `obs`** (doc informativ l.47). Asta include **`b64Image` - (poză odometru, l.40)** — VFP-ul trimite azi gol. Posibil gap de conformitate în producție. De validat. -3. **Înlocuire/reparație odometru** (l.37, l.39): cer **ambele** valori `odometruInitial` + `odometruFinal`. - VFP trimite azi `odometruInitial: null`. Anti-fraudă (penal până la 5.000 lei). -4. **`sistemReparat` e „codificat în lista de prestații"** (l.45) → probabil derivabil din codurile - `codPrestatie` prin mapare, nu input liber separat. Reduce Open Question #2. -5. **URL-uri: spec-ul are typo-uri de copy/paste** (postPrezentare listat ca `/patchPrezentare/{id}` l.244; - markAnulata ca `/getPrezentare/{id}` l.227). **Sursa de adevăr = URL-urile testate din VFP** - (`/prezentari/postPrezentare`, `/prezentari/markPrezentareAnulataById/{id}`). -6. **Monitorizare:** spec-ul are `getAllPrezentari` (prezentări active); VFP folosește - `getAllPrezentariFinalizate` (nedocumentat). De ales deliberat — vezi „Monitorizare" mai jos. - -## ⚠️ Prerequisite blocant — „The Assignment" (spike, ~1h, ÎNAINTE de orice cod) - -Pe endpoint-ul de **test RAR**, măsoară pe `/public/login` + `postPrezentare`: -- **Durata de viață a JWT-ului** (login, apoi postPrezentare la intervale crescătoare până la 401) - → dimensionează fereastra de retry autonom din worker. -- **Dacă `postPrezentare` trece FĂRĂ `b64Image`** și fără `odometruInitial` → decide dacă poza e - obligatorie în prod (constatare #2) și dacă ROAAUTO trebuie să atașeze poza. -- **Valorile acceptate pentru `tipPrestatie` / `sistemReparat`** (enum nedocumentat) — probează câteva. - -Rezultatul acestui spike decide robustețea cozii și scopul real al ROAAUTO. Nu pornește implementarea -worker-ului înainte de el. - -## Arhitectură - -``` -ROAAUTO (VFP, la client) GATEWAY FastAPI (central, 1 container) - citește comanda + creds RAR din Oracle POST /v1/prezentari {comanda + RAR creds + idempotency implicit} - ──HTTPS──────────────────────────────────────▶ API - ├─ valid Pydantic (vin, odometru, prestatii) - ├─ rezolvă op→codPrestatie (operations_mapping) - ├─ derivă sistemReparat din coduri - ├─ calc idempotency_key = hash(conținut canonic) - ├─ INSERT submission (PII criptat tranzitoriu, queued) - ◀── {submissionId, status: queued|needs_mapping|needs_data} ──┘ (UNIQUE → dedup, întoarce id existent) - - WORKER (proces separat, restart:always, poll SQLite WAL) - claim atomic: BEGIN IMMEDIATE; UPDATE…SET sending WHERE id=? AND status='queued' - login RAR → JWT → postPrezentare → retry/backoff ÎN fereastra JWT - succes: scrie idPrezentare; PURJEAZĂ creds; PII criptat rămâne max 90 zile - - Browser ─▶ Dashboard (Jinja2+HTMX): monitorizare + coadă curentă + editor mapări + nomenclator + audit CSV - ROAAUTO (timer re-push) ─▶ GET /v1/prezentari?status=error → retrimite cu creds proaspete (durabilitate pene lungi) -``` - -### Data flow + shadow paths (postPrezentare) - -``` -INPUT ──▶ VALIDARE ──▶ MAPARE op→cod ──▶ ENQUEUE ──▶ WORKER login+send ──▶ RAR - │ │ │ │ │ - nil/empty vin invalid cod lipsă → idempotency JWT expirat → error (re-push ROAAUTO) - vin? odometru<0 needs_mapping key dublu → RAR 4xx → needs_data/error (logat, NU silent) - creds? repair fără sistemReparat întoarce id RAR 5xx/timeout → retry backoff - init → needs_data nederivabil existent b64Image lipsă (dacă obligatoriu) → needs_data -``` - -### Mașina de stări submission - -``` -queued → sending → { sent | needs_mapping | needs_data | error } - needs_mapping : operație fără codPrestatie mapat → ținut gateway-side, NU trimis incomplet - needs_data : repair odometru fără init/final SAU poză lipsă (dacă obligatorie) → ținut, NU trimis - error : eligibil re-push din ROAAUTO (GET ?status=error) - sent : are idPrezentare RAR; creds purjate; PII criptat max 90 zile; terminal -``` - -## Componente (un repo, `docker compose up`) - -1. **API `app/api/v1`** (FastAPI): - - `POST /v1/prezentari` (una/mai multe) → Pydantic, mapare, enqueue, răspuns `submissionId`. - - `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică + re-push ROAAUTO. - - `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`. - - `GET/PUT /v1/mapari` — CRUD mapare per cont, cu **sugestie fuzzy** pe denumire (cherry-pick). - - `PATCH /v1/prezentari/{id}/anulare`, `/corectie` — proxy markPrezentareAnulataById / patchPrezentare. - - `GET /v1/audit/export?from=&to=` — **CSV** cu ce s-a trimis (cherry-pick, leagă reținerea 90 zile). - - Auth gateway: **API key per cont ROA** (separată de creds RAR), cu emitere/rotire/revocare. - - **Redactare credențiale (CORE, nu opțional):** middleware care garantează că body-ul pe - `/v1/prezentari` NU se loghează niciodată și `password` se scrubează din excepții/APM. - - *(Amânat: `POST /v1/import` xlsx/csv — treapta 2.)* -2. **Client RAR `app/rar_client.py`** — portare din `rar_autopass.prg` + `rar-forms.prg`: - login+JWT, getNomenclatorPrestatii, postPrezentare, getAllPrezentari, getPrezentare, - markPrezentareAnulataById, patchPrezentare. `httpx` + retry/backoff. **URL-uri din VFP testat**, nu din spec. -3. **Worker `app/worker`** — proces separat sub Docker `restart: always` (NU asyncio în uvicorn dacă scalezi - workeri — un singur scriitor pe coadă ca să nu dublezi un record legal). Buclă claim atomic → login → send → - retry → scrie status+idPrezentare. `b64Image` mare → BLOB/path pe disc, nu RAM. -4. **Dashboard `app/web`** — Jinja2 + HTMX (server-rendered, zero build): monitorizare, stare coadă, - editor mapări (cu fuzzy), browser nomenclator, **banner alertă submission-uri blocate** (cherry-pick). -5. **SQLite (WAL)** — un fișier `.db`: - - `accounts`, `api_keys`. - - `operations_mapping` (cod_op_service → codPrestatie, `auto_send`) ← `mapare_prestatii.DBF`. - - `nomenclator_rar` (cache {codPrestatie, numePrestatie}) ← `prestatii_rar.DBF`. - - `submissions`: `idempotency_key` UNIQUE, status, statusCode RAR, eroare, `idPrezentare`, retry, timestamps. - PII (`vin`, `odometru`, `dataPrestatie`, `prestatii`, `b64Image`) **criptat** + `purge_after` (sent+90z). - - **Niciun câmp pentru parole RAR.** - -## Securitate & raza de explozie (constatare review #5) - -Gateway-ul vede parolele RAR ale tuturor clienților în memorie/tranzit. Zero-storage reduce riscul la rest, -dar o greșeală de logging scurge parole live, iar AGPL = codul e public. Controale **hard**: -- Middleware de redactare (mai sus) — body-uri cu parole niciodată în loguri/APM/excepții. -- HTTPS obligatoriu; recomandare TLS pinning din ROAAUTO către gateway. -- Parola folosită doar pentru `login` în worker, apoi ștearsă din itemul de coadă. - -### Error & rescue map (extras) - -| Codepath | Ce poate eșua | Excepție | Rescued? | Acțiune | User vede | -|---|---|---|---|---|---| -| rar_client.login | timeout/5xx | httpx.TimeoutException | Y | retry backoff în fereastra JWT | submission `error`, re-push | -| rar_client.login | 401 creds greșite | AuthError | Y | NU retry; marchează error+motiv | „credențiale RAR invalide" | -| rar_client.postPrezentare | 4xx validare RAR | RarValidationError | Y | needs_data + payload RAR logat | rând flag-uit în dashboard | -| rar_client.postPrezentare | JSON malformat/empty | JSONDecodeError | Y | error + raw response logat (scrub) | submission `error` | -| API enqueue | idempotency dublu | IntegrityError(UNIQUE) | Y | întoarce submissionId existent | „deja înregistrat" | -| worker claim | două procese | (prevenit) BEGIN IMMEDIATE | Y | un singur scriitor | n/a | -| mapare | cod lipsă | (control de flux) | Y | needs_mapping, NU trimite | „necesită mapare" | - -**Regulă:** fără `except Exception` generic. Fiecare rescue: retry / degradare cu mesaj / re-raise cu context. - -### Failure modes registry (gap-uri critice de evitat) - -| Codepath | Failure | Rescued | Test | User vede | Logged | -|---|---|---|---|---|---| -| postPrezentare repair fără odometru init | record fraud-sensibil incomplet | Y (needs_data) | DA | flag dashboard | DA | -| dublu-send din 2 bucle retry | duplicat la RAR | Y (idempotency UNIQUE) | DA | nimic (transparent) | DA | -| poză lipsă dacă obligatorie | RAR respinge | Y (needs_data după spike) | DA | flag dashboard | DA | -| submission blocat tăcut | declarație legală pierdută | Y (alertă cherry-pick) | DA | banner + webhook | DA | - -## Client ROAAUTO (VFP) — refactor minim - -- `settings.xml` păstrează doar **URL gateway + API key** (rotește parola de test expusă acum în SVN!). -- Creds RAR ale clientului se citesc din **Oracle** și se trimit în payload la gateway peste HTTPS. -- `export_comenzi.prg` rămâne, dar construiește JSON și face `POST /v1/prezentari` (nu XML + apel RAR direct). -- Dispar din VFP: `Login`, `UpdateNomenclator`, `GetCodRarPentruOperatie`, maparea, `rar_log` → în web. -- Job periodic „re-push pending" — reutilizează timer-ul existent (`OnAutoProcessTimer`/`nTimerHandle` din `rar-forms.prg`). -- Dacă spike-ul confirmă poza obligatorie: ROAAUTO atașează poza odometrului (de clarificat sursa). - -## Migrare date - -`tools/import_dbf.py` (cu `dbfread`) — **dry-run + raport întâi**: rânduri valide, mapări orfane, -coduri necunoscute în nomenclator. Confirmi, apoi scrie în SQLite. Surse: `mapare_prestatii.DBF`, -`prestatii_rar.DBF`. (`rar_log.DBF` NU se migrează — jurnalul nou e `submissions` + live din RAR.) - -## Observabilitate (cherry-picks) - -- `/healthz` — worker viu + ultimul login RAR reușit + adâncime coadă. -- `/metrics` — submissions pe status, latență send, retry count, backlog needs_mapping/needs_data. -- Alertă submission-uri blocate — banner dashboard + webhook/email peste prag (plasă de siguranță legală). - -## Monitorizare — sursa de adevăr - -Citit **live din RAR** + stare coadă locală. Atenție: `getAllPrezentariFinalizate` (VFP) întoarce doar -FINALIZATA; `getAllPrezentari` (spec) întoarce active. Alege `getAllPrezentari` dacă vrei și draft/anulate. -Cache scurt (ex. 30-60s) + UX „RAR indisponibil, arăt ultima stare a cozii" (nu lega dashboard-ul de uptime RAR). - -## Deploy - -- Start: **LXC Proxmox + Cloudflare Tunnel** (0 €, teste). Producție: **VPS mic always-on** (~5 €/lună). -- Un container: uvicorn (API) + worker (proces 2), `restart: always`, volum SQLite. Mutare = copiezi container + `.db`. -- Open-source pe github.com/romfast, **AGPL-3.0**. ⚠️ Vezi „Decizie one-way door" mai jos. -- romfast.ro/hosting.com = doar landing (ASGI + worker daemon nu merg pe shared hosting). - -## Verificare (end-to-end) - -1. Spike „The Assignment" rulat, JWT TTL + cerințe b64Image/odometru documentate. -2. `import_dbf.py --dry-run` produce raport corect pe DBF-urile reale; apoi import confirmat. -3. `docker compose up`; `/healthz` verde. -4. `POST /v1/prezentari` cu o comandă reală din ROAAUTO (test) → `submissionId`, worker trimite, - apare `FINALIZATA` la RAR (test) și în dashboard. -5. Re-trimite aceeași comandă identică → întoarce același `submissionId` (idempotency), NU dublă la RAR. -6. Trimite operație nemapată → `needs_mapping`; repair odometru fără init → `needs_data`; nu se trimit incomplet. -7. Oprește RAR (sau forțează 5xx) → submission `error`, ROAAUTO re-push recuperează; nimic blocat tăcut. -8. Verifică: SQLite n-are câmp parolă; după `sent`, PII e criptat și are `purge_after`; logurile n-au parole. -9. Teste automate: unit (mapare, idempotency hash, validare odometru), integration (worker claim atomic, - retry/backoff), E2E pe endpoint test RAR. - -## Decizie one-way door de semnalat (CEO) - -**Licență AGPL fără CLA** poate bloca un viitor strat SaaS comercial: odată ce intră PR-uri externe sub AGPL, -relicențierea cere acordul tuturor contributorilor. Dacă vrei să păstrezi opțiunea de dual-license (open core + -hosted comercial — exact teza ta de la treapta 2), adoptă **CLA / copyright assignment din ziua 1**. AGPL în sine -e bună pentru moat (forțează competitorii care-l găzduiesc să-și deschidă modificările). Decizia: deliberat, acum. - -## NOT in scope (amânat, cu motiv) - -- `POST /v1/import` xlsx/csv + UX mapare coloane — treapta 2 (piață non-ROA). Motor identic, fără rescriere. -- Modelul de conturi RAR (addClient/roluri) — nu-l replicăm; rămâne la RAR. -- Outbox în Oracle (Approach C) — pentru clienți non-ROA / viitor, cere acces gateway→Oracle. -- Agregare/produse din datele service-urilor — niciodată default; doar opt-in + anonimizare + lawyered. -- Redis/arq/Postgres — SQLite WAL + un worker acoperă volumul (60-100 prezentări/lună/client). - -## Open questions rămase - -1. Sursa pozei odometrului în fluxul ROAAUTO (dacă spike-ul confirmă b64Image obligatoriu). -2. `tipPrestatie` — valori acceptate (de probat la spike). -3. Un singur user RAR per agent economic sau mai mulți (afectează `idUser`/filtrare monitorizare). -4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client. - -## What already exists (reuse) - -| Sub-problemă | Reuse din VFP | -|---|---| -| Contract RAR (login/JWT, nomenclator, postPrezentare, cancel) | `rar_autopass.prg`, `rar-forms.prg:655,720` → `rar_client.py` | -| Mapare op→codPrestatie + `auto_send` | `GetCodRarPentruOperatie` → `operations_mapping` | -| Timer re-push | `OnAutoProcessTimer`/`nTimerHandle` (`rar-forms.prg`) | -| Export (oglindă treapta 2) | `btnExportExcel.Click` (`rar_advanced.prg`) | -| Migrare DBF | `import_dbf.py` citește direct cele 3 `.DBF` | diff --git a/docs/plans/plan.md b/docs/plans/plan.md new file mode 100644 index 0000000..74c1913 --- /dev/null +++ b/docs/plans/plan.md @@ -0,0 +1,321 @@ +# Plan unic — Gateway RAR AUTOPASS (migrare ROAAUTO din VFP în Web API) + +> **Planul executabil, sursă unică.** Consolidează design-ul de produs + planul de implementare, +> aliniat la contractul verificat live. Citește-l împreună cu: +> - `docs/api-rar-contract.md` — **contractul RAR (sursa de adevăr)**, verificat live 2026-06-15. +> - `docs/CONTEXT.md` — continuitate între sesiuni. +> +> Înlocuiește fostele `plan-eng-review.md` + `plan-design-review.md` (consolidate aici). +> Ultima actualizare: 2026-06-15. + +## Cum reiei (sesiune nouă) + +1. `git clone git@gitea.romfast.ro:romfast/rar-autopass.git` +2. `cp settings.xml.example settings.xml` și completează credențialele de test RAR (blocul ``). NU se comite. +3. Citește `docs/api-rar-contract.md` (contractul) și acest plan. +4. Pornește de la **Roadmap de execuție** (mai jos) — pasul blocant următor e **T1**. + +--- + +## 1. Problema și rezultatul țintă + +ROAAUTO (Visual FoxPro + Oracle, la fiecare service client) declară azi prestațiile la **RAR AUTOPASS** +direct din clasa `RarAutoPass` (`rar_autopass.prg`), prin `MSXML2.ServerXMLHTTP`. Obligație legală +(L.142/2023, OM 210/2024). Integrarea e testată doar pe endpoint-ul de test RAR, nepusă la clienți. + +Problema reală e de **ISV**: nu vrei să redistribui un `.exe` VFP la fiecare corecție. Muți logica +(mapare + login RAR + jurnal + retry) pe un **gateway central depanabil o dată pentru toți**; ROAAUTO +rămâne client subțire. Un client real a cerut automatizarea — primul plătitor, nu ipoteză. + +**Rezultat țintă (treapta 1):** o prezentare reală trimisă din ROAAUTO prin gateway apare `FINALIZATA` +la RAR, vizibilă în dashboard, cu retry pe erori tranzitorii și fără a stoca parole. + +## 2. Contract RAR — reguli care guvernează implementarea + +Detalii complete: `docs/api-rar-contract.md`. Esențialul pentru cod: + +- **Endpoint-uri** (verificate live): login `POST /public/login`; nomenclator `GET /nomenclator/getNomenclatorPrestatii`; + trimitere `POST /prezentari/postPrezentare`. Bază test `https://apps.rarom.ro/test-rar-autopass`, prod `…/rar-autopass`. +- **JWT TTL = 30h** (108000s). Worker-ul se re-loghează la expirare; retry-ul NU e plafonat la 30h. +- **Payload** `postPrezentare`: `vin`, `nrInmatriculare`, `dataPrestatie`, `odometruFinal`, `odometruInitial`, + `prestatii:[{codPrestatie,idPrezentare:null}]`, `sistemReparat`, `status:"FINALIZATA"`, `obs?`, `b64Image?`. + **`tipPrestatie` NU se trimite** (generat de server, `GENERIC`). +- **Reguli de validare (RAR le aplică; le replicăm în gateway înainte de enqueue):** + - `vin`: 17 caractere majuscule, fără spații/special, **fără O, I, Q** → regex `^[A-HJ-NPR-Z0-9]{17}$`. + - `nrInmatriculare`: max 10, litere+cifre, majuscule → `^[A-Z0-9]{1,10}$`. + - `dataPrestatie`: ∈ [2024-12-01, azi], TZ `Europe/Bucharest`. + - `b64Image`: **opțional**; dacă prezent, base64 valid. + - `odometruInitial`: `null` normal; **obligatoriu dacă `prestatii` ∋ `R-ODO` sau `I-ODO`**; `odometruInitial <= odometruFinal`. + - `odometruFinal`: trimis ca string (per exemplul oficial). + - `sistemReparat`: trimis mereu; default `"null"` în v1. + - Normalizează `vin`/`nrInmatriculare` cu `.strip().upper()` înainte de regex (DBF-urile pot avea spații). +- **Anulare/corecție prin API: NU există.** API e strict INSERT `FINALIZATA`; `FINALIZATA` nu se anulează + (doar `SALVATA`, pe care API nu le produce). Corecția = email `suport.autopass@rarom.ro`. +- **Monitorizare:** pe TEST lista finalizate **nu întoarce `prestatii`** (pe prod da, cu filtrare + export Excel). +- **Idempotency:** RAR n-are câmp nr. comandă și acceptă duplicate → dedup-ul e responsabilitatea noastră. + +## 3. Arhitectură + +``` +ROAAUTO (VFP, la client) GATEWAY FastAPI (central, 1 container) + citește comanda + creds RAR din Oracle POST /v1/prezentari {comanda + RAR creds + idempotency implicit} + ──HTTPS──────────────────────────────────────▶ API + ├─ valid Pydantic (vin 17/fără O,I,Q; nrInm; dată ∈ [2024-12-01,azi] TZ Bucharest) + ├─ rezolvă op→codPrestatie (operations_mapping) + ├─ odometruInitial cerut DOAR dacă coduri ∋ R-ODO/I-ODO + ├─ NU trimite tipPrestatie; sistemReparat="null" + ├─ calc idempotency_key = hash(conținut canonic) + ├─ INSERT submission (PII criptat tranzitoriu, queued) + ◀── {submissionId, status: queued|needs_mapping|needs_data} ──┘ (UNIQUE → dedup, întoarce id existent) + + WORKER (proces separat, restart:always, poll SQLite WAL) + claim atomic: BEGIN IMMEDIATE; UPDATE…SET sending WHERE id=? AND status='queued' + login RAR → JWT (30h, re-login la expirare) → reconciliere → postPrezentare → retry/backoff + succes: scrie idPrezentare(data.id); PURJEAZĂ creds; PII criptat rămâne max 90 zile + + Browser ─▶ Dashboard (Jinja2+HTMX): monitorizare live RAR + coadă + editor mapări + nomenclator + audit CSV + + BANNER alertă submission-uri blocate (singura plasă pe pene > 30h) + (Re-push din ROAAUTO: SCOS din v1. Durabilitatea = SQLite pe volum persistent + backup + restart.) +``` + +### Mașina de stări submission + +``` +queued → sending → { sent | needs_mapping | needs_data | error } + needs_mapping : operație fără codPrestatie mapat → ținut gateway-side, NU trimis incomplet + needs_data : prestatii ∋ R-ODO/I-ODO fără odometruInitial, SAU validare RAR eșuată (VIN/dată/nrInm) → ținut + error : login creds invalide, RAR 4xx nerecuperabil, sau pană > 30h → ALERTĂ banner (fără re-push automat) + sent : are idPrezentare (data.id) RAR; creds purjate; PII criptat max 90 zile; terminal + (anulare/corecție: NU există tranziție — FINALIZATA e terminal la RAR; corecția = suport RAR) +``` + +## 4. Componente (un repo, `docker compose up`) + +1. **API `app/api/v1`** (FastAPI): + - `POST /v1/prezentari` (una/mai multe) → Pydantic (validările din §2), mapare, enqueue, `submissionId`. + - `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică (stare coadă + idPrezentare). + - `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`. + - `GET/PUT /v1/mapari` — CRUD mapare per cont, cu **sugestie fuzzy** pe denumire (cherry-pick). + - `GET /v1/audit/export?from=&to=` — **CSV** cu ce s-a trimis (cherry-pick, leagă reținerea 90 zile). + - Auth gateway: **API key per cont ROA** (separată de creds RAR), cu emitere/rotire/revocare. + - **Redactare credențiale (CORE):** middleware care garantează că body-ul pe `/v1/prezentari` NU se loghează + niciodată și `password` se scrubează din excepții/APM. + - *(SCOS din v1: anulare/corecție prin API. Amânat: `POST /v1/import` xlsx/csv — treapta 2.)* +2. **Client RAR `app/rar_client.py`** — portare din `rar_autopass.prg` + `rar-forms.prg`: login+JWT, + getNomenclatorPrestatii, postPrezentare, getAllPrezentari/getAllPrezentariFinalizate. `httpx` + retry/backoff. + **URL-uri din VFP testat** (verificate live). *(NU portăm anulare/corecție.)* +3. **Worker `app/worker`** — proces propriu supravegheat (NU task `asyncio` în uvicorn — altfel worker mort lasă + containerul „sănătos"), sub Docker `restart: always`. Buclă claim atomic → login → reconciliere → send → retry. + - JWT 30h, **re-login la expirare** (retry nu e plafonat la 30h). + - **Reconciliere înainte de re-send (anti-duplicat pe răspuns pierdut — P1):** dacă răspunsul RAR se pierde după + ce RAR a inserat, rândul rămâne `sending`; un re-send orb ar crea duplicat (RAR acceptă duplicate). Înainte de + re-send, worker interoghează lista RAR pe `VIN + dataPrestatie + odometruFinal`; dacă găsește → marchează `sent` + cu `idPrezentare` găsit, NU re-trimite. **UNIQUE pe `submissions` NU acoperă acest caz.** + - **Lease/timeout pe `sending` orfane** (worker mort mid-POST) → recuperat de reconciliere. + - `b64Image` (opțional) mare → BLOB/path pe disc, nu RAM. +4. **Dashboard `app/web`** — Jinja2 + HTMX (server-rendered, zero build): monitorizare live RAR + coadă + editor + mapări (cu fuzzy) + browser nomenclator + **banner alertă submission-uri blocate**. Stări explicite: + empty (coadă goală cu CTA), error (needs_mapping/needs_data cu motiv), **RAR indisponibil → ultima stare a cozii**. +5. **SQLite (WAL)** — un fișier `.db` pe **volum persistent + backup**: + - `accounts`, `api_keys`. + - `operations_mapping` (cod_op_service → codPrestatie, `auto_send`) ← `mapare_prestatii.DBF`. + - `nomenclator_rar` (cache {codPrestatie, numePrestatie}) ← `prestatii_rar.DBF`. + - `submissions`: `idempotency_key` UNIQUE, status, statusCode RAR, eroare, `idPrezentare`, retry, timestamps. + PII (`vin`, `odometru`, `dataPrestatie`, `prestatii`, `b64Image`) **criptat** + `purge_after` (sent+90z). + - **Niciun câmp pentru parole RAR.** + +## 5. Securitate & raza de explozie + +Gateway-ul vede parolele RAR ale tuturor clienților în memorie/tranzit. Zero-storage reduce riscul la rest, +dar o greșeală de logging scurge parole live, iar AGPL = codul e public. Controale **hard**: +- Middleware de redactare — body-uri cu parole niciodată în loguri/APM/excepții. +- HTTPS obligatoriu; recomandare TLS pinning din ROAAUTO către gateway. +- Parola folosită doar pentru `login` în worker. Cu JWT 30h, după primul login reușit tokenul acoperă retry-urile; + creds se șterg. Dacă login-ul eșuează (RAR jos), creds rămân în itemul (criptat) până la primul login reușit. + +### Failure modes registry + +| Codepath | Failure | Rescued | User vede | Logged | +|---|---|---|---|---| +| postPrezentare cu R-ODO/I-ODO fără odometru init | record fraud-sensibil incomplet | Y (needs_data determinist) | flag dashboard | DA | +| dublu-send (răspuns RAR pierdut, rând `sending`) | duplicat la RAR (UNIQUE NU acoperă) | Y (reconciliere VIN+dată+odometru) | nimic (transparent) | DA | +| dublu-enqueue (același conținut, 2 cereri API) | submission dublu | Y (idempotency UNIQUE) | „deja înregistrat" | DA | +| VIN cu O/I/Q sau ≠17 / dată în afara intervalului | RAR respinge 4xx | Y (validare Pydantic înainte de enqueue) | flag „date invalide" | DA | +| login 401 creds greșite | nu se poate trimite | Y (NU retry; error+motiv) | „credențiale RAR invalide" | DA | +| pană RAR > 30h (fără re-push) | declarație legală întârziată | Parțial (alertă, NU auto-recovery) | banner + webhook | DA | +| worker mort (proces, nu container) | coada se oprește tăcut | Y (proces propriu + /healthz → restart) | banner | DA | + +**Regulă:** fără `except Exception` generic. Fiecare rescue: retry / degradare cu mesaj / re-raise cu context. + +> **Risc acceptat (2026-06-15):** scoaterea re-push-ului lasă penele RAR > 30h fără recuperare automată. Acoperit de +> alerta de submission blocat. Re-introducem re-push (timer VFP `OnAutoProcessTimer`) dacă apare în practică o pană > 30h. + +## 6. Client ROAAUTO (VFP) — refactor minim + +- `settings.xml` păstrează doar **URL gateway + API key** (rotește parola de test expusă acum în SVN!). +- Creds RAR ale clientului se citesc din **Oracle** și se trimit în payload la gateway peste HTTPS. +- `export_comenzi.prg` rămâne, dar construiește JSON și face `POST /v1/prezentari` (nu XML + apel RAR direct). +- Dispar din VFP: `Login`, `UpdateNomenclator`, `GetCodRarPentruOperatie`, maparea, `rar_log` → în web. +- **ROAAUTO = pur expeditor în v1.** Re-push scos; timer-ul `OnAutoProcessTimer` rămâne disponibil dacă revine. +- **Fără atașare poză** (`b64Image` opțional) — un câmp mai puțin de construit în VFP. + +## 7. Migrare date + +`tools/import_dbf.py` (cu `dbfread`) — **dry-run + raport întâi**: rânduri valide, mapări orfane, coduri +necunoscute în nomenclator. Confirmi, apoi scrie în SQLite. Surse: `mapare_prestatii.DBF`, `prestatii_rar.DBF`. +(`rar_log.DBF` NU se migrează — jurnalul nou e `submissions` + live din RAR.) + +## 8. Observabilitate (cherry-picks) + +- `/healthz` — worker viu + ultimul login RAR reușit + adâncime coadă; pică → semnal de restart. +- `/metrics` — submissions pe status, latență send, retry count, backlog needs_mapping/needs_data. +- Alertă submission-uri blocate — banner dashboard + webhook/email peste prag (plasă de siguranță legală, acum critică). + +## 9. Deploy + +- Start: **LXC Proxmox + Cloudflare Tunnel** (0 €, teste). Producție: **VPS mic always-on** (~5 €/lună). +- Un container: uvicorn (API) + worker (proces 2), `restart: always`, **volum SQLite persistent + backup**. + Mutare = copiezi container + `.db`. +- Open-source pe github.com/romfast, **AGPL-3.0**. ⚠️ Decide **CLA / copyright assignment din ziua 1** dacă vrei + opțiunea de dual-license (open core + hosted comercial). Relicențierea fără CLA cere acordul tuturor contributorilor. +- romfast.ro/hosting.com = doar landing (ASGI + worker daemon nu merg pe shared hosting). + +--- + +## 10. Roadmap de execuție (pașii rămași) + +Nimic din cod nu e scris încă (`app/`, `tools/` nu există). Ordine recomandată: + +- [ ] **T1 (P1, BLOCANT) — verificare contract live.** Un singur `POST /prezentari/postPrezentare` real pe **test** + cu o prezentare minimă validă (status FINALIZATA, sistemReparat `"null"`, fără b64Image/odometruInitial). Capturează: + mesajele de eroare exacte pe constrângeri (VIN cu O/I/Q, dată < 2024-12-01), forma exactă a răspunsului, `data.id`, + acceptarea `sistemReparat:"null"`. **Fă-l ÎNAINTE de a construi suprafața** — poate invalida design-ul validării/erorilor. + Verify: 200 + record vizibil în `getAllPrezentariFinalizate`. +- [ ] **T5 (P1) — `tools/import_dbf.py`** dry-run + raport pe `mapare_prestatii.DBF` / `prestatii_rar.DBF`, apoi import în SQLite. + Verify: raport rânduri valide/orfane/coduri necunoscute; import idempotent. +- [ ] **Schelet repo** — `app/api/v1`, `app/rar_client.py`, `app/worker`, `app/web`, SQLite (WAL), `docker compose up`, `/healthz` verde. +- [ ] **T3 (P1) — validare Pydantic completă** (VIN `^[A-HJ-NPR-Z0-9]{17}$`, nrInm, dată ∈ [2024-12-01,azi] TZ Bucharest, + R-ODO/I-ODO→odometruInitial, `odometruInitial<=odometruFinal`, normalize strip/upper). Verify: unit tests per regulă. +- [ ] **T4 (P1) — payload builder** cu `status:"FINALIZATA"`, `sistemReparat:"null"`, fără `tipPrestatie`, `odometruFinal` string. + Verify: snapshot payload == exemplul oficial din contract. +- [ ] **T2 (P1) — `app/worker` reconciliere** VIN+dată+odometru înainte de re-send pe `sending` + lease/timeout orfane. + Verify: test integration — răspuns pierdut simulat → fără duplicat la RAR. +- [ ] **T6 (P2) — worker proces/container propriu supravegheat;** `/healthz` pică → restart. Verify: worker omorât → restart automat. +- [ ] **T7 (P2) — deploy:** SQLite pe volum persistent numit + backup (singura copie durabilă, re-push scos). + Verify: recreare container → coada supraviețuiește. +- [ ] **Dashboard** (Jinja2+HTMX) cu stările empty/error/RAR-indisponibil + banner alertă. Apoi `/design-review` pe UI-ul live. + +### De decis ulterior (urmărit, nu blocant) +- **[P2]** Defer criptare-at-rest + purjare 90z până după primul postPrezentare real reușit? (gold-plating vs. privacy-argument-de-adopție). +- **[P2]** Gate pe `auto_send` pentru operații nou-mapate / valori neobișnuite — un record `FINALIZATA` eronat e permanent. + +## 11. Verificare (end-to-end) + +1. ✅ Spike „The Assignment" (JWT 30h, b64Image opțional, tipPrestatie server-gen, R-ODO/I-ODO) — `docs/api-rar-contract.md`. +2. T1: `postPrezentare` real pe test → 200 + `data.id`. +3. `import_dbf.py --dry-run` raport corect; apoi import confirmat. +4. `docker compose up`; `/healthz` verde. +5. `POST /v1/prezentari` cu o comandă reală (test) → `submissionId`, worker trimite, apare `FINALIZATA` la RAR + dashboard. +6. Re-trimite aceeași comandă identică → același `submissionId` (idempotency), NU dublă la RAR. +7. Operație nemapată → `needs_mapping`; `prestatii` ∋ R-ODO/I-ODO fără `odometruInitial` → `needs_data`; + VIN cu O/I/Q sau dată < 2024-12-01 → respins la validare, NU ajunge la RAR. +8. Oprește RAR / forțează 5xx → submission `error`, worker reia în fereastra de re-login; pană > 30h → alertă banner. +9. Simulează răspuns pierdut → reconcilierea previne duplicatul. +10. SQLite n-are câmp parolă; după `sent`, PII criptat + `purge_after`; logurile n-au parole. +11. Teste: unit (mapare, idempotency hash, validare VIN/dată/nrInm, R-ODO→odometruInitial), integration (claim atomic, + retry/backoff, reconciliere), E2E pe endpoint test RAR. + +## 12. NOT in scope (amânat, cu motiv) + +- **Re-push din ROAAUTO** — scos din v1; worker + alertă acoperă. Re-introducem dacă apare pană RAR > 30h. +- **Anulare/corecție prin API** — `FINALIZATA` nu se anulează/corectează prin API; corecția = suport RAR. +- **Atașare poză odometru în ROAAUTO** — `b64Image` opțional, nu în treapta 1. +- `POST /v1/import` xlsx/csv + UX mapare coloane — treapta 2 (piață non-ROA). Motor identic, fără rescriere. +- Modelul de conturi RAR (addClient/roluri) — rămâne la RAR. +- Outbox în Oracle (Approach C) — pentru clienți non-ROA / viitor, cere acces gateway→Oracle. +- Agregare/produse din datele service-urilor — niciodată default; doar opt-in + anonimizare + lawyered. +- Redis/arq/Postgres — SQLite WAL + un worker acoperă volumul (60-100 prezentări/lună/client). + +## 13. Open questions rămase + +1. ~~Sursa pozei odometrului~~ — închis (b64Image opțional). +2. ~~`tipPrestatie` / JWT TTL~~ — închis (server-generated; 30h). Rămâne: ce valori reale acceptă `sistemReparat` în afară de `"null"`. +3. Un singur user RAR per agent economic sau mai mulți (`idUser`/`idAgent` — afectează filtrarea monitorizării). +4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client. + +## 14. Decizii blocate (nu le re-deschide fără motiv) + +- Idempotency = **hash de conținut pe server**, UNIQUE (RAR n-are câmp nr. comandă, acceptă duplicate) + **reconciliere + înainte de re-send** pentru răspuns pierdut. +- **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare (defensibilitate vs privacy). +- Odometru: **strict + `needs_data`**, declanșat determinist de `R-ODO`/`I-ODO`. +- **Re-push scos din v1**; **anulare/corecție API scoase** (FINALIZATA). +- URL-urile RAR: **sursa de adevăr = VFP testat / contract verificat**, NU spec-ul vechi (typo-uri). +- Cherry-picks v1: alertă submission-uri blocate (critică), `/healthz`+`/metrics`, sugestie fuzzy mapare, export audit CSV. + +--- + +# ANEXĂ — Rațiunea de produs & direcția SaaS + +> Context de produs (din `/office-hours`), păstrat pentru deciziile de prioritizare. Nu blochează execuția treptei 1. + +## Demand evidence (validat: client real + lege) + +Un **client real a cerut automatizarea** introducerii prezentărilor în AUTOPASS — de aici a pornit proiectul. +Status quo înlocuit: interfața web oficială AUTOPASS, unde service-urile introduc **manual, prezentare cu prezentare**. +Obligație legală reală (L.142/2023): la fiecare prestație VIN + odometru; reparare/înlocuire odometru; operațiuni +principale la direcție, frânare, structura caroseriei/șasiului și alte sisteme de siguranță. Amenzi: info eronate +1.000–2.000 lei; manipulare odometru până la 5.000 lei / penal. Implică **toți** clienții ROA + **mii de service-uri +non-ROA** cu aceeași obligație → piață dincolo de ROA pentru un canal de import (xlsx/csv) ulterior. + +## Teza de produs + +*Cel mai ușor mod de a băga operațiile de service în AUTOPASS* — din ROAAUTO (API), din alte aplicații, sau din +fișiere — în loc de tastarea manuală. Câștigi prin **efort minim cerut service-ului**, nu prin features. +Lecția GTM (demoanaf.ro): viral prin variantă **reimaginată, simplă** a unui serviciu oficial greoi. + +## Trepte (același motor — mapare + coadă + trimitere + monitorizare — fără rescriere) + +- **Treapta 1 (acum):** core-ul pentru clientul care a cerut + clienții ROA, prin ROAAUTO. +- **Treapta 2 (non-ROA, web upload):** import xlsx/csv cu **mapare reținută** (map once, reuse forever) + dashboard. + Login web, fără instalare. Freemium **pe volum** (gratis ~30-40 prezentări/lună; plată peste prag). Metrica de preț = + prezentări/lună = unitatea obligatorie legal. +- **Treapta 3:** integrări mai adânci + sugestii AI de mapare (eventual conector MCP). + +**Moat:** (1) mapările reținute per service cresc costul de plecare; (2) lățimea integrării; (3) fiabilitatea de +conformitate (retry, monitorizare din RAR); (4) **privacy** (nu reținem datele lor) — argument de adopție. + +## Adopție & praguri (cifre reale) + +Clienți reali cunoscuți: **60-80** și **80-100 prezentări/lună** → ~2-4 min/prezentare manual × volum = **3-6 ore/lună** +de tastare = durerea care convertește. Prima folosire trivială (upload → mapare reținută → trimite, sub 5 min). +Gratis la volumul lor = fără decizie de achiziție. Service-urile mici = bază virală (durere mică); volumele mari = +cei mai dispuși să plătească. + +## Import xlsx/csv — UX (treapta 2) + +Două straturi de mapare, **ambele reținute per cont**: (1) mapare coloane (schema fișier → câmpuri canonice), +(2) mapare operații (etichete service → `codPrestatie`, cu sugestie fuzzy). Flux: upload → recunoaște coloanele → +propune maparea → **preview** (rânduri nemapate flag-uite) → „Trimite la RAR" → monitorizare. +Spectru de integrare (același backend): API → drop fișier (folder/SFTP/email-to-import) → upload manual în browser. + +## Opțiuni de deploy + +| Opțiune | Cost | Când | +|---|---|---| +| LXC Proxmox + Cloudflare Tunnel | 0 € | Start + teste (risc: net/curent birou) | +| VPS mic always-on (ex. Hetzner) | ~5 €/lună | Clienți reali / producție (recomandat) | +| romfast.ro / shared hosting | inclus | ⚠️ Doar landing (ASGI + worker daemon nepotrivite pe shared) | + +--- + +## What already exists (reuse din VFP) + +| Sub-problemă | Reuse | +|---|---| +| Contract RAR (login/JWT, nomenclator, postPrezentare) | `rar_autopass.prg`, `rar-forms.prg:655,720` → `rar_client.py` | +| Mapare op→codPrestatie + `auto_send` | `GetCodRarPentruOperatie` → `operations_mapping` | +| Export (oglindă treapta 2) | `btnExportExcel.Click` (`rar_advanced.prg`) | +| Migrare DBF | `import_dbf.py` citește direct cele `.DBF` | +| Timer (dacă revine re-push) | `OnAutoProcessTimer`/`nTimerHandle` (`rar-forms.prg`) — nefolosit în v1 | +