# 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` |