T5 (tools/import_dbf.py): citire prestatii_rar.DBF / mapare_prestatii.DBF cu dbfread, raport dry-run (randuri valide/duplicate/goale, mapari orfane = cod necunoscut in nomenclator), --commit cu upsert idempotent in tranzactie. Dashboard: browser nomenclator, indicator stare RAR (indisponibil? derivat din ultimul login < 30h, coada arata ultima stare locala), export audit CSV (/v1/audit/export?status=sent|all&date_from&date_to, b64Image exclus, coloana purge_after pentru retentia 90z). Verify: 11 teste noi (test_import_dbf 6, test_dashboard 5), suita 111 pass, dry-run real pe DBF-urile din repo + smoke live dashboard/CSV. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
27 KiB
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ă)
git clone git@gitea.romfast.ro:romfast/rar-autopass.gitcp settings.xml.example settings.xmlși completează credențialele de test RAR (blocul<test>). NU se comite.- Citește
docs/api-rar-contract.md(contractul) și acest plan. - 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; nomenclatorGET /nomenclator/getNomenclatorPrestatii; trimiterePOST /prezentari/postPrezentare. Bază testhttps://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?.tipPrestatieNU 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], TZEurope/Bucharest.b64Image: opțional; dacă prezent, base64 valid.odometruInitial:nullnormal; obligatoriu dacăprestatii∋R-ODOsauI-ODO;odometruInitial <= odometruFinal.odometruFinal: trimis ca string (per exemplul oficial).sistemReparat: trimis mereu; default"null"în v1.- Normalizează
vin/nrInmatricularecu.strip().upper()înainte de regex (DBF-urile pot avea spații).
- Anulare/corecție prin API: NU există. API e strict INSERT
FINALIZATA;FINALIZATAnu se anulează (doarSALVATA, pe care API nu le produce). Corecția = emailsuport.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)
- 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/prezentariNU se loghează niciodată șipasswordse scrubează din excepții/APM. - (SCOS din v1: anulare/corecție prin API. 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/getAllPrezentariFinalizate.httpx+ retry/backoff. URL-uri din VFP testat (verificate live). (NU portăm anulare/corecție.) - Worker
app/worker— proces propriu supravegheat (NU taskasyncioîn uvicorn — altfel worker mort lasă containerul „sănătos"), sub Dockerrestart: 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 peVIN + dataPrestatie + odometruFinal; dacă găsește → marcheazăsentcuidPrezentaregăsit, NU re-trimite. UNIQUE pesubmissionsNU acoperă acest caz. - Lease/timeout pe
sendingorfane (worker mort mid-POST) → recuperat de reconciliere. b64Image(opțional) mare → BLOB/path pe disc, nu RAM.
- 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. - SQLite (WAL) — un fișier
.dbpe 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_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.
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.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. - ROAAUTO = pur expeditor în v1. Re-push scos; timer-ul
OnAutoProcessTimerrămâne disponibil dacă revine. - Fără atașare poză (
b64Imageopț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ă:
- T1 (P1, BLOCANT) — verificare contract live. ✅ 2026-06-15.
postPrezentarereal pe test (recorddata.id=68514). Capturate: format eroaredata:[{field,message}]+ 3 mesaje exacte (VIN O/I/Q, dată veche, dată viitoare), forma răspuns success,idPrezentare==id,idAgentserver-side,sistemReparat:"null"acceptat,b64Image/odometruInitialomise OK. Descoperire: WAF cereUser-Agent(altfel 403). Toate detaliile îndocs/api-rar-contract.md. - T5 (P1) —
tools/import_dbf.py✅ 2026-06-15.dbfread→ raport (rânduri valide, duplicate, goale, mapări orfane = cod necunoscut în nomenclator) peprestatii_rar.DBF(20 coduri) +mapare_prestatii.DBF(gol în arhivă). Default dry-run;--commitscrie idempotent (upsert penomenclator_rarPK +operations_mappingUNIQUE), tranzacțieBEGIN 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. - Schelet repo — ✅ 2026-06-15.
app/api/v1,app/rar_client.py(cu User-Agent),app/worker,app/web, SQLite (WAL),Dockerfile+docker compose,/healthzverde. Verificat: login prin client OK, nomenclator 18 coduri, worker heartbeat →worker_alive=True, enqueue + dedup idempotency funcționale. - 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.pyper regulă +tests/test_api.pyrutare/idempotenta). - T4 (P1) — payload builder ✅ 2026-06-15.
app/payload.py:status:"FINALIZATA",sistemReparat:"null", fărătipPrestatie,odometruFinal/odometruInitialstring (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. - T2 (P1) —
app/workerreconciliere ✅ 2026-06-15.app/reconcile.pymatch 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 (pesteworker_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). - 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: handlerRequestValidationErrorcare DROP-eazăinput/ctxdin 422 (vectorul de scurgere a parolei pe/v1/prezentari), filtru loggingscrub_textpe root+uvicorn,passwordcurepr=Falseîn model. Auth: hash SHA-256 înapi_keys(cheia în clar emisă o singură dată), headerX-API-Key/Authorization: Bearer, enforcement pe flagAUTOPASS_require_api_key(prod on→401, dev off→cont default id=1; cheie prezentă invalidă→401 mereu).account_idreal curge din cheie în ingestie + mapare. Verify: 16 teste (tests/test_security.py). - Livrare creds per-cerere (P1) ✅ 2026-06-15.
app/crypto.py(Fernet, cheie dinAUTOPASS_creds_key; nesetată → cheie efemeră la runtime). Creds RAR criptate per submission (submissions.rar_creds_enc) la ingestie — niciodată în clar în DB. Worker:AccountSessionsface 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). - T6 (P2) — worker supravegheat ✅ 2026-06-15.
app/worker/healthcheck.py(probe pe heartbeat-ul din DB: beat mai vechi deworker_heartbeat_stale_s→ exit 1) cablat în compose ca healthcheck pe serviciul worker. Prinde worker-ul AGĂȚAT (proces viu, beat înghețat), pe carerestart:always(doar la EXIT) nu-l vede. Sidecarautohealrestartează efectiv containerul marcat unhealthy (compose simplu doar marchează, nu restartează). Verify: 3 teste (tests/test_deploy.py). - T7 (P2) — deploy ✅ 2026-06-15.
tools/backup.py(backup ONLINE viaConnection.backup— WAL nu se copiază sigur cucp;--keep Nrotește snapshot-urile) + volum SQLite persistent numit (autopass-data, deja în compose)..env.exampledocumentează env-urile. Fix critic descoperit la split-ul în 2 containere:AUTOPASS_CREDS_KEYtrebuie PARTAJATĂ api↔worker (altfel worker nu decriptează creds) — acum impusă în compose (${...:?}→ fail explicit dacă lipsește). Verify: 2 teste (tests/test_deploy.py). - 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. Rămas:/design-reviewpe UI-ul live (cosmetic, neblocant).
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_sendpentru operații nou-mapate / valori neobișnuite — un recordFINALIZATAeronat e permanent.
11. Verificare (end-to-end)
- ✅ Spike „The Assignment" (JWT 30h, b64Image opțional, tipPrestatie server-gen, R-ODO/I-ODO) —
docs/api-rar-contract.md. - T1:
postPrezentarereal pe test → 200 +data.id. import_dbf.py --dry-runraport corect; apoi import confirmat.docker compose up;/healthzverde.POST /v1/prezentaricu o comandă reală (test) →submissionId, worker trimite, apareFINALIZATAla RAR + dashboard.- Re-trimite aceeași comandă identică → același
submissionId(idempotency), NU dublă la RAR. - 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. - Oprește RAR / forțează 5xx → submission
error, worker reia în fereastra de re-login; pană > 30h → alertă banner. - Simulează răspuns pierdut → reconcilierea previne duplicatul.
- SQLite n-are câmp parolă; după
sent, PII criptat +purge_after; logurile n-au parole. - 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 —
FINALIZATAnu se anulează/corectează prin API; corecția = suport RAR. - Atașare poză odometru în ROAAUTO —
b64Imageopțional, nu în treapta 1. POST /v1/importxlsx/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
Sursa pozei odometrului— închis (b64Image opțional).— închis (server-generated; 30h). Rămâne: ce valori reale acceptătipPrestatie/ JWT TTLsistemReparatîn afară de"null".- Un singur user RAR per agent economic sau mai mulți (
idUser/idAgent— afectează filtrarea monitorizării). - 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 deR-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 |