Initial commit: baza VFP ROAAUTO + planuri migrare Web API

Arhiva clasei RarAutoPass (VFP) care declara prestatiile la RAR AUTOPASS,
ca baza pentru rescrierea ca gateway central Python/FastAPI.

Include:
- sursa VFP (.prg) + datele necesare migrarii (mapare_prestatii, prestatii_rar)
- spec-ul oficial RAR (txt)
- docs/plans/: plan-design-review + plan-eng-review
- docs/CONTEXT.md: handoff pentru continuarea in alta sesiune
- .gitignore specific Visual FoxPro (ignora artefacte compilate + credentiale)

settings.xml (cu parola de test in clar) EXCLUS; vezi settings.xml.example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:10:28 +03:00
commit 78d21d5a38
21 changed files with 4316 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
# 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.0002.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.
```