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>
16 KiB
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)
Prezentarin-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.- Toate câmpurile sunt obligatorii except
obs(doc informativ l.47). Asta includeb64Image(poză odometru, l.40) — VFP-ul trimite azi gol. Posibil gap de conformitate în producție. De validat. - Înlocuire/reparație odometru (l.37, l.39): cer ambele valori
odometruInitial+odometruFinal. VFP trimite aziodometruInitial: null. Anti-fraudă (penal până la 5.000 lei). sistemReparate „codificat în lista de prestații" (l.45) → probabil derivabil din codurilecodPrestatieprin mapare, nu input liber separat. Reduce Open Question #2.- 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}). - Monitorizare: spec-ul are
getAllPrezentari(prezentări active); VFP foloseștegetAllPrezentariFinalizate(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ă
postPrezentaretrece 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)
- API
app/api/v1(FastAPI):POST /v1/prezentari(una/mai multe) → Pydantic, mapare, enqueue, răspunssubmissionId.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/prezentariNU se loghează niciodată șipasswordse scrubează din excepții/APM. - (Amânat:
POST /v1/importxlsx/csv — treapta 2.)
- Client RAR
app/rar_client.py— portare dinrar_autopass.prg+rar-forms.prg: login+JWT, getNomenclatorPrestatii, postPrezentare, getAllPrezentari, getPrezentare, markPrezentareAnulataById, patchPrezentare.httpx+ retry/backoff. URL-uri din VFP testat, nu din spec. - Worker
app/worker— proces separat sub Dockerrestart: 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.b64Imagemare → BLOB/path pe disc, nu RAM. - 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). - 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_keyUNIQUE, 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.xmlpă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.prgrămâne, dar construiește JSON și facePOST /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/nTimerHandledinrar-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)
- Spike „The Assignment" rulat, JWT TTL + cerințe b64Image/odometru documentate.
import_dbf.py --dry-runproduce raport corect pe DBF-urile reale; apoi import confirmat.docker compose up;/healthzverde.POST /v1/prezentaricu o comandă reală din ROAAUTO (test) →submissionId, worker trimite, apareFINALIZATAla RAR (test) și în dashboard.- Re-trimite aceeași comandă identică → întoarce același
submissionId(idempotency), NU dublă la RAR. - Trimite operație nemapată →
needs_mapping; repair odometru fără init →needs_data; nu se trimit incomplet. - Oprește RAR (sau forțează 5xx) → submission
error, ROAAUTO re-push recuperează; nimic blocat tăcut. - Verifică: SQLite n-are câmp parolă; după
sent, PII e criptat și arepurge_after; logurile n-au parole. - 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/importxlsx/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
- Sursa pozei odometrului în fluxul ROAAUTO (dacă spike-ul confirmă b64Image obligatoriu).
tipPrestatie— valori acceptate (de probat la spike).- Un singur user RAR per agent economic sau mai mulți (afectează
idUser/filtrare monitorizare). - 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 |