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