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>
242 lines
16 KiB
Markdown
242 lines
16 KiB
Markdown
# 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` |
|