359 lines
27 KiB
Markdown
359 lines
27 KiB
Markdown
# Plan unic — Gateway RAR AUTOPASS (migrare ROAAUTO din VFP în Web API)
|
||
|
||
> **Planul executabil, sursă unică.** Consolidează design-ul de produs + planul de implementare,
|
||
> aliniat la contractul verificat live. Citește-l împreună cu:
|
||
> - `docs/api-rar-contract.md` — **contractul RAR (sursa de adevăr)**, verificat live 2026-06-15.
|
||
> - `docs/CONTEXT.md` — continuitate între sesiuni.
|
||
>
|
||
> Înlocuiește fostele `plan-eng-review.md` + `plan-design-review.md` (consolidate aici).
|
||
> Ultima actualizare: 2026-06-15.
|
||
|
||
## Cum reiei (sesiune nouă)
|
||
|
||
1. `git clone git@gitea.romfast.ro:romfast/rar-autopass.git`
|
||
2. `cp settings.xml.example settings.xml` și completează credențialele de test RAR (blocul `<test>`). NU se comite.
|
||
3. Citește `docs/api-rar-contract.md` (contractul) și acest plan.
|
||
4. Pornește de la **Roadmap de execuție** (mai jos) — pasul blocant următor e **T1**.
|
||
|
||
---
|
||
|
||
## 1. Problema și rezultatul țintă
|
||
|
||
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**; 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 `FINALIZATA`
|
||
la RAR, vizibilă în dashboard, cu retry pe erori tranzitorii și fără a stoca parole.
|
||
|
||
## 2. Contract RAR — reguli care guvernează implementarea
|
||
|
||
Detalii complete: `docs/api-rar-contract.md`. Esențialul pentru cod:
|
||
|
||
- **Endpoint-uri** (verificate live): login `POST /public/login`; nomenclator `GET /nomenclator/getNomenclatorPrestatii`;
|
||
trimitere `POST /prezentari/postPrezentare`. Bază test `https://apps.rarom.ro/test-rar-autopass`, prod `…/rar-autopass`.
|
||
- **JWT TTL = 30h** (108000s). Worker-ul se re-loghează la expirare; retry-ul NU e plafonat la 30h.
|
||
- **Payload** `postPrezentare`: `vin`, `nrInmatriculare`, `dataPrestatie`, `odometruFinal`, `odometruInitial`,
|
||
`prestatii:[{codPrestatie,idPrezentare:null}]`, `sistemReparat`, `status:"FINALIZATA"`, `obs?`, `b64Image?`.
|
||
**`tipPrestatie` NU se trimite** (generat de server, `GENERIC`).
|
||
- **Reguli de validare (RAR le aplică; le replicăm în gateway înainte de enqueue):**
|
||
- `vin`: 17 caractere majuscule, fără spații/special, **fără O, I, Q** → regex `^[A-HJ-NPR-Z0-9]{17}$`.
|
||
- `nrInmatriculare`: max 10, litere+cifre, majuscule → `^[A-Z0-9]{1,10}$`.
|
||
- `dataPrestatie`: ∈ [2024-12-01, azi], TZ `Europe/Bucharest`.
|
||
- `b64Image`: **opțional**; dacă prezent, base64 valid.
|
||
- `odometruInitial`: `null` normal; **obligatoriu dacă `prestatii` ∋ `R-ODO` sau `I-ODO`**; `odometruInitial <= odometruFinal`.
|
||
- `odometruFinal`: trimis ca string (per exemplul oficial).
|
||
- `sistemReparat`: trimis mereu; default `"null"` în v1.
|
||
- Normalizează `vin`/`nrInmatriculare` cu `.strip().upper()` înainte de regex (DBF-urile pot avea spații).
|
||
- **Anulare/corecție prin API: NU există.** API e strict INSERT `FINALIZATA`; `FINALIZATA` nu se anulează
|
||
(doar `SALVATA`, pe care API nu le produce). Corecția = email `suport.autopass@rarom.ro`.
|
||
- **Monitorizare:** pe TEST lista finalizate **nu întoarce `prestatii`** (pe prod da, cu filtrare + export Excel).
|
||
- **Idempotency:** RAR n-are câmp nr. comandă și acceptă duplicate → dedup-ul e responsabilitatea noastră.
|
||
|
||
## 3. 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 17/fără O,I,Q; nrInm; dată ∈ [2024-12-01,azi] TZ Bucharest)
|
||
├─ rezolvă op→codPrestatie (operations_mapping)
|
||
├─ odometruInitial cerut DOAR dacă coduri ∋ R-ODO/I-ODO
|
||
├─ NU trimite tipPrestatie; sistemReparat="null"
|
||
├─ 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 (30h, re-login la expirare) → reconciliere → postPrezentare → retry/backoff
|
||
succes: scrie idPrezentare(data.id); PURJEAZĂ creds; PII criptat rămâne max 90 zile
|
||
|
||
Browser ─▶ Dashboard (Jinja2+HTMX): monitorizare live RAR + coadă + editor mapări + nomenclator + audit CSV
|
||
+ BANNER alertă submission-uri blocate (singura plasă pe pene > 30h)
|
||
(Re-push din ROAAUTO: SCOS din v1. Durabilitatea = SQLite pe volum persistent + backup + restart.)
|
||
```
|
||
|
||
### 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 : prestatii ∋ R-ODO/I-ODO fără odometruInitial, SAU validare RAR eșuată (VIN/dată/nrInm) → ținut
|
||
error : login creds invalide, RAR 4xx nerecuperabil, sau pană > 30h → ALERTĂ banner (fără re-push automat)
|
||
sent : are idPrezentare (data.id) RAR; creds purjate; PII criptat max 90 zile; terminal
|
||
(anulare/corecție: NU există tranziție — FINALIZATA e terminal la RAR; corecția = suport RAR)
|
||
```
|
||
|
||
## 4. Componente (un repo, `docker compose up`)
|
||
|
||
1. **API `app/api/v1`** (FastAPI):
|
||
- `POST /v1/prezentari` (una/mai multe) → Pydantic (validările din §2), mapare, enqueue, `submissionId`.
|
||
- `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică (stare coadă + idPrezentare).
|
||
- `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`.
|
||
- `GET/PUT /v1/mapari` — CRUD mapare per cont, cu **sugestie fuzzy** pe denumire (cherry-pick).
|
||
- `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):** middleware care garantează că body-ul pe `/v1/prezentari` NU se loghează
|
||
niciodată și `password` se scrubează din excepții/APM.
|
||
- *(SCOS din v1: anulare/corecție prin API. 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/getAllPrezentariFinalizate. `httpx` + retry/backoff.
|
||
**URL-uri din VFP testat** (verificate live). *(NU portăm anulare/corecție.)*
|
||
3. **Worker `app/worker`** — proces propriu supravegheat (NU task `asyncio` în uvicorn — altfel worker mort lasă
|
||
containerul „sănătos"), sub Docker `restart: always`. Buclă claim atomic → login → reconciliere → send → retry.
|
||
- JWT 30h, **re-login la expirare** (retry nu e plafonat la 30h).
|
||
- **Reconciliere înainte de re-send (anti-duplicat pe răspuns pierdut — P1):** dacă răspunsul RAR se pierde după
|
||
ce RAR a inserat, rândul rămâne `sending`; un re-send orb ar crea duplicat (RAR acceptă duplicate). Înainte de
|
||
re-send, worker interoghează lista RAR pe `VIN + dataPrestatie + odometruFinal`; dacă găsește → marchează `sent`
|
||
cu `idPrezentare` găsit, NU re-trimite. **UNIQUE pe `submissions` NU acoperă acest caz.**
|
||
- **Lease/timeout pe `sending` orfane** (worker mort mid-POST) → recuperat de reconciliere.
|
||
- `b64Image` (opțional) mare → BLOB/path pe disc, nu RAM.
|
||
4. **Dashboard `app/web`** — Jinja2 + HTMX (server-rendered, zero build): monitorizare live RAR + coadă + editor
|
||
mapări (cu fuzzy) + browser nomenclator + **banner alertă submission-uri blocate**. Stări explicite:
|
||
empty (coadă goală cu CTA), error (needs_mapping/needs_data cu motiv), **RAR indisponibil → ultima stare a cozii**.
|
||
5. **SQLite (WAL)** — un fișier `.db` pe **volum persistent + backup**:
|
||
- `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.**
|
||
|
||
## 5. Securitate & raza de explozie
|
||
|
||
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 — 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. Cu JWT 30h, după primul login reușit tokenul acoperă retry-urile;
|
||
creds se șterg. Dacă login-ul eșuează (RAR jos), creds rămân în itemul (criptat) până la primul login reușit.
|
||
|
||
### Failure modes registry
|
||
|
||
| Codepath | Failure | Rescued | User vede | Logged |
|
||
|---|---|---|---|---|
|
||
| postPrezentare cu R-ODO/I-ODO fără odometru init | record fraud-sensibil incomplet | Y (needs_data determinist) | flag dashboard | DA |
|
||
| dublu-send (răspuns RAR pierdut, rând `sending`) | duplicat la RAR (UNIQUE NU acoperă) | Y (reconciliere VIN+dată+odometru) | nimic (transparent) | DA |
|
||
| dublu-enqueue (același conținut, 2 cereri API) | submission dublu | Y (idempotency UNIQUE) | „deja înregistrat" | DA |
|
||
| VIN cu O/I/Q sau ≠17 / dată în afara intervalului | RAR respinge 4xx | Y (validare Pydantic înainte de enqueue) | flag „date invalide" | DA |
|
||
| login 401 creds greșite | nu se poate trimite | Y (NU retry; error+motiv) | „credențiale RAR invalide" | DA |
|
||
| pană RAR > 30h (fără re-push) | declarație legală întârziată | Parțial (alertă, NU auto-recovery) | banner + webhook | DA |
|
||
| worker mort (proces, nu container) | coada se oprește tăcut | Y (proces propriu + /healthz → restart) | banner | DA |
|
||
|
||
**Regulă:** fără `except Exception` generic. Fiecare rescue: retry / degradare cu mesaj / re-raise cu context.
|
||
|
||
> **Risc acceptat (2026-06-15):** scoaterea re-push-ului lasă penele RAR > 30h fără recuperare automată. Acoperit de
|
||
> alerta de submission blocat. Re-introducem re-push (timer VFP `OnAutoProcessTimer`) dacă apare în practică o pană > 30h.
|
||
|
||
## 6. 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.
|
||
- **ROAAUTO = pur expeditor în v1.** Re-push scos; timer-ul `OnAutoProcessTimer` rămâne disponibil dacă revine.
|
||
- **Fără atașare poză** (`b64Image` opțional) — un câmp mai puțin de construit în VFP.
|
||
|
||
## 7. 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.)
|
||
|
||
## 8. Observabilitate (cherry-picks)
|
||
|
||
- `/healthz` — worker viu + ultimul login RAR reușit + adâncime coadă; pică → semnal de restart.
|
||
- `/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ă, acum critică).
|
||
|
||
## 9. 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 persistent + backup**.
|
||
Mutare = copiezi container + `.db`.
|
||
- Open-source pe github.com/romfast, **AGPL-3.0**. ⚠️ Decide **CLA / copyright assignment din ziua 1** dacă vrei
|
||
opțiunea de dual-license (open core + hosted comercial). Relicențierea fără CLA cere acordul tuturor contributorilor.
|
||
- romfast.ro/hosting.com = doar landing (ASGI + worker daemon nu merg pe shared hosting).
|
||
|
||
---
|
||
|
||
## 10. Roadmap de execuție (pașii rămași)
|
||
|
||
Nimic din cod nu e scris încă (`app/`, `tools/` nu există). Ordine recomandată:
|
||
|
||
- [x] **T1 (P1, BLOCANT) — verificare contract live.** ✅ 2026-06-15. `postPrezentare` real pe test (record `data.id=68514`).
|
||
Capturate: format eroare `data:[{field,message}]` + 3 mesaje exacte (VIN O/I/Q, dată veche, dată viitoare),
|
||
forma răspuns success, `idPrezentare==id`, `idAgent` server-side, `sistemReparat:"null"` acceptat, `b64Image`/`odometruInitial` omise OK.
|
||
**Descoperire: WAF cere `User-Agent` (altfel 403).** Toate detaliile în `docs/api-rar-contract.md`.
|
||
- [x] **T5 (P1) — `tools/import_dbf.py`** ✅ 2026-06-15. `dbfread` → raport (rânduri valide, duplicate, goale, mapări
|
||
orfane = cod necunoscut în nomenclator) pe `prestatii_rar.DBF` (20 coduri) + `mapare_prestatii.DBF` (gol în arhivă).
|
||
Default dry-run; `--commit` scrie idempotent (upsert pe `nomenclator_rar` PK + `operations_mapping` UNIQUE), tranzacție
|
||
`BEGIN IMMEDIATE`/ROLLBACK. Verify: 6 teste (`tests/test_import_dbf.py`, cu writer dBASE III minimal pentru fixturi) +
|
||
dry-run real pe DBF-urile din repo.
|
||
- [x] **Schelet repo** — ✅ 2026-06-15. `app/api/v1`, `app/rar_client.py` (cu User-Agent), `app/worker`, `app/web`, SQLite (WAL),
|
||
`Dockerfile` + `docker compose`, `/healthz` verde. Verificat: login prin client OK, nomenclator 18 coduri,
|
||
worker heartbeat → `worker_alive=True`, enqueue + dedup idempotency funcționale.
|
||
- [x] **T3 (P1) — validare completă** ✅ 2026-06-15. `app/validation.py` (VIN `^[A-HJ-NPR-Z0-9]{17}$`, nrInm, dată ∈
|
||
[2024-12-01,azi] TZ Bucharest, R-ODO/I-ODO→odometruInitial, `odometruInitial<=odometruFinal`) + normalize strip/upper în
|
||
modelele Pydantic. **Eșecurile de conținut → `needs_data` (ținute, nu 422)** per masina de stări; JSON malformat → 422.
|
||
Verify: 29 teste pass (`tests/test_validation.py` per regulă + `tests/test_api.py` rutare/idempotenta).
|
||
- [x] **T4 (P1) — payload builder** ✅ 2026-06-15. `app/payload.py`: `status:"FINALIZATA"`, `sistemReparat:"null"`, fără
|
||
`tipPrestatie`, `odometruFinal`/`odometruInitial` string (initial gol → null), `prestatii:[{codPrestatie,idPrezentare:null}]`,
|
||
obs/b64Image omise când lipsesc. Verify: 10 teste (`tests/test_payload.py`), inclusiv snapshot vs exemplul oficial din contract.
|
||
- [x] **T2 (P1) — `app/worker` reconciliere** ✅ 2026-06-15. `app/reconcile.py` match pe vin+dataPrestatie+odometruFinal(int,
|
||
id maxim la duplicate) + worker: recuperare orfane (lease), reconciliere pe eroare tranzitorie/timeout înainte de re-send,
|
||
retry/backoff exponential (peste `worker_max_retries` → error+banner), re-login la token expirat. Rută monitorizare descoperită
|
||
live: `GET /prezentari/getAllPrezentariFinalizate` → `data.content` (filtrele nu merg pe test → fetch tot, match client-side).
|
||
Verify: 15 teste (`tests/test_worker_reconcile.py`) + validare LIVE (reconciliere record 68514 din finalizate reale).
|
||
- [x] **Securitate CORE (P1)** ✅ 2026-06-15. `app/security.py` (redactare creds) + `app/auth.py` (API-key per cont) +
|
||
`tools/apikey.py` (CLI emitere/rotire/revocare). Redactare: handler `RequestValidationError` care DROP-ează
|
||
`input`/`ctx` din 422 (vectorul de scurgere a parolei pe `/v1/prezentari`), filtru logging `scrub_text` pe root+uvicorn,
|
||
`password` cu `repr=False` în model. Auth: hash SHA-256 în `api_keys` (cheia în clar emisă o singură dată), header
|
||
`X-API-Key` / `Authorization: Bearer`, enforcement pe flag `AUTOPASS_require_api_key` (prod on→401, dev off→cont default
|
||
id=1; cheie prezentă invalidă→401 mereu). `account_id` real curge din cheie în ingestie + mapare. Verify: 16 teste
|
||
(`tests/test_security.py`).
|
||
- [x] **Livrare creds per-cerere (P1)** ✅ 2026-06-15. `app/crypto.py` (Fernet, cheie din `AUTOPASS_creds_key`; nesetată →
|
||
cheie efemeră la runtime). Creds RAR criptate per submission (`submissions.rar_creds_enc`) la ingestie — niciodată în
|
||
clar în DB. Worker: `AccountSessions` face login PER CONT cu creds decriptate, cache JWT 30h în memorie, ȘTERGE creds-urile
|
||
contului după primul login reușit (token-ul acoperă restul). Fallback creds `<test>` în dev. 401 creds greșite → error fără
|
||
retry; token expirat → invalidare sesiune + requeue; fără creds (restart) → requeue „indisponibile" (ROAAUTO re-trimite).
|
||
Verify: 10 teste (`tests/test_creds_delivery.py`). **Risc acceptat:** la restart token+creds se pierd → contul re-loghează
|
||
la următorul submission cu creds (degradare per modelul efemer).
|
||
- [x] **T6 (P2) — worker supravegheat** ✅ 2026-06-15. `app/worker/healthcheck.py` (probe pe heartbeat-ul din DB: beat
|
||
mai vechi de `worker_heartbeat_stale_s` → exit 1) cablat în compose ca healthcheck pe serviciul worker. Prinde worker-ul
|
||
AGĂȚAT (proces viu, beat înghețat), pe care `restart:always` (doar la EXIT) nu-l vede. Sidecar `autoheal` restartează
|
||
efectiv containerul marcat unhealthy (compose simplu doar marchează, nu restartează). Verify: 3 teste (`tests/test_deploy.py`).
|
||
- [x] **T7 (P2) — deploy** ✅ 2026-06-15. `tools/backup.py` (backup ONLINE via `Connection.backup` — WAL nu se copiază sigur
|
||
cu `cp`; `--keep N` rotește snapshot-urile) + volum SQLite persistent numit (`autopass-data`, deja în compose). `.env.example`
|
||
documentează env-urile. **Fix critic descoperit la split-ul în 2 containere:** `AUTOPASS_CREDS_KEY` trebuie PARTAJATĂ
|
||
api↔worker (altfel worker nu decriptează creds) — acum impusă în compose (`${...:?}` → fail explicit dacă lipsește).
|
||
Verify: 2 teste (`tests/test_deploy.py`).
|
||
- [x] **Dashboard** (Jinja2+HTMX) ✅ 2026-06-15. Stări explicite: empty (coadă/mapări goale cu CTA), error (needs_mapping
|
||
în editor + motiv pe submission), **RAR indisponibil** (indicator stare RAR derivat din ultimul login < 30h → coada arată
|
||
ultima stare cunoscută local, nu live), banner alertă blocate (poll 15s). Componente: status worker/RAR, editor mapări
|
||
fuzzy, **browser nomenclator**, coadă (poll 10s), **export audit CSV** (`GET /v1/audit/export?status=sent|all&date_from&date_to`,
|
||
b64Image exclus, coloană `purge_after`). Verify: 5 teste (`tests/test_dashboard.py`) + smoke live.
|
||
- [x] **`/design-review` pe UI-ul live** ✅ 2026-06-15. Regression vs baseline: cele 3 findings prior (ierarhie titluri,
|
||
font stack, tap targets) încă rezolvate. Nou + fixat: **FINDING-004** — htmx era din CDN (unpkg); gateway-ul rulează
|
||
offline → vendorizat local (`app/web/static/htmx.min.js` + mount `/static`, commit `b12be3d`). Design score B, AI slop A.
|
||
|
||
### De decis ulterior (urmărit, nu blocant)
|
||
- **[P2]** Defer criptare-at-rest + purjare 90z până după primul postPrezentare real reușit? (gold-plating vs. privacy-argument-de-adopție).
|
||
- **[P2]** Gate pe `auto_send` pentru operații nou-mapate / valori neobișnuite — un record `FINALIZATA` eronat e permanent.
|
||
|
||
## 11. Verificare (end-to-end)
|
||
|
||
1. ✅ Spike „The Assignment" (JWT 30h, b64Image opțional, tipPrestatie server-gen, R-ODO/I-ODO) — `docs/api-rar-contract.md`.
|
||
2. T1: `postPrezentare` real pe test → 200 + `data.id`.
|
||
3. `import_dbf.py --dry-run` raport corect; apoi import confirmat.
|
||
4. `docker compose up`; `/healthz` verde.
|
||
5. `POST /v1/prezentari` cu o comandă reală (test) → `submissionId`, worker trimite, apare `FINALIZATA` la RAR + dashboard.
|
||
6. Re-trimite aceeași comandă identică → același `submissionId` (idempotency), NU dublă la RAR.
|
||
7. Operație nemapată → `needs_mapping`; `prestatii` ∋ R-ODO/I-ODO fără `odometruInitial` → `needs_data`;
|
||
VIN cu O/I/Q sau dată < 2024-12-01 → respins la validare, NU ajunge la RAR.
|
||
8. Oprește RAR / forțează 5xx → submission `error`, worker reia în fereastra de re-login; pană > 30h → alertă banner.
|
||
9. Simulează răspuns pierdut → reconcilierea previne duplicatul.
|
||
10. SQLite n-are câmp parolă; după `sent`, PII criptat + `purge_after`; logurile n-au parole.
|
||
11. Teste: unit (mapare, idempotency hash, validare VIN/dată/nrInm, R-ODO→odometruInitial), integration (claim atomic,
|
||
retry/backoff, reconciliere), E2E pe endpoint test RAR.
|
||
|
||
## 12. NOT in scope (amânat, cu motiv)
|
||
|
||
- **Re-push din ROAAUTO** — scos din v1; worker + alertă acoperă. Re-introducem dacă apare pană RAR > 30h.
|
||
- **Anulare/corecție prin API** — `FINALIZATA` nu se anulează/corectează prin API; corecția = suport RAR.
|
||
- **Atașare poză odometru în ROAAUTO** — `b64Image` opțional, nu în treapta 1.
|
||
- `POST /v1/import` xlsx/csv + UX mapare coloane — treapta 2 (piață non-ROA). Motor identic, fără rescriere.
|
||
- Modelul de conturi RAR (addClient/roluri) — 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).
|
||
|
||
## 13. Open questions rămase
|
||
|
||
1. ~~Sursa pozei odometrului~~ — închis (b64Image opțional).
|
||
2. ~~`tipPrestatie` / JWT TTL~~ — închis (server-generated; 30h). Rămâne: ce valori reale acceptă `sistemReparat` în afară de `"null"`.
|
||
3. Un singur user RAR per agent economic sau mai mulți (`idUser`/`idAgent` — afectează filtrarea monitorizării).
|
||
4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client.
|
||
|
||
## 14. Decizii 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) + **reconciliere
|
||
înainte de re-send** pentru răspuns pierdut.
|
||
- **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare (defensibilitate vs privacy).
|
||
- Odometru: **strict + `needs_data`**, declanșat determinist de `R-ODO`/`I-ODO`.
|
||
- **Re-push scos din v1**; **anulare/corecție API scoase** (FINALIZATA).
|
||
- URL-urile RAR: **sursa de adevăr = VFP testat / contract verificat**, NU spec-ul vechi (typo-uri).
|
||
- Cherry-picks v1: alertă submission-uri blocate (critică), `/healthz`+`/metrics`, sugestie fuzzy mapare, export audit CSV.
|
||
|
||
---
|
||
|
||
# ANEXĂ — Rațiunea de produs & direcția SaaS
|
||
|
||
> Context de produs (din `/office-hours`), păstrat pentru deciziile de prioritizare. Nu blochează execuția treptei 1.
|
||
|
||
## Demand evidence (validat: client real + lege)
|
||
|
||
Un **client real a cerut automatizarea** introducerii prezentărilor în AUTOPASS — de aici a pornit proiectul.
|
||
Status quo înlocuit: interfața web oficială AUTOPASS, unde service-urile introduc **manual, prezentare cu prezentare**.
|
||
Obligație legală reală (L.142/2023): la fiecare prestație VIN + odometru; reparare/înlocuire odometru; operațiuni
|
||
principale la direcție, frânare, structura caroseriei/șasiului și alte sisteme de siguranță. Amenzi: info eronate
|
||
1.000–2.000 lei; manipulare odometru până la 5.000 lei / penal. Implică **toți** clienții ROA + **mii de service-uri
|
||
non-ROA** cu aceeași obligație → piață dincolo de ROA pentru un canal de import (xlsx/csv) ulterior.
|
||
|
||
## Teza de produs
|
||
|
||
*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ă. Câștigi prin **efort minim cerut service-ului**, nu prin features.
|
||
Lecția GTM (demoanaf.ro): viral prin variantă **reimaginată, simplă** a unui serviciu oficial greoi.
|
||
|
||
## 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.
|
||
- **Treapta 2 (non-ROA, web upload):** import xlsx/csv cu **mapare reținută** (map once, reuse forever) + dashboard.
|
||
Login web, fără instalare. Freemium **pe volum** (gratis ~30-40 prezentări/lună; plată peste prag). Metrica de preț =
|
||
prezentări/lună = unitatea obligatorie legal.
|
||
- **Treapta 3:** 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; (3) fiabilitatea de
|
||
conformitate (retry, monitorizare din RAR); (4) **privacy** (nu reținem datele lor) — argument de adopție.
|
||
|
||
## Adopție & praguri (cifre reale)
|
||
|
||
Clienți reali cunoscuți: **60-80** și **80-100 prezentări/lună** → ~2-4 min/prezentare manual × volum = **3-6 ore/lună**
|
||
de tastare = durerea care convertește. Prima folosire trivială (upload → mapare reținută → trimite, sub 5 min).
|
||
Gratis la volumul lor = fără decizie de achiziție. Service-urile mici = bază virală (durere mică); volumele mari =
|
||
cei mai dispuși să plătească.
|
||
|
||
## Import xlsx/csv — UX (treapta 2)
|
||
|
||
Două straturi de mapare, **ambele reținute per cont**: (1) mapare coloane (schema fișier → câmpuri canonice),
|
||
(2) mapare operații (etichete service → `codPrestatie`, cu sugestie fuzzy). Flux: upload → recunoaște coloanele →
|
||
propune maparea → **preview** (rânduri nemapate flag-uite) → „Trimite la RAR" → monitorizare.
|
||
Spectru de integrare (același backend): API → drop fișier (folder/SFTP/email-to-import) → upload manual în browser.
|
||
|
||
## Opțiuni de deploy
|
||
|
||
| Opțiune | Cost | Când |
|
||
|---|---|---|
|
||
| LXC Proxmox + Cloudflare Tunnel | 0 € | Start + teste (risc: net/curent birou) |
|
||
| VPS mic always-on (ex. Hetzner) | ~5 €/lună | Clienți reali / producție (recomandat) |
|
||
| romfast.ro / shared hosting | inclus | ⚠️ Doar landing (ASGI + worker daemon nepotrivite pe shared) |
|
||
|
||
---
|
||
|
||
## What already exists (reuse din VFP)
|
||
|
||
| Sub-problemă | Reuse |
|
||
|---|---|
|
||
| Contract RAR (login/JWT, nomenclator, postPrezentare) | `rar_autopass.prg`, `rar-forms.prg:655,720` → `rar_client.py` |
|
||
| Mapare op→codPrestatie + `auto_send` | `GetCodRarPentruOperatie` → `operations_mapping` |
|
||
| Export (oglindă treapta 2) | `btnExportExcel.Click` (`rar_advanced.prg`) |
|
||
| Migrare DBF | `import_dbf.py` citește direct cele `.DBF` |
|
||
| Timer (dacă revine re-push) | `OnAutoProcessTimer`/`nTimerHandle` (`rar-forms.prg`) — nefolosit în v1 |
|
||
</content>
|