Files
rar-autopass/docs/plans/plan-design-review.md
Marius Mutu 78d21d5a38 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>
2026-06-14 23:10:28 +03:00

272 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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