Files
rar-autopass/docs/plans/plan.md
Claude Agent fa65e1da2e docs(plan): marcheaza /design-review done (FINDING-004 htmx local)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:59:25 +00:00

359 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

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