docs: sistem ROADMAP unic (progres + proces) + template PRD

Inlocuieste planurile vechi (consolidate/realizate) cu un singur
docs/ROADMAP.md: dashboard de progres (Treapta 1+2 DONE LIVE, Etapa 3
TODO) + proces de dezvoltare embedded (PLAN separat de EXECUTE/VERIFY
pe sesiuni, PRD per livrabila cu stories atomice, agent team, bootstrap
reluabil din starea PRD).

- adauga docs/prd/TEMPLATE-prd.md (schelet PRD)
- sterge docs/plans/plan.md (Treapta 1 realizat), plan-treapta2.md
  (Treapta 2 realizat), docs/CONTEXT.md (snapshot neactual)
- actualizeaza referintele in README.md si api-rar-contract.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-17 07:21:20 +00:00
parent c38807d88c
commit fbf82622b6
7 changed files with 332 additions and 966 deletions

View File

@@ -1,120 +0,0 @@
# Context proiect — Gateway RAR AutoPass (migrare ROAAUTO din VFP în Web API)
> Fișier de continuitate între sesiuni. Citește-l înainte de a relua lucrul.
> Ultima actualizare: 2026-06-15.
>
> ⚠️ **SURSA DE ADEVĂR pentru contractul RAR = `docs/api-rar-contract.md`** (verificat live).
> Acolo unde planurile (`docs/plans/*`) diferă, contractul are dreptate. Vezi „Corecții față de planuri".
## Reluare pe alt calculator (portabil)
Tot ce ai nevoie pentru a continua e în acest repo (acest fișier + `docs/plans/`).
```
git clone git@gitea.romfast.ro:romfast/rar-autopass.git
```
Remote-ul Gitea (org `romfast`) merge prin SSH: `git@gitea.romfast.ro:romfast/<repo>.git`.
Push-to-create e activ (un `git push -u origin main` creează repo-ul automat).
După clonare: copiază `settings.xml.example``settings.xml` și completează credențialele
(NU se comite). Apoi citește mai jos.
## Ce este acest repo
Arhiva **bazei Visual FoxPro** existente (clasa `RarAutoPass`, ROAAUTO) care declară
prestațiile de service la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024), **plus**
planurile pentru rescrierea ca **Web API central (Python / FastAPI)**.
Codul VFP de aici este **punctul de plecare / sursa de adevăr de contract** pentru
versiunea web. Nu se mai dezvoltă; se portează.
## Stare actuală (iunie 2026)
- Integrarea VFP **funcționează** și e **testată pe endpoint-ul de test RAR**, dar
**nu e pusă la clienți** încă.
- Comunică direct cu RAR prin `MSXML2.ServerXMLHTTP` din `rar_autopass.prg` / `rar-forms.prg`.
- Maparea operație→`codPrestatie` în `mapare_prestatii.DBF`; nomenclator în `prestatii_rar.DBF`;
jurnal în `rar_log.DBF`; credențiale (în clar) în `settings.xml`.
- ⚠️ `settings.xml` conținea o **parolă de test reală** (`marius.mutu@romfast.ro`).
E **exclus din git** (`.gitignore`) și înlocuit cu `settings.xml.example`.
**De rotit parola** — a fost expusă în istoricul SVN vechi.
## Fișiere-cheie (VFP) și ce reutilizăm
| Fișier | Rol | Se portează în |
|---|---|---|
| `rar_autopass.prg` | clasa `RarAutoPass`: login+JWT, nomenclator, postPrezentare, cancel | `app/rar_client.py` |
| `rar-forms.prg` | UI + timer auto-process (`OnAutoProcessTimer`) | logica → worker; timer → re-push ROAAUTO |
| `export_comenzi.prg` | citește comenzi/operații, construiește payload | client subțire: `POST /v1/prezentari` |
| `rar_advanced.prg` | export Excel (oglindă pentru treapta 2) | referință import xlsx/csv |
| `mapare_prestatii.DBF` | cod_op_service → codPrestatie | `operations_mapping` (via `tools/import_dbf.py`) |
| `prestatii_rar.DBF` | nomenclator {codPrestatie, numePrestatie} | `nomenclator_rar` (via `tools/import_dbf.py`) |
| `Documentatie Serviciu AutoPass_Final.txt`, `Document informativ RAR- Autopass.txt` | spec oficial RAR (vechi, are typo-uri) | înlocuit de `docs/api-rar-contract.md` |
| `docs/api-rar-documentatie-oficiala.md` | răspuns oficial programatori RAR | sintetizat în `docs/api-rar-contract.md` |
| `docs/api-rar-contract.md` | **contract verificat live — sursa de adevăr** | referință pentru `app/` |
## Planul (în `docs/plans/`)
**`plan.md`** — **planul unic executabil** (sursă unică). Consolidează designul de produs +
implementarea, aliniat la contractul verificat live. Conține: arhitectură, reguli contract,
validare, mașina de stări, componente, securitate, failure modes, **Roadmap de execuție (T1-T7 +
pașii rămași)**, verificare E2E, NOT in scope, decizii blocate, și anexa de produs/SaaS.
> Continuă cu **`docs/plans/plan.md`** → secțiunea „Roadmap de execuție". Pasul blocant următor = **T1**
> (un `postPrezentare` real pe test). Fostele `plan-eng-review.md` + `plan-design-review.md` au fost
> consolidate în `plan.md` (review-urile eng + design au intervenit pe el).
## Arhitectura țintă (rezumat)
```
ROAAUTO (VFP, client subțire) ──HTTPS──▶ Gateway FastAPI (central, 1 container)
trimite comanda + creds RAR API: validare → mapare op→cod → enqueue (PII criptat)
◀── {submissionId, status} ─────────────┘
WORKER (proces separat): claim atomic → login RAR → postPrezentare → retry
Dashboard (Jinja2+HTMX): monitorizare live din RAR + stare coadă + editor mapări
ROAAUTO (timer) ──▶ GET /v1/prezentari?status=error → re-push (durabilitate pene lungi)
```
Stack: Python/FastAPI + SQLite (WAL) + httpx. Deploy: LXC Proxmox + Cloudflare Tunnel (start) → VPS (~5€/lună).
Open-source pe github.com/romfast, AGPL-3.0 (⚠️ decide CLA din ziua 1 dacă vrei dual-license).
## „The Assignment" (spike) — REZOLVAT în mare parte (2026-06-15)
Detalii complete în `docs/api-rar-contract.md`. Rezumat:
1. **JWT TTL = 108000s = 30 ORE** (nu „scurt"). → worker-ul singur poate relua peste pene lungi;
re-push ROAAUTO devine secundar. Reconsideră arhitectura de robustețe din planuri.
2. **`b64Image` (poza) = OPȚIONALĂ** (confirmat oficial). Open question „sursa pozei" închisă.
3. **`tipPrestatie` = generat de server** (`GENERIC`), nu se trimite. `sistemReparat` se trimite
(poate fi `"null"`); valorile reale rămân de probat.
4. **`needs_data` determinist:** `odometruInitial` obligatoriu doar dacă `prestatii` conține
`R-ODO` sau `I-ODO`.
Rămas: **un singur `postPrezentare` real pe test** (mesaje de eroare exacte + `data.id`). Vezi contract.
## De făcut după spike (din `plan.md`, Roadmap de execuție + Verificare)
1. `tools/import_dbf.py --dry-run` pe `mapare_prestatii.DBF` + `prestatii_rar.DBF` (raport întâi, apoi import).
2. Schelet repo: `app/api/v1`, `app/rar_client.py`, `app/worker`, `app/web`, SQLite (WAL), `docker compose up`, `/healthz` verde.
3. `POST /v1/prezentari` cu o comandă reală (test) → worker trimite → `FINALIZATA` la RAR + în dashboard.
4. Test idempotency (re-trimitere identică → același `submissionId`, fără dublu la RAR).
5. `needs_mapping` / `needs_data` (nu se trimite incomplet); `error` + re-push.
6. Verifică: SQLite fără câmp parolă; după `sent` PII criptat + `purge_after`; loguri fără parole.
7. Teste: unit (mapare, hash idempotency, validare odometru), integration (claim atomic, retry), E2E test RAR.
## Decizii deja 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).
- **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare (defensibilitate vs privacy).
- Odometru repair: **strict + stare `needs_data`** (nu trimite incomplet).
- Cherry-picks în v1: alertă submission-uri blocate, `/healthz`+`/metrics`, sugestie fuzzy mapare, export audit CSV.
- URL-urile RAR: **sursa de adevăr = VFP testat**, NU spec-ul (are typo-uri de copy/paste).
## Open questions rămase (actualizat 2026-06-15)
1. ~~Sursa pozei odometrului~~**închis** (poză opțională, vezi contract).
2. ~~`tipPrestatie` valori~~**închis pentru request** (generat de server). Rămâne: ce valori
reale acceptă `sistemReparat` (în afară de `"null"`).
3. Un singur user RAR per agent economic sau mai mulți (afectează `idUser`/`idAgent` / filtrare monitorizare).
4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client.
5. Anulare/corecție: **nu există flux API** (records `FINALIZATA`); corecția = email suport RAR. De
reflectat în UX dashboard (nu promite anulare).

261
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,261 @@
# AutoPass — Roadmap & Proces de Dezvoltare
> **Sursa unica de progres + procesul de lucru.** O sesiune noua are nevoie doar de promptul:
> *"Citeste docs/ROADMAP.md si continua roadmap-ul [optional: livrabila X.Y]."*
> Sesiunea isi detecteaza singura faza (§5.7), planifica SAU executa, verifica si inchide —
> cu doua porti umane: aprobarea PRD-ului si confirmarea commit-ului.
>
> Contractul RAR (sursa de adevar de contract) = `docs/api-rar-contract.md`. Acolo unde un plan
> difera de contract, **contractul are dreptate**.
>
> Status fara emoji (preferinta proiect): **TODO** neinceput · **WIP** in lucru · **DONE** gata ·
> **BLOCAT** blocat de o dependenta · **AMANAT** deferat/taiat cu motiv.
---
## 1. Context
Gateway central care declara prestatiile de service-auto la **RAR AUTOPASS** (Legea 142/2023,
OM 210/2024), portat din clasa Visual FoxPro `RarAutoPass` (ROAAUTO). Stack: **Python/FastAPI +
SQLite (WAL) + httpx + Jinja2/HTMX**. Un container (API) + un proces separat (worker).
Doua canale de intrare, ambele **LIVE pe endpoint-ul de test RAR**:
- **Treapta 1** — canal API (`POST /v1/prezentari`) pentru ROAAUTO / soft propriu.
- **Treapta 2** — import xlsx/csv + mapare coloane + UI web (HTMX) pentru service-uri non-ROA.
---
## 2. Arhitectura (rezumat)
```
Canal API (ROAAUTO) ─┐
Upload web (xlsx/csv) ─┴─▶ Gateway FastAPI ─▶ validare → mapare op→cod → enqueue (PII criptat)
WORKER (proces separat): claim atomic → login RAR → postPrezentare → retry
Dashboard (Jinja2+HTMX) ◀───────┴── monitorizare live RAR + coada + editor mapari + audit CSV
```
Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e terminal la RAR
(fara anulare/corectie prin API); idempotency = hash de continut server-side; JWT TTL = 30h.
---
## 3. Stadiu Implementare (dashboard)
> **Singurul loc din document care se modifica pe parcurs.** Detaliile NU intra aici — stau in
> PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata:
> schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare".
**Ultima actualizare**: 2026-06-17 — Treapta 1 + Treapta 2 LIVE pe test; urmatorul focus = Etapa 3 (multi-cont).
### Etapa 1 — Canal API ROAAUTO (Treapta 1)
| # | Livrabila | Status | Data | Detalii |
|---|-----------|--------|------|---------|
| 1.1 | Gateway complet: validare + mapare + coada + worker + reconciliere + dashboard | DONE | 2026-06-15 | E2E LIVE prin gateway: `POST /v1/prezentari` → worker → `FINALIZATA` la RAR test (`idPrezentare=68516`). Cod `app/`, 279 teste. |
### Etapa 2 — Import xlsx/csv + UI web (Treapta 2)
| # | Livrabila | Status | Data | Detalii |
|---|-----------|--------|------|---------|
| 2.1 | Parser xlsx/csv 2-treceri + staging criptat (`POST /v1/import`) | DONE | 2026-06 | `app/import_parse.py`; encoding RO, delimiter sniff, coercion, robustete sheet/merged/footer |
| 2.2 | Creds RAR durabile per-cont + worker re-login fallback | DONE | 2026-06 | `accounts.rar_creds_enc`, gate purjare worker |
| 2.3 | `batch_id` scope `reresolve_account` (inchide R1 bulk-send) | DONE | 2026-06 | `app/mapping.py` |
| 2.4 | Mapare coloane + semnatura + detectie drift + fuzzy | DONE | 2026-06 | `app/api/v1/import_router.py` |
| 2.5 | Preview 6 stari + canonicalize partajat + gate HARD + atestare | DONE | 2026-06 | `duplicate_in_file`, `already_sent`, `needs_review`, `import_attestations`, TOCTOU `ON CONFLICT` |
| 2.6 | UI web upload HTMX (drop → mapare → preview → confirma) | DONE | 2026-06 | `app/web/templates/_upload.html`, `_mapcoloane.html`, `_preview_import.html` |
| 2.7 | Export randuri esuate CSV + job purjare `purge_after` + E2E | DONE | 2026-06 | `export_failed_rows`, `tests/test_import_e2e.py` |
### Etapa 3 — Multi-cont / self-onboarding (URMATORUL FOCUS)
| # | Livrabila | Status | Data | Detalii |
|---|-----------|--------|------|---------|
| 3.1 | Creare cont nou (endpoint/tool dedicat) | TODO | | Azi doar prin `INSERT` SQL manual |
| 3.2 | Filtrare pe cont a GET-urilor de listare | TODO | | `GET /v1/prezentari`, `/v1/nomenclator`, `/v1/audit/export` sunt globale acum |
| 3.3 | Self-onboarding web (login email+parola → emite cheie) | TODO | | Nu exista ruta `/login`; cheile se emit programatic (`app/auth.py`) |
### Etapa 4 — Viitor (Treapta 3)
| # | Livrabila | Status | Data | Detalii |
|---|-----------|--------|------|---------|
| 4.1 | Mapare AI / conector MCP (sugestie peste fuzzy) | TODO | | |
| 4.2 | Editare/anulare prezentari trimise | BLOCAT | | `FINALIZATA` terminal la RAR — fara flux API; corectia = suport RAR |
### Amanat / taiat (cu motiv)
| Livrabila | Status | Motiv |
|-----------|--------|-------|
| Drop-fisier SFTP / email-to-import | AMANAT | Valideaza intai upload-ul manual (wedge-ul real), apoi re-evalueaza |
| Contor volum + prag freemium | AMANAT | Metrici de pret inainte sa existe useri; trivial de adaugat post-validare |
| Billing complet (Stripe etc.) | AMANAT | Dupa validarea pragului de volum |
---
## 4. Open questions / riscuri acceptate
- **Un cont = un agent RAR sau mai multi** — afecteaza maparea creds in UI + filtrarea monitorizarii.
Relevant pentru Etapa 3.
- **Valori reale `sistemReparat`** — azi se trimite `"null"`; ce alte valori accepta RAR ramane de probat.
- **R4 (acceptat constient):** creds RAR durabile-at-rest (Treapta 2) → la scurgerea cheii Fernet toate
parolele sunt decriptabile (vs. doar in-flight pe canalul API). Mitigare: rotatie cheie + redactare loguri.
---
## 5. Proces de implementare
### 5.1 Principii
1. **PLAN separat de EXECUTE/VERIFY, pe sesiuni distincte.** Planificarea consuma mult context
(model puternic) si produce **PRD-ul** ca artefact de predare. Executia reia intr-o **sesiune noua**
bazat doar pe PRD — nu pe transcriptul planificarii. **Starea persista in PRD**, nu in conversatie.
2. **TDD ca contract:** testul scris ÎNAINTE = forma executabila a criteriilor de acceptare. Un worker
cu test rosu clar nu poate declara fals victoria — testul decide.
3. **Story atomic = unitate de paralelizare SI de rollback:** un story = un commit logic = un worker =
un test-suite verde.
4. **VERIFY in context curat:** un subagent dedicat care primeste DOAR PRD-ul + instructiunile §5.6,
NU transcriptul executiei (altfel mosteneste partinirea de confirmare a celui care a implementat).
5. **Single writer:** doar sesiunea lead scrie in PRD, ROADMAP si git. Workerii scriu doar cod si teste.
6. **E2E pe canalul real:** un flux se verifica prin canalul lui (upload web prin browser, API prin
`POST /v1/prezentari`), nu doar prin apel intern. RAR test = endpoint real.
### 5.2 Cele 4 faze (separabile pe sesiuni)
```
SESIUNE A (Opus/Fable) SESIUNE B (lead + agent team Sonnet)
┌─────────────────────┐ ┌──────────────────────────────────────────────────┐
│ FAZA 1: PLAN │ PRD │ FAZA 2: EXECUTE FAZA 3: VERIFY FAZA 4: CLOSE │
│ citeste ROADMAP+cod │ aprobat │ TeamCreate + subagent context /code-review │
│ scrie PRD (stories │ ───────▶ │ workeri Sonnet curat: pytest + writeback dash │
│ atomice + teste) │ (in PRD) │ TDD per story E2E RAR test propune commit │
│ plan-reviews gstack │ │ lead bifeaza PRD raport in PRD │
│ ▼ POARTA UMANA │ │ ▼ POARTA UMANA │
│ aprobare PRD │ │ confirmare commit│
└─────────────────────┘ └──────────────────────────────────────────────────┘
```
PLAN poate rula singur intr-o sesiune (scump, model puternic) si se opreste la poarta de aprobare.
EXECUTE → VERIFY → CLOSE reiau intr-o sesiune noua din PRD-ul aprobat. Pot fi si inlantuite in
aceeasi sesiune daca vrei — starea din PRD le face reluabile oricum.
### 5.3 PRD per livrabila
- **1 PRD per livrabila** din dashboard: `docs/prd/prd-X.Y-<slug>.md` (skill `/prd`).
- Prima linie dupa titlu: `**Stare**: draft` (vezi §5.7 pentru tranzitii).
- Contine obligatoriu: obiectiv, **Non-Goals** (anti scope-creep), stories atomice (§5.4),
riscuri, intrebari deschise (rezolvate cu utilizatorul ÎNAINTE de executie), graful de valuri.
- PRD-ul **nu repeta** strategia/contractul — le linkeaza (`docs/api-rar-contract.md`, acest ROADMAP).
- **Review-uri de plan** (aplicate IN PRD inainte de cod): `/plan-ceo-review` (valoare/scope) +
`/plan-eng-review` (fezabilitate/teste) — obligatorii; `/plan-design-review` — doar daca atinge UI.
### 5.4 Story atomic (template)
```markdown
### US-003: <titlu scurt>
**Ca** <rol> **vreau** <capabilitate> **pentru ca** <motiv>.
- **Depinde de**: US-001
- **Fisiere**: `app/<modul>.py`, `app/web/templates/<x>.html` (~N fisiere)
- **Test intai (RED)**: `tests/test_<x>.py``test_<caz_1>`, `test_<caz_2>`
- **Acceptance criteria**:
- [ ] <criteriu testabil 1>
- [ ] <criteriu testabil 2>
- **Verificare E2E**: <browser HTMX / POST /v1/prezentari pe RAR test>
```
Testul de atomicitate: (a) poate un Sonnet sa-l termine intr-o singura sesiune, cu teste, fara sa
intrebe nimic? daca nu → sparge-l. (b) lasa sistemul **functional** daca e ultimul story? (c) `Fisiere`
+ `Depinde de` complete (decid ce se paralelizeaza). (d) backend + UI pentru acelasi comportament = **2 stories**.
### 5.5 Executie — agent team (lead orchestreaza, NU scrie cod)
- `TeamCreate` creeaza echipa + lista de task-uri partajata; `Agent` cu `team_name` + `name` +
`model: sonnet` spawneaza **teammates adresabili**. Lead-ul ii coordoneaza prin `SendMessage`
(clarificari/re-directionare la cald, fara re-spawn) si `TaskUpdate` (atribuire/inchidere task).
- **Valuri (waves):** lead-ul grupeaza stories pe graful de dependente. Val 1 = stories fara
`Depinde de` si fara fisiere comune → teammates paraleli. Val 2 = deblocate de Val 1. Etc.
- **Reguli hard:** max 2-3 teammates simultan (server dev + SQLite partajate); **fisiere comune = nu
paralel** (sau `isolation: worktree` + merge de catre lead); teammates **nu** comit, nu scriu in
PRD/ROADMAP, nu pornesc/opresc servere, nu rezolva ambiguitati singuri (escaladeaza la lead).
- **Raport teammate** (prin `SendMessage`): test RED citat → implementare → test GREEN citat →
fisiere atinse → abateri; apoi `TaskUpdate``completed`.
- Dupa fiecare val: lead-ul ruleaza regresia (`python3 -m pytest -q`) si bifeaza stories in PRD.
- Livrabile mici pot rula **fara TeamCreate** (un singur worker sau lead direct) — dar PRD-ul,
verificarea in context curat si writeback-ul raman obligatorii.
### 5.6 VERIFY (subagent cu context curat)
Lead-ul spawneaza un subagent dedicat care primeste DOAR PRD-ul + aceste instructiuni, NU transcriptul:
> Esti verificator independent pentru livrabila {X.Y} (PRD: `docs/prd/prd-{X.Y}-<slug>.md`).
> Citeste PRD-ul; NU porni de la premisa ca implementarea e corecta.
> 1. Suita: `python3 -m pytest -q` — verde (citeaza output-ul).
> 2. Criteriile de acceptare ale fiecarui story din PRD.
> 3. E2E pe canalul atins: UI web → `./start.sh test both --send`, browser pe `http://localhost:8000/`
> (Playwright MCP sau `/browse`), fluxul din PRD; canal API → `POST /v1/prezentari` pe RAR test.
> 4. **Regresia de aur:** fluxul existent nu are voie sa se strice — `POST /v1/prezentari` (sau
> import → commit) → worker → `FINALIZATA` la RAR test, vizibil in dashboard (`./start.sh test finalizate`).
> 5. Raport PASS/FAIL per criteriu cu dovezi. FAIL-urile NU le repari (`/qa-only`, nu `/qa`) — le documentezi.
Lead-ul scrie raportul in PRD ca `## Raport VERIFY`. Toate PASS → CLOSE. Exista FAIL → inapoi la
EXECUTE (stories de fix), apoi VERIFY din nou cu subagent NOU.
### 5.7 Bootstrap sesiune (detectie faza din starea PRD)
Starea persista in PRD (prima linie dupa titlu), actualizata de lead la fiecare tranzitie:
`**Stare**: draft → aprobat → in-executie → verify-pass → inchis`. Asta face procesul reluabil
din orice sesiune noua — starea e in fisiere, nu in conversatie.
| Stare gasita | Faza de pornire |
|---|---|
| nu exista `docs/prd/prd-{X.Y}-*.md` | PLAN |
| PRD `**Stare**: draft` | PLAN (reia stories/review-uri) |
| PRD `**Stare**: aprobat` / `in-executie`, stories nebifate | EXECUTE |
| toate stories bifate, fara `## Raport VERIFY` cu PASS | VERIFY |
| `## Raport VERIFY` = PASS | CLOSE |
| PRD `**Stare**: inchis` | livrabila gata — alege urmatoarea din dashboard |
La bootstrap: daca utilizatorul a numit `{X.Y}`, aceea e; altfel prima `WIP` din dashboard, apoi
prima `TODO` in ordinea etapelor. **Confirma alegerea cu utilizatorul** — singura intrebare permisa la start.
### 5.8 CLOSE + porti umane
1. `/code-review` pe diff-ul livrabilei. Probleme → fix acum (worker) sau story nou (inapoi in PRD).
2. **Writeback:** in §3 dashboard — status `DONE` + data + link PRD; actualizeaza "Ultima actualizare".
PRD: `**Stare**: inchis`.
3. **POARTA UMANA:** propune commit-ul (mesaj conventional commits, fara emoji; vezi git rules) si
ASTEAPTA confirmarea explicita — niciodata commit automat.
### 5.9 Anti-patterns (opreste-te daca observi)
| Anti-pattern | Corectia |
|---|---|
| Teammate "termina" fara output de test citat | Raportul e invalid — cere dovezi prin `SendMessage` |
| Test scris DUPA implementare ca formalitate | Nu e TDD — testul e contractul; refa bucla |
| Lead-ul scrie cod "ca e mai rapid" | Pierde orchestrarea; spawn teammate chiar si pt un fix mic |
| Stories paralele care ating acelasi fisier | Re-planifica valurile sau worktree + merge de catre lead |
| PRD modificat in executie fara re-review | Schimbare de scope → inapoi la PLAN |
| Lead verifica singur, fara subagent VERIFY | Partinire de confirmare — VERIFY cere context proaspat |
| Continua peste poarta umana | Aprobarea PRD si confirmarea commit sunt ale utilizatorului — opreste-te |
---
## 6. Skill-uri in proces
| Faza | Skill | Rol |
|------|-------|-----|
| PLAN | `/prd` | genereaza PRD-ul (`docs/prd/prd-X.Y-<slug>.md`) |
| PLAN | `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review` | review de plan (primele 2 obligatorii; design doar UI) |
| EXECUTE | `/investigate` | bug neinteles raportat de worker — root cause, nu patch orb |
| VERIFY | `/browse`, `/qa-only` | browser headless / QA doar-raport (nu repara — pastreaza independenta) |
| CLOSE | `/code-review` | review pe diff-ul livrabilei |
| oricand | `/learn` | salveaza gotcha-uri in `.claude/rules/` (nu in acest doc) |
---
## 7. Intretinere
Acest document se actualizeaza la **schimbari de proces** (nu per livrabila) si la **dashboard**
(la fiecare livrabila terminata — §3). Lectiile operationale (gotchas, pattern-uri descoperite in
executie) merg in `.claude/rules/` via `/learn`, nu aici.

View File

@@ -1,7 +1,7 @@
# Contract RAR AUTOPASS — sursa de adevăr (verificat live)
> **Acesta este documentul autoritativ pentru contractul API RAR AUTOPASS.**
> Înlocuiește presupunerile din `docs/plans/*` acolo unde diferă. Dacă un plan
> Înlocuiește presupunerile din planurile vechi acolo unde diferă. Dacă un plan
> contrazice acest fișier, **acest fișier are dreptate**.
>
> Surse:
@@ -240,7 +240,7 @@ Fiecare item din `content` (live):
> Reconciliere (T2): înainte de re-send pe un rând `sending`, GET finalizate, match pe
> `vin + dataPrestatie + odometruFinal(int)`; dacă există → marchează `sent` cu id-ul găsit, NU re-trimite.
## Corecții față de `docs/plans/*` (citește înainte de a refolosi planurile)
## Corecții față de planurile inițiale (context istoric)
1. **JWT „scurt" → de fapt 30 de ORE.** Planurile (`plan-design-review` §„Gestiunea credențialelor",
`plan-eng-review` §worker) presupun JWT scurt și mută durabilitatea pe re-push din ROAAUTO

View File

@@ -1,474 +0,0 @@
# Plan Treapta 2 — Import xlsx/csv + mapare coloane (canal non-ROA)
> **Plan executabil, post-review CEO (SELECTIVE EXPANSION).** Continuă `plan.md` (Treapta 1 = LIVE).
> Acoperă funcționalitățile de integrare care AZI nu există: upload fișier, mapare coloane,
> spectru de integrare. Motor identic cu Treapta 1 (mapare op→cod + coadă + worker + monitorizare).
> Ultima actualizare: 2026-06-16. Review: `/plan-ceo-review` + voce externă (subagent independent).
## 1. Problema (de ce acum)
Treapta 1 servește clienții ROA prin ROAAUTO (API JSON). Dar obligația legală (L.142/2023)
apasă pe **mii de service-uri non-ROA** care AZI introduc manual în interfața web AUTOPASS,
prezentare cu prezentare (2-4 min × 60-100/lună = 3-6 ore/lună de tastare). Nu au ROAAUTO și nu
vor scrie cod. Au deja datele în Excel/export din propriul soft.
**Rezultat țintă:** un service non-ROA, fără instalare, încarcă un fișier (xlsx/csv), mapează
coloanele o singură dată (reținut), vede preview cu rândurile cu probleme flag-uite, confirmă
explicit, apasă „Trimite la RAR" și prezentările apar `FINALIZATA`. A doua oară: drop fișier → trimite.
## 2. Ce există deja (reuse, NU se rescrie)
| Sub-problemă | Reuse din Treapta 1 | Atenție la review |
|---|---|---|
| Validare conținut (VIN/dată/odometru/nrInm) | `app/validation.py` | OK, se compune cu batch |
| Mapare operație→codPrestatie + fuzzy | `app/mapping.py`, `operations_mapping` | ⚠️ `reresolve_account` e account-GLOBAL (vezi Risc R1) |
| Coadă + idempotency | `submissions`, `app/idempotency.py` | ⚠️ cheia exclude obs/op-denumire (vezi 3.4) |
| Worker login RAR + postPrezentare + retry | `app/worker`, `app/rar_client.py` | ⚠️ purjează creds (vezi 3.6, decizie D4) |
| Reconciliere anti-duplicat | `app/reconcile.py` | OK |
| Monitorizare + audit CSV | `/v1/prezentari`, `/v1/audit/export`, dashboard | OK |
| Auth API-key per cont | `app/auth.py` | OK |
| Criptare PII (Fernet) | `app/crypto.py` | refolosit pt. `import_rows` |
Nou = **doar stratul de INTRARE** (parsare + mapare coloane + preview) + **un fix de model creds pe web**.
## 3. Funcționalități noi (scope confirmat la review)
### 3.1 Upload fișier — `POST /v1/import`
- `multipart/form-data`: `.xlsx`, `.xls`, `.csv`. Encoding: UTF-8 + **fallback `cp1250`/`latin2` (RO)** + BOM.
- Parsare: `openpyxl` (xlsx) / `csv` stdlib. Limită (ex. 5 MB / ~5000 rânduri) → semnal explicit, nu trunchiere tăcută.
- **openpyxl `read_only=True` streaming (Eng#6):** `load_workbook(read_only=True, data_only=True)`; verifică
`max_row`/dimensiune ÎNAINTE de parsare → `FileTooLarge` fără parse parțial. Memorie marginală, rămâne sincron
(acceptabil la volumul țintă, R2). `wb.close()` la final.
- **CSV delimiter sniff (Eng#3):** export Excel RO folosește `;` (virgula = separator zecimal). `csv.Sniffer`
pe `{; , \t}` sau probă explicită; alege delimiter-ul care dă >1 coloană consistent. 1 coloană → `HeaderError`
clar, NU mapare oarbă.
- **Coercion Excel (R3):** odometru numeric → `123456.0`; VIN/nr cu zerouri tăiate; date ca `datetime`. Normalizarea
e centralizată în `idempotency.canonicalize_row` (vezi 3.4bis); coercion nerecuperabilă → starea `needs_review` (3.4).
- **Dată dezambiguizată (Voce#2):** celulă `datetime` nativă din openpyxl → folosită DIRECT (neambiguă). Celulă
STRING → aplică `date_fmt` mapat, DAR dacă `zi<=12` (deci și MM.DD ar fi valid) → forțează `needs_review`,
nu trimite orb. Acoperă fișierul mixt datetime/string (cazul real RO).
- **Robustețe export RO real (Voce#6/#7):** dacă workbook-ul are >1 sheet non-gol → cere alegerea sheet-ului
(nu presupune `active`); rezolvă celulele header **îmbinate** (un-merge logic → nume reale sau flag, nu nume goale);
taie rândurile trailing unde coloanele-cheie (VIN+dată) sunt goale (footer `TOTAL`/`Întocmit de:`); rând fără VIN
= **skip structural**, nu `needs_data` fantomă.
- Detectează header (primul rând non-gol), întoarce `{import_id, columns, sample_rows}`. NU trimite nimic la RAR.
- Stochează în `import_batches` / `import_rows` (PII **criptat** cu `app/crypto.py`, `purge_after` ca `submissions`).
### 3.2 Mapare coloane (NOUĂ — stratul care lipsește azi)
- **Schemă fișier → câmpuri canonice**: `vin`, `nr_inmatriculare`, `data_prestatie`, `odometru_final`,
`odometru_initial?`, `operatie` (denumire/cod), `obs?`.
- **Reținută per cont** (`column_mappings`), cu **semnătură de coloane**. Map once, reuse forever.
- **Detectie drift (acceptat D3):** maparea reținută se aplică DOAR dacă semnătura coloanelor se potrivește
exact. Coloane mutate/redenumite → NU aplica orb, cere re-confirmare. Previne maparea tăcută greșită la upload 2.
- Auto-sugestie fuzzy pe nume coloană („VIN"/„Serie sasiu"→vin; „KM"→odometru_final).
- **DRY (Eng#4):** refolosește `mapping.normalize_for_match` (NFKD+lowercase+strip) + `fuzz.token_sort_ratio`
(rapidfuzz) — ACELAȘI primitiv ca editorul de operații. Map `{camp_canonic: [sinonime]}`, zero dependință nouă.
- Format dată configurabil per mapare (`DD.MM.YYYY` RO vs ISO) → normalizat la `YYYY-MM-DD` (vezi dezambiguizarea în 3.1).
### 3.3 Mapare operații (reuse Treapta 1)
- Eticheta operației din fișier → `codPrestatie` prin `operations_mapping` + fuzzy.
- **Gate auto_send pe coduri noi (acceptat D3):** o operație nou-mapată sau cod neobișnuit NU se trimite
automat → review manual o dată (un `FINALIZATA` eronat e permanent).
### 3.4 Preview + commit (gate HARD)
- `GET /v1/import/{id}/preview`: fiecare rând cu stare derivată (rulează `validation.py` + `resolve_prestatii`
FĂRĂ enqueue). **Cinci stări:**
- `ok` — gata de trimis.
- `needs_mapping` — operație fără cod.
- `needs_data` — validare RAR eșuată / odometru lipsă.
- `needs_review` (acceptat D6, R3) — coercion Excel suspectat (VIN numeric, odometru float). **Blochează
auto-send chiar dacă validarea trece** — VIN stricat = `FINALIZATA` permanent greșit.
- `already_sent` (acceptat D5) — cheia idempotency există deja. Preview arată „deja trimis pe `<data>` ca
`idPrezentare X`". **Niciodată dedup tăcut într-un commit în masă** — decizie explicită per-rând.
- **Lookup batch, nu N+1 (Eng#5):** calculează toate cheile, apoi `SELECT idempotency_key FROM submissions
WHERE account_id=? AND idempotency_key IN (chunk)` (chunk-uri ~900 param SQLite). O(1) interogări, nu 5000.
`load_mapping` o singură dată ca POST.
- `duplicate_in_file` (Voce#3, NOU) — coliziune INTRA-batch. Grupare pe cheie în fișierul parsat:
`|grup|>1` identice → „rândul 12 și 88 identice"; același `vin+dată+odometru` cu operație diferită →
„rândul 12 și 41 diferă doar prin operație, confirmă". `already_sent` verifică doar batch-uri anterioare;
aceasta prinde coliziunile din ACELAȘI fișier (altfel `UNIQUE` global le înghite/erează mid-batch).
- **Gate HARD de confirmare (acceptat D3 + Voce#1):** rezumat dry-run (X gata, Y date lipsă, Z nemapate, W deja
trimise) + confirmare explicită (tastezi numărul de prezentări). **Plus atestare pe VALORI, nu doar pe total:**
preview-ul arată per-rând valorile FINALE rezolvate (VIN, dată ca `YYYY-MM-DD` cum o vede RAR, km); rândurile
`needs_review` trebuie bifate explicit „verificat" ca să intre în N. `N` dovedește numărul; bifa dovedește
conținutul. Oprește atât count-error cât și content-error (VIN coercionat / dată swap în rândul individual).
- Commit = enqueue în `submissions` DOAR rândurile `ok` confirmate → worker → monitorizare standard.
- **Log atestare (Voce#9):** la commit scrie `import_attestations` (batch_id, account_id, confirmed_by, ts,
`rows_hash`=sha256 peste valorile rezolvate confirmate, n_confirmed). Apare în `/v1/audit/export`. UI sub bară:
„Confirmând, TU ești declarantul acestor N prezentări la RAR (ireversibil)". Apărare legală + trasabilitate
(L.142/2023 — operatorul e declarantul de rol).
### 3.4bis Cheie idempotency canonică partajată (Eng#2)
- Coercion-ul Excel (`123456.0`) calculat ÎNAINTE de cheie poate da cheie diferită de POST live (`123456`) →
`already_sent` ratează → al doilea `FINALIZATA`. Fix: extrage normalizarea canonică (odometru strip `.0`,
VIN upper/strip, dată `YYYY-MM-DD`) într-un helper public `idempotency.canonicalize_row(raw) -> dict` +
`build_key(account_id, canon)`. **Parser-ul de import ȘI `POST /v1/prezentari` apelează ACELAȘI helper** înainte
de cheie ȘI de validare. O sursă de adevăr; cele două canale nu pot diverge.
- **REGRESIE CRITICĂ:** cheia produsă de refactor trebuie să fie IDENTICĂ cu cea de azi pentru input-uri
existente, altfel rândurile deja trimise capătă cheie nouă → re-trimise. Test de regresie obligatoriu.
### 3.5 Spectru de integrare (același backend)
1. **API** (există) — ROAAUTO / soft propriu.
2. **Upload manual în browser** (3.1-3.4) — service fără cod. **Acesta e scope-ul acestei trepte.**
3. ~~Drop fișier (SFTP/email-to-import)~~ — **CUT din această treaptă** (vezi NOT in scope, decizie D6).
### 3.6 Acces web + creds RAR (model creds CORECTAT — decizie D4 + Eng#1/Voce#5)
- Login web (email + parolă cont) → folosește/emite API-key existent.
- Creds RAR introduse în UI, **criptate-at-rest per-cont** pe **coloana `accounts.rar_creds_enc`** (ALTER aditiv,
exact ca migrarea existentă a aceleiași coloane pe `submissions`; NU tabel nou — Eng#1). O singură sursă per cont.
- **Worker re-login (fallback):** `claim_one` rămâne; la login worker-ul face `creds = submission.creds_enc
OR SELECT rar_creds_enc FROM accounts WHERE id=?`. Submission web fără creds → ia din `accounts` → login OK.
- **De ce abatere de la zero-storage Treptei 1:** canalul web **nu are re-pusher** (ROAAUTO re-trimitea
la creds lipsă; aici nu există). Worker-ul trebuie să poată re-login oricând, altfel o serie încărcată
zile mai târziu, după un restart worker, rămâne blocată permanent → declarație legală netrimisă, tăcut.
- **Gate purjare `worker:271` (Voce#5, P1):** purjarea existentă `UPDATE submissions SET rar_creds_enc=NULL
WHERE account_id=?` e ACCOUNT-scoped → la primul login web ar șterge creds de pe TOATE submission-urile
contului, inclusiv cele API-channel efemere. Conturile CU `accounts.rar_creds_enc` durabil: purjarea devine
inofensivă (worker re-citește din `accounts`). Conturile FĂRĂ durabil (API-channel pur Treapta 1): purjarea
rămâne neschimbată. **Test = coadă MIXTĂ API+web** (după login web, submission-urile API tot se trimit), nu doar web.
- Compensare risc: creds tot criptate (Fernet), tot redactate din loguri; doar **persistate**, nu efemere.
Blast-radius mai mare la scurgere cheie Fernet (creds durabile vs. doar in-flight) — acceptat conștient (D4).
## 4. Date noi (SQLite)
- `column_mappings` (account_id, signature_coloane, json mapare, format_data, created_at).
- `import_batches` (id, account_id, filename, status, total/ok/needs_*/already_sent/duplicate_in_file, created_at, purge_after).
- `import_rows` (batch_id, row_index, raw_json **criptat**, resolved_status, error). Purjate cu batch-ul.
- `accounts.rar_creds_enc` — **coloană durabilă per-cont** (ALTER aditiv, NU tabel nou) pentru canalul web (D4, Eng#1).
- `import_attestations` (batch_id, account_id, confirmed_by, ts, rows_hash, n_confirmed) — log atestare legală (Voce#9).
- `submissions += batch_id, row_index` (T7, P1) — scope pentru `reresolve_account` + trasabilitate export rânduri eșuate.
**Închide R1** (bulk-send tăcut cross-batch). T7 e predecesor HARD al U3 (vezi Roadmap).
## 5. NOT in scope (amânat / tăiat, cu motiv)
- **Drop-fișier SFTP / email-to-import** (era U7) — TĂIAT. Trei mecanisme de intrare înainte ca un singur
service non-ROA să fi încărcat manual un fișier. Validează întâi upload-ul manual, apoi decide. (D6)
- **Contor volum + prag freemium** (era U6) — DEFER. Metrici de preț înainte să existe useri; a contoriza o
obligație legală e delicat. Contorul e trivial de adăugat post-validare. (D6)
- **Wedge auto-drop (SFTP/email-to-import)** — DEFER, confirmat de user la eng review: „verific manual upload-ul
întâi". Manual upload e wedge-ul; auto-drop se re-evaluează post-validare (tensiune Voce#8 vs CEO D6, rezolvată
în favoarea manual).
- **Mapare AI / conector MCP** — Treapta 3.
- **Editare/anulare prezentări trimise** — `FINALIZATA` terminal, neschimbat.
- **Billing complet (Stripe etc.)** — după validarea pragului.
## 6. Mașina de stări (rând de import → submission)
```
┌─────────── POST /v1/import (parsare, staging, NU trimite) ──────────┐
▼ │
import_row.resolved_status: │
ok ─────────────┐ │
needs_mapping │ (preview: rezolva fara enqueue) │
needs_data │ │
needs_review ───┤ (coercion suspectat → blocheaza auto-send) │
already_sent ───┘ (cheie idempotency exista → decizie per-rand) │
│ │
▼ GATE HARD confirmare (tastezi N prezentari) │
commit: enqueue DOAR rinduri `ok` confirmate ──▶ submissions (queued) ──▶ worker (Treapta 1, neatins)
login RAR → postPrezentare → FINALIZATA
```
## 7. Error & Rescue Map (stratul nou)
```
CODEPATH | CE POATE MERGE PROST | EXCEPTIE / STARE
-------------------------------|-----------------------------------|----------------------
POST /v1/import parse xlsx | fisier corupt / non-xlsx | BadZipFile/InvalidFile
| encoding RO (cp1250) | UnicodeDecodeError
| >5MB / >5000 randuri | FileTooLarge (custom)
| header lipsa / coloane duplicate | HeaderError (custom)
parse cell | VIN/odometru coercion Excel | → stare needs_review
| data DD.MM.YYYY | → normalizare, altfel needs_data
apply column_mapping | semnatura coloane != reținuta | → cere re-confirmare (drift)
preview resolve | cheie idempotency exista | → stare already_sent
commit | confirmare numar gresit | reject, nu enqueue
| worker fara creds (restart) | → REZOLVAT D4 (creds durabile)
STARE / EXCEPTIE | RESCUED? | ACTIUNE | USER VEDE
------------------------|----------|----------------------------------|---------------------------
BadZipFile/InvalidFile | Y | 422, mesaj „fisier invalid" | „Fisier nerecunoscut (xlsx/csv)"
UnicodeDecodeError | Y | retry cp1250/latin2, apoi 422 | „Encoding nesuportat"
FileTooLarge | Y | 413, fara parsare partiala | „Max 5000 randuri / 5MB"
HeaderError | Y | 422 + ce coloane s-au gasit | „Antet neclar, verifica fisierul"
needs_review (coercion) | Y | blocheaza auto-send, cere uman | rind galben „verifica VIN/km"
already_sent | Y | NU dedup tacut, decizie per-rind | „deja trimis pe <data> #X"
drift semnatura coloane | Y | nu aplica orb, re-mapare | „coloanele difera, confirma maparea"
worker fara creds | Y (D4) | re-login din creds durabile | nimic (transparent)
```
## 8. Failure Modes Registry
```
CODEPATH | FAILURE MODE | RESCUED? | TEST? | USER VEDE | LOGGED?
--------------------------------|-------------------------------|----------|-------|------------------|--------
upload parse | encoding/format RO | Y | Y | mesaj clar | DA
cell coercion (VIN/odo Excel) | VIN stricat trece validarea | Y(D6) | Y | needs_review | DA
column_mapping drift | mapare tacuta gresita upload2 | Y(D3) | Y | re-confirmare | DA
commit in masa | trimite 100 randuri gresite | Y(D3) | Y | gate confirmare | DA
re-export (idempotency) | duplicat / corectie inghitita | Y(D5) | Y | already_sent | DA
worker restart, creds purjate | serie blocata permanent tacut | Y(D4) | Y | nimic (re-login) | DA
mapare salvata → re-resolve | trimite tacut randuri cross- | Y(T7) | Y | gate confirmare | DA
| batch / feed API live | | | (batch scoped) |
data string zi<=12 (DD vs MM) | data gresita-dar-valida trece | Y(V#2) | Y | needs_review | DA
duplicat in ACELASI fisier | UNIQUE global inghite/ereaza | Y(V#3) | Y | duplicate_in_file| DA
multi-op same vin+data+odo | reconcile dropa rand netrimis | Y(V#4) | Y | confirma manual | DA
creds durabile, login web | purjare account-scoped sterge | Y(V#5) | Y | nimic (fallback) | DA
| creds API-channel efemere | | | |
100 declaratii dintr-un N | raspundere fara atestare/rol | Y(V#9) | Y | UI declarant+log | DA
export RO: sheet 2 / merged hdr | HeaderError pe fisier valid | Y(V#6) | Y | alege sheet/flag | DA
export RO: footer TOTAL parsat | prestatie fantoma needs_data | Y(V#7) | Y | skip structural | DA
```
**R1 ÎNCHIS (T7, P1):** `reresolve_account` (mapping.py:253) primește `batch_id` și se scope-ază la seria comitată;
T7 e predecesor HARD al U3. Salvarea unei mapări nu mai poate auto-queue rânduri cross-batch / din feed API live.
Niciun failure mode silent rămas neacoperit.
## 9. Riscuri / open questions
- **R1 (ÎNCHIS la eng review):** mapare account-global → bulk-send tăcut cross-batch. Fix `batch_id` scoping
promovat la P1-blocking (T7), predecesor HARD al U3. Nu mai e deschis.
- **R2:** fișiere mari (mii rânduri) → upload sincron + `openpyxl read_only` streaming + cap hard înainte de
parsare (Eng#6). Async amânat până apare nevoia reală.
- **R4 (nou, blast-radius):** creds durabile-at-rest (D4) → la scurgerea cheii Fernet, toate parolele RAR sunt
decriptabile (vs. doar in-flight azi). Acceptat conștient; mitigare = rotație cheie + redactare loguri (existent).
- **R3:** coercion Excel nerecuperabilă la parsare → stare `needs_review` (acceptat D6).
- Un cont = un agent RAR sau mai mulți (afectează maparea creds în UI) — open question moștenit din plan.md.
- `b64Image` rămâne opțional, omis în upload v2.
## 10. Roadmap (reordonat — eng review: T7 înainte de U3)
- [ ] **U1** — `import_batches`/`import_rows`/`column_mappings` + parser xlsx/csv (`POST /v1/import`), cu
encoding RO + delimiter sniff + openpyxl read_only + dezambiguizare dată + robustețe sheet/merged/footer.
PII criptat în staging. (T3 + Voce#2/#6/#7 + Eng#3/#6)
- [ ] **U2** — creds RAR durabile per-cont pe web (`accounts.rar_creds_enc`, ALTER aditiv) + worker re-login
fallback + gate purjare `worker:271` (fix D4 + Voce#5). **Mutat înainte** — dependență hard end-to-end.
- [ ] **T7** — `batch_id`/`row_index` pe `submissions` + scope `reresolve_account` la seria comitată.
**Predecesor HARD al U3** — închide R1 (bulk-send cross-batch) ÎNAINTE ca save-mapare să ajungă live.
- [ ] **U3** — mapare coloane + reținere per cont + semnătură + detectie drift + auto-sugestie fuzzy (reuse
`normalize_for_match`, Eng#4). Nu se livrează până T7 nu e merged.
- [ ] **U4** — preview (6 stări: ok/needs_mapping/needs_data/needs_review/already_sent/duplicate_in_file) +
lookup `already_sent` batch (Eng#5) + canonicalize partajat (Eng#2) + gate HARD confirmare cu atestare pe
valori (Voce#1) + log atestare (Voce#9) + commit selectiv → coadă.
- [ ] **U5** — UI web upload (Jinja2+HTMX în dashboard): drop → mapează → preview → confirmă → trimite.
- [ ] **U6 (P2)** — export rânduri eșuate (CSV) pentru corecție + re-upload (acceptat D3; depinde de T7).
- [ ] ~~contor freemium~~ — DEFER (D6). ~~drop-fișier SFTP~~ — CUT (D6, re-eval post-validare).
## 11. Diagrame
### Arhitectură (componente noi vs existente)
```
[Fisier xlsx/csv] GATEWAY (existent, neatins sub linie)
│ upload ┌──────────────────────────────────────┐
▼ │ app/validation.py app/mapping.py │
POST /v1/import ──parse──▶ import_batches │ app/idempotency.py app/crypto.py │
(NOU) (cp1250, coercion) │ │ app/reconcile.py │
│ ▼ └──────────────┬───────────────────────┘
│ import_rows (PII cript) │ commit (rinduri ok)
▼ │ preview ▼
column_mappings (NOU) ──semnatura──▶ │ resolve submissions+batch_id ──▶ WORKER (existent)
(mapare retinuta+drift) │ (6 stari) queued (T7 scope) login RAR → postPrezentare
▲ ▼ └─▶ FINALIZATA (permanent)
accounts.rar_creds_enc (NOU, D4) ───creds durabile──▶ worker re-login fallback (fara re-pusher)
```
### Data flow + shadow paths (upload → commit)
```
FISIER ──▶ PARSE ──▶ MAP COLOANE ──▶ RESOLVE ──▶ CONFIRM ──▶ ENQUEUE
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
[gol?] [coercion?] [drift?] [already_ [N gresit?] [dup key?
[non-xlsx?][encoding?] [nemapat?] sent?] reject → already_sent]
[>5MB?] [needs_ [auto_send [needs_ [creds? → D4]
review] gate] data?]
```
## 12. Implementation Tasks
Sintetizate din findings. P1 blochează ship; P2 = aceeași treaptă; P3 = follow-up.
- [ ] **T1 (P1, human ~1zi / CC ~30min)** — schema — coloană `accounts.rar_creds_enc` durabilă + worker re-login fallback + **gate purjare `worker:271`**
- Surfaced by: voce externă #1 (D4) + Voce#5 — `worker/__main__.py:271` purjază account-scoped, web n-are re-pusher
- Files: `app/schema.sql` (ALTER aditiv), `app/db.py:_migrate`, `app/worker/__main__.py`, `app/crypto.py`
- Verify: test — (a) serie web, worker restart, token expirat → re-login din `accounts` → trimite;
(b) **coadă MIXTĂ** API(efemer)+web(durabil) → după login web, submission-urile API tot se trimit
- [ ] **T2 (P1, human ~half zi / CC ~20min)** — preview — stare `already_sent` + lookup **batch IN(...)** (no N+1, no silent dedup)
- Surfaced by: voce externă #2 (D5) + Eng#5 — `idempotency.py:23`; 5000 randuri = N+1 dacă per-rând
- Files: `app/api/v1/` import preview, `app/idempotency.py`
- Verify: test — (a) re-upload cu typo odometru corectat → already_sent, nu al doilea FINALIZATA; (b) 5000 randuri → ≤7 interogări
- [ ] **T3 (P1, human ~half zi / CC ~20min)** — parse — coercion guard + stare `needs_review` (blochează auto-send) + **mesaj formule-None**
- Surfaced by: voce externă #8 (D6, R3) + **Eng pass 2 Issue 3** — `openpyxl data_only=True` întoarce `None` pe celule cu formule necalculate (export soft RO ≠ Excel) → indistinct de gol → cad în `needs_data` cu mesaj generic confuz pe un fișier care arată plin
- **Issue 3 (P2):** când o coloană obligatorie întoarce `None` pe o pondere mare de rânduri (euristică pe rata de None, fără `data_only=False`), emite mesaj țintit: „fișier cu formule fără valori salvate — deschide și re-salvează în Excel". Gate-ul `needs_data` prevenea deja trimiterea greșită; asta e doar claritate UX.
- **Ordonare critică (Eng#2/§3.4bis):** `canonicalize_row` rulează ÎNAINTE de `validate_prezentare` — `_parse_int` (`validation.py:44`, `isdigit()`) respinge `"123456.0"`; coercion-ul trebuie să taie `.0` înainte ca validarea să-l vadă (altfel `needs_data` în loc de `needs_review`).
- Files: parser import, preview resolve
- Verify: test — (a) VIN `0123…` numeric din xlsx → needs_review, nu se trimite; (b) xlsx cu coloană de formule fără cache → mesaj specific, nu `needs_data` generic; (c) odometru `123456.0` → canonicalizat la `123456` înainte de validare
- [ ] **T4 (P1, human ~half zi / CC ~20min)** — mapare — semnătură coloane + detectie drift
- Surfaced by: review D3
- Files: `column_mappings`, mapare coloane
- Verify: test — upload 2 cu coloane mutate → cere re-confirmare, nu aplică orb
- [ ] **T5 (P1, human ~half zi / CC ~25min)** — preview — gate HARD confirmare (tastezi N) + **atestare pe valori rezolvate** (Voce#1)
- Surfaced by: review D3 + Voce#1 — N dovedește totalul, bifa dovedește conținutul (VIN/dată/km finale)
- Files: UI preview, commit endpoint
- Verify: test — (a) commit fără N corect → reject; (b) rând `needs_review` nebifate → exclus din N, nu se trimite
- [ ] **T6 (P1, human ~half zi / CC ~15min)** — mapare — gate auto_send pe coduri nou-mapate (**NU e additiv — schimbă cod existent**)
- Surfaced by: review D3 + plan.md P2 + **Eng pass 2 OV-1** — `auto_send` e SCRIS (`save_mapping`) și afișat (`_mapari.html:49`) dar CITIT de niciun codepath; `reresolve_account` și bucla POST resolve trec pe `queued` ignorând flag-ul → AZI codurile nou-mapate se auto-trimit deja (bug latent Treapta 1)
- **OV-1 (P1):** T6 trebuie să MODIFICE `reresolve_account` ȘI resolve-ul POST/import să consulte `auto_send` (`auto_send=0` → stare ținută/`needs_review`), nu doar să adauge un gate nou.
- Files: `app/mapping.py` (reresolve_account), `app/api/v1/router.py` (POST resolve), commit
- Verify: test — (a) cod nou-mapat cu `auto_send=0` → nu auto-send, review manual; (b) **regresie:** mapare existentă cu `auto_send=1` tot se requeue ca azi
- [ ] **T7 (P1, human ~1zi / CC ~30min)** — **R1 ÎNCHIS** — `batch_id`/`row_index` pe submissions + scope `reresolve_account` (**predecesor HARD al U3**)
- Surfaced by: voce externă #3+#5 + Voce#10 — `mapping.py:253` account-global (PROMOVAT la P1-blocking la eng review)
- Files: `app/schema.sql`, `app/db.py:_migrate`, `app/mapping.py`, commit
- Verify: test — (a) salvare mapare în batch A NU trimite rânduri din batch B / feed API; (b) canal API (batch_id NULL) tot se re-rezolvă ca azi
- [ ] **T8 (P2, human ~half zi / CC ~15min)** — export rânduri eșuate CSV (depinde de T7 pt. trasabilitate)
- Surfaced by: review D3
- Files: import export endpoint
- Verify: descarci needs_data/needs_mapping ca CSV, corectezi, re-upload
- [ ] **T9 (P1, human ~half zi / CC ~20min)** — idempotency — `canonicalize_row` + `build_key` partajat (parser + POST), DRY + **normalizare account_id**
- Surfaced by: Eng#2 + **Eng pass 2 OV-2** — coercion înainte de cheie → divergență `already_sent`; `idempotency.py:23` hash-uiește `account_id` AS-PASSED (`None` pe canal API, `router.py:66`) dar rândurile se stochează sub `account_or_default`=1 → același rând logic capătă cheie diferită cross-canal → `already_sent` ratează → al doilea `FINALIZATA`
- **OV-2 (P1):** `canonicalize_row`/`build_key` aplică `account_or_default` ÎNAINTE de hash (None și 1 colapsează la o cheie). Tensiune cu §3.4bis „cheie identică": rândurile vechi cheie-`None` trebuie reconciliate (recompute o-singură-dată SAU dual-lookup), documentat explicit.
- Files: `app/idempotency.py`, parser import, `app/api/v1/router.py`
- Verify: test — (a) **cross-canal:** cheie(API canal-None) == cheie(import canal-rezolvat) pt. același rând logic; (b) regresie: strategia de reconciliere a cheilor vechi acoperită de test (fără re-trimitere tăcută)
- [ ] **T10 (P1, human ~half zi / CC ~20min)** — parse — dezambiguizare dată **la nivel de coloană** (datetime nativ direct; string ambiguu → needs_review)
- Surfaced by: Voce#2 + **Eng pass 2 OV-8** — `validation.py:81` (`date.fromisoformat`) acceptă orice ISO valid în interval → un DD/MM swap valid-dar-greșit trece. `zi≤12` per-rând ratează coloana uniform MM.DD (rândurile `zi>12` par neambigue și trec ca `ok`)
- **OV-8 (P3):** detectează formatul din ÎNTREAGA coloană — dacă ORICE rând are token poziția-1 `>12`, coloana e DD-first; aplică formatul la toate rândurile, nu doar flag per-rând `zi≤12`.
- Files: parser import, preview resolve
- Verify: test — (a) `03.04.2026` string ambiguu → needs_review; (b) celulă datetime nativă → folosită direct; (c) coloană uniform MM.DD cu rânduri `zi>12` → format detectat la nivel de coloană, nu trec orb ca `ok`
- [ ] **T11 (P1, human ~half zi / CC ~20min)** — preview — detecție coliziuni intra-batch (DOAR la preview/commit, NU în worker)
- Surfaced by: Voce#3+#4 + **Eng pass 2 OV-3** — `UNIQUE` global înghite dup intra-fișier; `reconcile.py` e op-blind PRIN DESIGN (recuperare răspuns pierdut, worker:184/217)
- **OV-3 (P1):** detecția coliziunilor intra-fișier trăiește EXCLUSIV la preview/commit (`duplicate_in_file`). NU edita `reconcile.py` / `worker/__main__.py` — a face reconcile op-aware regresează T2 (recuperarea POST-ului pierdut pe timeout legitim). Intra-file dedup (preview-time) ≠ reconcile stare-RAR (worker-time): probleme diferite.
- Files: preview resolve (NU reconcile.py, NU worker)
- Verify: test — (a) 2 rânduri identice în fișier → `duplicate_in_file`; (b) batch cu vin+data+odo colidant → flag la preview, cere manual; (c) **regresie T2:** `match_finalizata` rămâne op-blind, recuperarea răspuns-pierdut neschimbată
- [ ] **T12 (P2, human ~half zi / CC ~15min)** — commit — log `import_attestations` + UI „ești declarantul" + **commit per-rând ON CONFLICT (TOCTOU)**
- Surfaced by: Voce#9 + **Eng pass 2 Issue 1** — `already_sent` la preview e un snapshot; gardianul real e indexul UNIQUE la INSERT, minute mai târziu. Un canal concurent (API live / al 2-lea import) poate insera cheia colidant în fereastra preview→commit → un INSERT multi-rând într-o tranzacție rollback-uiește TOT batch-ul (`router.py:100` e INSERT simplu, nu OR IGNORE) → utilizatorul a tastat N, confirmat, primește eroare opacă, iar `rows_hash`(N) nu mai corespunde cu ce s-a inserat.
- **Issue 1 (P1):** commit inserează per-rând cu `INSERT … ON CONFLICT(idempotency_key) DO NOTHING`; rândurile care colidează se reclasifică `already_sent` în rezultatul commit-ului; `import_attestations.rows_hash` + `n_confirmed` acoperă DOAR rândurile efectiv puse în coadă (nu N inițial). Respectă principiul planului „niciodată dedup tăcut".
- Files: `app/schema.sql`, commit endpoint, `app/api/v1/router.py` (audit export), UI preview
- Verify: test — (a) commit → rând `import_attestations` cu rows_hash + n_confirmed; apare în `/v1/audit/export`; (b) **TOCTOU:** cheie inserată de canal concurent după preview → rând reclasificat `already_sent`, atestarea acoperă doar rândurile puse în coadă
- [ ] **T13 (P2, human ~1zi / CC ~25min)** — parse — robustețe export RO (multi-sheet + merged header + trim footer)
- Surfaced by: Voce#6+#7 — sheet 2 / celule îmbinate → HeaderError pe fișier valid; footer TOTAL → prestatie fantomă
- Files: parser import
- Verify: test — (a) workbook 2 sheets → cere alegerea; (b) header merged → nume reale; (c) footer fără VIN → skip, nu needs_data
- [ ] **T14 (P2, human ~half zi / CC ~15min)** — perf — CSV delimiter sniff + openpyxl `read_only` streaming + cap înainte de parse
- Surfaced by: Eng#3+#6 — `;` RO dă 1 coloană tăcut; DOM întreg = vârf memorie
- Files: parser import
- Verify: test — (a) CSV `;` → coloane corecte; 1 coloană → HeaderError; (b) >5000 rânduri → FileTooLarge fără parse parțial
- [ ] **T15 (P2, human ~half zi / CC ~20min)** — test — **E2E integrare** import→commit→worker (RAR mock)
- Surfaced by: Test review — mock-urile per-unit ascund cheia idempotency + re-login + batch scoping
- Files: `tests/test_import_e2e.py`
- Verify: upload fixture → mapează → preview → commit N → worker run_once(MockRar) → FINALIZATA; re-upload corectat → already_sent
- [ ] **T16 (P1, human ~2h / CC ~20min)** — retenție — job purjare + `purge_after` SET la insert (ambele canale)
- Surfaced by: **Eng pass 2 OV-5** — `purge_after` e exportat în audit dar SETAT de niciun INSERT și NICIUN job de purjare nu există (`grep purge_after` → doar SELECT). Planul presupunea paritate `submissions`/`import_rows` care nu există → PII criptat (Fernet) trăiește la nesfârșit. Decalaj GDPR/L.142.
- Files: `app/worker/__main__.py` (tick purjare), commit/insert (`submissions` + `import_batches`/`import_rows`)
- Verify: test — (a) insert → `purge_after` populat (sent+90z); (b) rând expirat → șters de tick-ul de purjare; (c) `import_rows` purjate cu batch-ul
### 12bis. Eng Review Pass 2 — sinteză (2026-06-16)
A doua trecere `/plan-eng-review` pe planul deja CLEARED: 6 findings noi (Claude) + 5 din vocea externă (subagent — Codex la cuotă), TOATE acceptate cu opțiunea completă (Lake 11/11). Detalii foldate în taskuri:
- **Issue 1 (P1, T12):** commit TOCTOU → per-rând `ON CONFLICT DO NOTHING`, atestare doar pe rândurile puse în coadă.
- **Issue 2 (P2, T13/T14) = vocea externă OV-6 (consens cross-model):** `openpyxl read_only=True` nu vede celule îmbinate → parser în 2 treceri (read_only dim-check + body; normal-mode header+merged DUPĂ cap-check).
- **Issue 3 (P2, T3):** `data_only=True` → `None` pe formule necalculate → mesaj specific (euristică rată-None).
- **Issue 4 (P3, U1):** `openpyxl` lipsește din `requirements.txt` → adaugă PINNED (ex. `openpyxl==3.1.x`) explicit în U1.
- **Issue 5 (P2, U1/U3):** teste explicite — (a) `import_rows.raw_json` criptat la rest (ciphertext pe disc, plaintext după decrypt); (b) fuzzy coloane refolosește `mapping.normalize_for_match` (fără normalizator duplicat).
- **Issue 6 (P2, U1/U4):** scrieri bulk sub autocommit (`db.py:17` `isolation_level=None`) → `BEGIN IMMEDIATE`…`COMMIT` + `executemany` (model `claim_one`); 5000 fsync → 1.
- **OV-1 (P1, T6):** `auto_send` coloană moartă (citită nicăieri) → T6 modifică `reresolve_account` + resolve POST, nu doar adaugă.
- **OV-2 (P1, T9):** skew `account_id` la hash → normalizare `account_or_default` în `canonicalize_row` + test cross-canal.
- **OV-3 (P1, T11):** intra-file dedup DOAR la preview/commit; NU atinge `reconcile.py`/worker (op-blind by design, T2).
- **OV-5 (P2, T16):** job purjare + `purge_after` la insert (nou T16, mai sus).
- **OV-8 (P3, T10):** dezambiguizare dată la nivel de coloană, nu per-rând `zi≤12`.
- **NOTE U1:** parserul = 2 treceri (Issue 2); adaugă `openpyxl` pinned (Issue 4); test PII-at-rest (Issue 5a); scrieri staging în tranzacție explicită + `executemany` (Issue 6). **NOTE U3:** test reuse `normalize_for_match` (Issue 5b). **NOTE U4:** enqueue în tranzacție explicită (Issue 6).
- **Constrângere asset offline (learning):** UI upload (U5) NU introduce assets din CDN — gateway rulează offline; refolosește htmx vendorizat local (`app/web/static/`).
- **Ordine livrare actualizată:** U1 → U2/T1 → T7 → U3 → U4 → U5; **T16 (purjare) poate merge în paralel** (independent de T7).
## 13. Design spec UI (post `/plan-design-review`)
> Clasificare: **APP UI** (tool intern, data-dense). Extinde sistemul existent din
> `app/web/templates/base.html` (`:root` tokens) — NU introduce limbaj nou. Refolosește:
> `--ok/--warn/--err/--accent`, `.card`, pills `.s-*`, `.maprow`, `.tablewrap`, empty states.
### 13.1 Information architecture (Pass 1)
```
DASHBOARD (existent)
├─ card UPLOAD (NOU, sus; primar/CTA cand coada e goala)
│ „Incarca fisier (xlsx/csv)" → drop zone + buton
├─ [dupa upload] → ecran/sectiune MAPARE
│ 1. mapare COLOANE (.maprow: camp canonic ← dropdown coloane)
│ 2. mapare OPERATII (editorul fuzzy existent)
├─ [dupa mapare] → PREVIEW (tabel dominant, 5 stari)
│ rezumat pills sus + filtru + bara confirmare jos
└─ coada submissions (existent, neatins)
```
Ierarhie preview: 1) rezumat stari (ce e gata/cu probleme), 2) tabelul, 3) bara de trimitere.
### 13.2 Tabel stari interacțiune (Pass 2)
```
ECRAN | LOADING | EMPTY | ERROR | SUCCESS | PARTIAL
---------------|--------------------|-----------------------|--------------------|--------------------|--------------------
Upload | spinner „se incarca| drop zone + „trage | „fisier invalid | → trece la mapare | n/a
| / se parseaza…" | fisierul aici" + CTA | (xlsx/csv)" rosu | |
Mapare coloane | — | „nicio coloana | dropdown rosu pe | toate verzi → next | unele campuri
| | detectata" | camp obligatoriu | | nemapate (galben)
Preview | „se valideaza N | „0 randuri in fisier" | rand rosu + motiv | „N gata de trimis" | rezumat: X ok,
| randuri…" | | per rand | verde | Y probleme
Trimitere | bara progres N/M | n/a | rand → error in | „N trimise" flash | „M din N trimise,
| | | coada | (.flash existent) | restul in coada"
```
Empty state upload = feature: warmth („Primul fisier? Trage-l aici.") + CTA + context („xlsx sau csv, max 5000 randuri").
### 13.3 User journey / arc emoțional (Pass 3)
```
PAS | USER FACE | USER SIMTE | UI SUSTINE
----|--------------------|-------------------------|---------------------------------
1 | incarca fisier | nesiguranta („trimite | mesaj clar „NU se trimite nimic
| | acum la RAR?") | pana confirmi" sub drop zone
2 | mapeaza coloane | efort prima data | auto-sugestie fuzzy pre-selectata;
| | | a 2-a oara: „mapare retinuta aplicata"
3 | vede preview | control / verificare | rezumat pills + problemele primele
4 | confirma (tastezi N| frica de greseala | gate explicit; „needs_review" galben
| | permanenta | blocheaza VIN suspect
5 | vede „N trimise" | usurare / incredere | .flash verde + rand sent in coada
```
5s: „inteleg ca nu trimite nimic inca". 5min: „maparea s-a retinut". 5 luni: „drop + trimite, sub 1 min".
### 13.4 AI slop (Pass 4) — 8/10
APP UI, refolosește sistemul calm existent. Fără card-mosaic decorativ, fără gradients, fără
3-column grid, fără border-left colorat ornamental. Pills semantice = funcționale, nu decor. OK.
### 13.5 Design system (Pass 5)
- Stări rând: refolosește `.s-queued/.s-sent/.s-error`; adaugă `.s-needs_review` (galben `--warn`),
`.s-already_sent` (muted), **`.s-duplicate_in_file` (muted, D10)**. Pills numerice rezumat = aceleași culori.
- **Semantica culorii (D10, post eng review):** amber `--warn` = „verifică valori" (DOAR needs_review);
muted `--muted` = „informațional / decizie per-rând" (already_sent + duplicate_in_file); roșu `--err` = blocat;
verde `--ok` = ok; albastru `--accent` = în coadă. `duplicate_in_file` diferențiat de already_sent prin TEXT
explicit + referință încrucișată („dublă cu rândul 88"), nu doar culoare (daltonism — pill poartă cuvântul).
- Mapare coloane = `.maprow` + `.mapcol.grow` + `select` (exact ca `_mapari.html`).
- Drop zone: `.card` cu bordura `--line` dashed la hover; fără estetică nouă.
- Bara confirmare: `.card` fix jos, buton `--accent` existent, `input[type=text]` pentru N.
- **Checkbox atestare (D11):** rândurile `needs_review` au `.chk` existent **per-rând** („verificat") — trebuie
bifate ca să intre în N (forțează privirea pe fiecare valoare rezolvată). `<label>` vizibil + focus tastatură.
- **Banner declarant (D12):** variantă `.banner` cu `--warn` (avertisment, nu eroare roșie), plasat DIRECT
deasupra input-ului N: „Confirmând, TU ești declarantul acestor N prezentări la RAR (ireversibil)". Inevitabil
la momentul confirmării (Krug). Anunțat la screen-reader.
### 13.6 Responsive & a11y (Pass 6)
- Mobil: `.maprow` deja se rupe (`flex-wrap`); tabel preview în `.tablewrap` (scroll în card, existent).
Bara de confirmare devine sticky bottom, nu fixed-overlap.
- Touch ≥44px (deja `.cardlink` min-height 36px → ridică la 44 pe butoanele de acțiune upload/confirm).
- a11y: drop zone are și buton (nu doar drag — drag nu e accesibil la tastatură); dropdown-urile de
mapare au `<label>` vizibil (nu placeholder-as-label); stările au și text, nu doar culoare
(pill cu cuvânt, nu doar pastilă colorată — daltonism); contrast ≥4.5:1 (tokenii existenți trec).
- Gate confirmare accesibil: input N cu label, eroare anunțată, focus pe el la deschidere.
### 13.7 Decizii de design (Pass 7, rezolvate)
- Mapare coloane = listă `.maprow` cu dropdown + eșantion (D8-A). Nu wizard, nu dropdown-pe-antet.
- Preview = rezumat pills + filtru pe stare + problemele primele + bară confirmare cu tastare N (D9-A).
- **D10 (post eng review):** `.s-duplicate_in_file` = muted + text „dublă cu rândul N" (grupat cu already_sent;
amberul rămâne doar „verifică valori"). Fără culoare nouă — disciplină de sistem.
- **D11:** atestare = `.chk` per-rând pe `needs_review`, obligatoriu pentru includere în N (atestare pe valori).
- **D12:** banner declarant = `.banner` `--warn` direct deasupra input-ului N (răspundere legală inevitabilă).
## GSTACK REVIEW REPORT
| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | issues_open | SELECTIVE EXPANSION: 6 propuneri, 5 acceptate, 2 deferate/taiate |
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 2 | clean | Pass 1: 12 findings foldate (R1 INCHIS, T7 P1). Pass 2: 11 findings noi (1 arh + 2 cod + 1 perf + 5 voce externa + 2 test), TOATE acceptate cu optiunea completa (Lake 11/11) |
| Outside Voice | subagent Claude (Codex la cuota) | Independent 2nd opinion | 2 | issues_found | Pass 1: 10 findings. Pass 2: 5 noi (OV-1 auto_send mort, OV-2 skew account_id, OV-3 reconcile op-blind, OV-5 fara job purjare, OV-8 data col-level), TOATE absorbite |
| Design Review | `/plan-design-review` | UI/UX gaps | 2 | clean | full 4→9 (9 decizii) + delta 5→9 pe 3 stari UI noi post-eng (D10/D11/D12) |
- **CROSS-MODEL:** Pass 2 — Issue 2 (Claude) == OV-6 (voce externa) au ajuns INDEPENDENT la aceeasi concluzie (`openpyxl read_only` nu vede merged cells → parser 2-treceri): consens, confidenta ridicata. Fara tensiune Claude-vs-voce-externa in pass 2. (Pass 1: vocea externa contrazisese design D9-A + CEO D6, rezolvate.)
- **PASS 2 — corectitudine pre-existenta:** OV-1/OV-2/OV-3/OV-5 sunt locuri unde planul presupunea o schimbare aditiva dar codul EXISTENT contrazicea presupunerea: `auto_send` coloana moarta (citita nicaieri), skew `account_id` la hash (None vs rezolvat), `reconcile` op-blind by design (T11 nu trebuie sa-l atinga), `purge_after` setat de nimeni + zero job purjare (PII nelimitat). Toate foldate in T6/T9/T11/T16.
- **VERDICT:** CEO + DESIGN + ENG (×2) CLEARED — gata de implementare. R1 INCHIS (T7 P1, predecesor HARD U3). Niciun critical gap silent ramas. Ordine livrare: U1 → U2/T1 → T7 → U3 → U4 → U5; T16 (purjare) in paralel.
NO UNRESOLVED DECISIONS

View File

@@ -1,366 +0,0 @@
# 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)
> **✅ E2E LIVE prin gateway confirmat 2026-06-15.** `POST /v1/prezentari` (VIN `WVWZZZ1KZAW000456`, OE-1) → worker
> login test → `postPrezentare` → record **`idPrezentare=68516` `FINALIZATA`** la RAR (confirmat și prin
> `getFinalizate`), vizibil în dashboard (Worker viu, RAR ok, Trimise 1, rând `sent`/68516/HTTP 200). Creds purjate
> după succes (`rar_creds_enc=null`). Idempotency: re-POST identic → `deduped=true`, același submission, fără record
> dublu la RAR. Pași 5 + 6 + 10(parțial) bifați. **Observație minoră:** un submission proaspăt trece o dată prin
> recuperarea orfanelor înainte de send (`retry_count=1`, +1 query la lista RAR); corect funcțional (nu duplică),
> de optimizat (skip reconciliere la primul send dintr-un rând `queued`).
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>

65
docs/prd/TEMPLATE-prd.md Normal file
View File

@@ -0,0 +1,65 @@
# PRD {X.Y} — <titlu livrabila>
**Stare**: draft
> Copiaza acest fisier ca `docs/prd/prd-{X.Y}-<slug>.md` si completeaza. Procesul complet:
> `docs/ROADMAP.md` §5. Contractul RAR (sursa de adevar): `docs/api-rar-contract.md`.
> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead).
## 1. Obiectiv
<1-2 fraze: ce livreaza si de ce acum. NU repeta strategia/contractul linkeaza-le.>
## 2. Non-Goals (anti scope-creep)
- <ce NU face aceasta livrabila, explicit>
## 3. Stories atomice
> Fiecare story: cea mai mica unitate care lasa sistemul functional. Backend + UI pentru acelasi
> comportament = 2 stories. `Fisiere` + `Depinde de` complete (decid paralelizarea).
### US-001: <titlu scurt>
**Ca** <rol> **vreau** <capabilitate> **pentru ca** <motiv>.
- **Depinde de**: —
- **Fisiere**: `app/<modul>.py`, `tests/test_<x>.py` (~N fisiere)
- **Test intai (RED)**: `tests/test_<x>.py``test_<caz_1>`, `test_<caz_2>`
- **Acceptance criteria**:
- [ ] <criteriu testabil 1>
- [ ] <criteriu testabil 2>
- **Verificare E2E**: <browser HTMX pe http://localhost:8000/ | POST /v1/prezentari pe RAR test>
### US-002: <titlu scurt>
**Ca** <rol> **vreau** <capabilitate> **pentru ca** <motiv>.
- **Depinde de**: US-001
- **Fisiere**: `...`
- **Test intai (RED)**: `tests/test_<y>.py``test_...`
- **Acceptance criteria**:
- [ ] <...>
- **Verificare E2E**: <...>
## 4. Riscuri
- <risc + mitigare>
## 5. Intrebari deschise
> Se rezolva cu utilizatorul ÎNAINTE de executie (poarta de aprobare PRD).
- <intrebare>
## 6. Valuri de executie (graful de dependente)
```
Val 1: [US-001] ← fara dependente, fisiere distincte → paralel (max 2-3 teammates)
Val 2: [US-002] ← deblocat de US-001
```
---
## Raport VERIFY
> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6.
> PASS/FAIL per criteriu, cu dovezi (output pytest citat, E2E pe RAR test). Lipseste pana la VERIFY.