diff --git a/README.md b/README.md index 17f11e1..948016a 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ sunt momentan **neprotejate si globale** (nu filtreaza pe cont). Filtrarea pe co | Creds RAR durabile per cont | **Implementat** | `POST /v1/conturi/rar-creds` | | Creare cont nou (service) | **De facut / manual** | momentan prin `INSERT` SQL (vezi mai jos); nu exista tool/endpoint dedicat | | Protejare + filtrare pe cont a GET-urilor de listare | **De facut** | `GET /v1/prezentari`, `/v1/nomenclator`, `/v1/audit/export` sunt globale acum | -| Self-onboarding web (login email+parola -> emite cheie) | **In plan** | `docs/plans/plan-treapta2.md` (sect. login web) — neimplementat | +| Self-onboarding web (login email+parola -> emite cheie) | **De facut** | `docs/ROADMAP.md` (Etapa 3.3) — neimplementat | > Lifecycle-ul cheilor se face DOAR din CLI, pe masina gateway-ului (admin) — nu exista > suprafata HTTP de administrare de securizat. Cheia in clar se afiseaza **o singura data** @@ -406,10 +406,10 @@ app/ mapping.py # mapare operatie -> cod prestatie + fuzzy lookup crypto.py # criptare Fernet creds RAR efemere (zero-storage at rest) schema.sql # schema SQLite -docs/ # contract RAR (sursa de adevar) + planuri + context +docs/ # contract RAR (sursa de adevar) + ROADMAP (progres + proces) tests/ # suita pytest legacy-vfp/ # arhiva Visual FoxPro ROAAUTO (legacy, doar referinta/migrare) ``` -Detalii de continuitate intre sesiuni: [`docs/CONTEXT.md`](docs/CONTEXT.md). -Plan executabil: [`docs/plans/plan.md`](docs/plans/plan.md). +Contract RAR (sursa de adevar): [`docs/api-rar-contract.md`](docs/api-rar-contract.md). +Roadmap + proces de dezvoltare: [`docs/ROADMAP.md`](docs/ROADMAP.md). diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md deleted file mode 100644 index 508c7d8..0000000 --- a/docs/CONTEXT.md +++ /dev/null @@ -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/.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). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..213c2ac --- /dev/null +++ b/docs/ROADMAP.md @@ -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-.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: +**Ca** **vreau** **pentru ca** . + +- **Depinde de**: US-001 +- **Fisiere**: `app/.py`, `app/web/templates/.html` (~N fisiere) +- **Test intai (RED)**: `tests/test_.py` — `test_`, `test_` +- **Acceptance criteria**: + - [ ] + - [ ] +- **Verificare E2E**: +``` + +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}-.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-.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. diff --git a/docs/api-rar-contract.md b/docs/api-rar-contract.md index d85515e..d20bca0 100644 --- a/docs/api-rar-contract.md +++ b/docs/api-rar-contract.md @@ -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 diff --git a/docs/plans/plan-treapta2.md b/docs/plans/plan-treapta2.md deleted file mode 100644 index 0d79bfc..0000000 --- a/docs/plans/plan-treapta2.md +++ /dev/null @@ -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 `` 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 #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ă). `