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

97
docs/CONTEXT.md Normal file
View File

@@ -0,0 +1,97 @@
# 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.
## Ce este acest repo
Arhiva **bazei Visual FoxPro** existente (clasa `RarAutoPass`, ROAAUTO) care declară
prestațiile de service la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024), **plus**
planurile pentru rescrierea ca **Web API central (Python / FastAPI)**.
Codul VFP de aici este **punctul de plecare / sursa de adevăr de contract** pentru
versiunea web. Nu se mai dezvoltă; se portează.
## Stare actuală (iunie 2026)
- Integrarea VFP **funcționează** și e **testată pe endpoint-ul de test RAR**, dar
**nu e pusă la clienți** încă.
- Comunică direct cu RAR prin `MSXML2.ServerXMLHTTP` din `rar_autopass.prg` / `rar-forms.prg`.
- Maparea operație→`codPrestatie` în `mapare_prestatii.DBF`; nomenclator în `prestatii_rar.DBF`;
jurnal în `rar_log.DBF`; credențiale (în clar) în `settings.xml`.
- ⚠️ `settings.xml` conținea o **parolă de test reală** (`marius.mutu@romfast.ro`).
E **exclus din git** (`.gitignore`) și înlocuit cu `settings.xml.example`.
**De rotit parola** — a fost expusă în istoricul SVN vechi.
## Fișiere-cheie (VFP) și ce reutilizăm
| Fișier | Rol | Se portează în |
|---|---|---|
| `rar_autopass.prg` | clasa `RarAutoPass`: login+JWT, nomenclator, postPrezentare, cancel | `app/rar_client.py` |
| `rar-forms.prg` | UI + timer auto-process (`OnAutoProcessTimer`) | logica → worker; timer → re-push ROAAUTO |
| `export_comenzi.prg` | citește comenzi/operații, construiește payload | client subțire: `POST /v1/prezentari` |
| `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 |
## Planurile (î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.
> Continuă cu **plan-eng-review** și **plan-design-review** — acestea sunt cele două
> documente de reluat în următoarea sesiune.
## Arhitectura țintă (rezumat)
```
ROAAUTO (VFP, client subțire) ──HTTPS──▶ Gateway FastAPI (central, 1 container)
trimite comanda + creds RAR API: validare → mapare op→cod → enqueue (PII criptat)
◀── {submissionId, status} ─────────────┘
WORKER (proces separat): claim atomic → login RAR → postPrezentare → retry
Dashboard (Jinja2+HTMX): monitorizare live din RAR + stare coadă + editor mapări
ROAAUTO (timer) ──▶ GET /v1/prezentari?status=error → re-push (durabilitate pene lungi)
```
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)
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).
Rezultatul decide robustețea cozii și scopul real al ROAAUTO. Nu porni worker-ul înainte.
## De făcut după spike (din plan-eng-review, secțiunea 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.
3. `POST /v1/prezentari` cu o comandă reală (test) → worker trimite → `FINALIZATA` la RAR + în dashboard.
4. Test idempotency (re-trimitere identică → același `submissionId`, fără dublu la RAR).
5. `needs_mapping` / `needs_data` (nu se trimite incomplet); `error` + re-push.
6. Verifică: SQLite fără câmp parolă; după `sent` PII criptat + `purge_after`; loguri fără parole.
7. Teste: unit (mapare, hash idempotency, validare odometru), integration (claim atomic, retry), E2E test RAR.
## Decizii deja 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).
- **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare (defensibilitate vs privacy).
- Odometru repair: **strict + stare `needs_data`** (nu trimite incomplet).
- 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
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).
4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client.

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

View File

@@ -0,0 +1,241 @@
# 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` |