refactor: comentarii strict functionale, fara referinte PRD/stories

Curatare globala a comentariilor si docstring-urilor (app, tools, teste,
scripturi): eliminate referintele la PRD-uri, US-xxx, task-uri istorice si
review-uri; pastrata doar informatia functionala, formulata scurt. Regula
adaugata in CLAUDE.md (sectiunea Stil). Fara modificari de cod sau comportament.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-06 13:48:38 +00:00
parent 86408887e5
commit 44f261e269
226 changed files with 1313 additions and 1760 deletions

112
CLAUDE.md
View File

@@ -1,104 +1,86 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Ghid Claude Code pentru acest repo.
## Ce este ## Ce este
Gateway web (Python / FastAPI) care preia prezentari de service-auto si le declara la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta). Gateway web (Python/FastAPI) care preia prezentari service-auto si le declara la **RAR AUTOPASS** (L.142/2023, OM 210/2024). Inlocuieste integrarea VFP `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta).
Limba proiectului este **romana**: cod, comentarii, commit-uri, documentatie. Fara emoji in raspunsuri sau fisiere noi (preferinta proiect). ## Stil
## Surse de adevar (citeste-le inainte sa modifici comportament) - Limba proiectului: **romana** (cod, comentarii, commit-uri, docs). Fara emoji.
- **Comentarii scurte, strict functionale.** Doar ce nu reiese din cod. FARA referinte la PRD-uri, stories (US-xxx), review-uri sau istoricul deciziei.
- `docs/api-rar-contract.md` — contractul RAR. **Unde un plan/cod difera de contract, contractul are dreptate.** ## Surse de adevar
- `docs/ROADMAP.md` — singura sursa de progres + procesul de lucru. O sesiune noua porneste de aici. Doar sectiunea "Stadiu Implementare" se modifica pe parcurs; detaliile stau in PRD-uri (`docs/prd/`).
- `docs/api-rar-contract.md` — contractul RAR; la divergenta, contractul are dreptate.
- `docs/ROADMAP.md` — progres + proces de lucru; sesiunile noi pornesc de aici. Se modifica doar "Stadiu Implementare"; detaliile stau in `docs/prd/`.
## Comenzi ## Comenzi
```bash ```bash
pip3 install -r requirements.txt # Python 3.12+ pip3 install -r requirements.txt # Python 3.12+
# Rulare locala (dev): API + worker sunt PROCESE SEPARATE # Dev: API + worker = procese SEPARATE
uvicorn app.main:app --reload --port 8010 # API: dashboard /, Swagger /docs, /healthz, /metrics uvicorn app.main:app --reload --port 8010 # dashboard /, /docs, /healthz, /metrics
python3 -m app.worker # worker (necesar doar pentru a procesa coada) python3 -m app.worker # proceseaza coada
# Wrapper-ul start.sh ambaleaza mediu (test/prod) + rol (api/worker/both/finalizate) # start.sh = mediu (test/prod) + rol (api/worker/both/finalizate); loguri in .run/
./start.sh test both --send # API + worker, trimite efectiv la RAR test (loguri in .run/) ./start.sh test both --send # trimite efectiv la RAR test
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR (verificare independenta) ./start.sh test finalizate # listeaza prezentarile inregistrate la RAR
./start.sh status # stare procese + /healthz ./start.sh status | stop
./start.sh stop # opreste procesele pornite cu "both"
./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul ./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul
# Teste (pytest; folosesc FastAPI TestClient + SQLite temporar). Testele live RAR sunt # Teste (TestClient + SQLite temporar); testele live RAR sunt skip implicit (marker `live`)
# skip implicit (marker `live`) — `pytest -q` nu atinge endpointul real.
python3 -m pytest -q python3 -m pytest -q
python3 -m pytest tests/test_worker_reconcile.py -q # un fisier python3 -m pytest tests/test_worker_reconcile.py::test_x -q
python3 -m pytest tests/test_worker_reconcile.py::test_x -q # un singur test AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # LIVE opt-in: creeaza FINALIZATA; cere settings.xml cu creds <test>
python3 -m pytest -q -m "not live" # exclude explicit testele live
# Test LIVE pe RAR test (opt-in, skip implicit; atinge endpointul real -> creeaza FINALIZATA): # Chei API (admin, doar CLI); cheia rfak_... se afiseaza O SINGURA DATA
# reproduce lantul mapare inline -> queued -> worker -> sent -> verificare in finalizate. python3 -m tools.apikey create --account 2 | list | rotate | revoke
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # necesita settings.xml cu creds <test>
# Lifecycle chei API (admin, doar din CLI — nu exista suprafata HTTP) docker compose up --build # deploy: api + worker + autoheal, acelasi image + volum SQLite
python3 -m tools.apikey create --account 2 # cheie afisata O SINGURA DATA (rfak_...)
python3 -m tools.apikey list|rotate|revoke
# Docker (deploy): api + worker + autoheal, acelasi image + volum SQLite
docker compose up --build
``` ```
## Arhitectura ## Arhitectura
**Doua procese peste acelasi SQLite (WAL) persistent**, care comunica EXCLUSIV prin tabela `submissions`: **Doua procese peste acelasi SQLite (WAL)**, comunica EXCLUSIV prin tabela `submissions`:
- **API** (`app.main:app`) — API v1 (`app/api/v1/router.py` + `import_router.py`), dashboard web HTMX (`app/web/routes.py` + `templates/`), `/healthz`, `/metrics`. Worker-ul **nu** ruleaza ca task aici: un worker mort nu trebuie sa lase containerul "sanatos". - **API** (`app.main:app`) — API v1 (`app/api/v1/`), dashboard HTMX (`app/web/` + `templates/`), `/healthz`, `/metrics`. Worker-ul NU ruleaza aici: un worker mort nu trebuie sa lase containerul "sanatos".
- **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recupereaza orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR (per cont)`postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe randuri `sending` orfane, re-login la JWT expirat (TTL 30h). Send DEZACTIVAT implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`) — sigur pentru probe. - **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recuperare orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR per cont → `postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe `sending` orfane, re-login la JWT expirat (TTL 30h). Send OFF implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`).
**Doua canale de intrare** convergent in coada `submissions`: **Doua canale de intrare** in coada: `POST /v1/prezentari` (ROAAUTO/soft propriu) si import web xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`).
- Canal API (Treapta 1): `POST /v1/prezentari` pentru ROAAUTO / soft propriu.
- Import web (Treapta 2): upload xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`, dashboard).
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard monitorizeaza live. Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard live.
### Invariante critice (usor de stricat) ### Invariante critice
- **`AUTOPASS_CREDS_KEY` trebuie sa fie ACEEASI intre API si worker.** API cripteaza creds RAR (Fernet), worker le decripteaza. Chei diferite → worker nu poate decripta → trimiterile esueaza. `start.sh both` genereaza o cheie efemera partajata; pentru prod pune una persistenta in `.env`. (`crypto.py`) - **`AUTOPASS_CREDS_KEY` identica intre API si worker** (Fernet, `crypto.py`): chei diferite → worker nu decripteaza creds → trimiterile esueaza. `start.sh both` genereaza cheie efemera partajata; in prod, cheie persistenta in `.env`.
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`), pentru ca RAR accepta duplicate si nu are nr. comanda. `build_key` normalizeaza INTOTDEAUNA `account_id` la `account_or_default` (None == 1) INAINTE de hash altfel acelasi rand logic primeste chei diferite pe canalele API vs import (OV-2). `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din coercion Excel) inainte de validare si de cheie. - **Idempotenta = hash de continut canonic** server-side (`idempotency.py`) RAR accepta duplicate, fara nr. comanda. `build_key` normalizeaza `account_id` la `account_or_default` (None == 1) INAINTE de hash, altfel acelasi rand primeste chei diferite pe API vs import. `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din Excel) inainte de validare si cheie.
- **`FINALIZATA` e terminal la RAR** fara anulare/corectie prin API. De aceea reconcilierea anti-duplicat: pe eroare **ambigua** (timeout / TransportError / 502/503/504 / 429 / 408) sau rand `sending` orfan, worker-ul cauta in finalizate (match pe vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara a re-trimite (`reconcile.py`). **EXCEPTIE: un RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) e un esec DEFINITIV (RAR a raspuns „am esuat", nu o pierdere de raspuns) → worker-ul NU reconciliaza si NU reincearca, marcheaza `error` cu mesajul RAR (`RAR_EROARE_SERVER`). Altfel ar marca fals `sent` pe un record PARTIAL pe care RAR (ne-tranzactional) il lasa la esec. - **`FINALIZATA` e terminal la RAR** (fara anulare/corectie). Pe eroare **ambigua** (timeout/TransportError/502/503/504/429/408) sau `sending` orfan, worker-ul cauta in finalizate (vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara re-trimitere (`reconcile.py`). **EXCEPTIE: RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) = esec DEFINITIV → fara reconciliere/retry, `error` cu mesajul RAR (`RAR_EROARE_SERVER`); altfel ar marca fals `sent` un record PARTIAL lasat de RAR (ne-tranzactional).
- **Creds RAR per cont**: durabile in `accounts.rar_creds_enc` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi fallback la cont. Purjarea sterge DOAR `submissions.rar_creds_enc`, NU `accounts.rar_creds_enc`. - **Creds RAR per cont**: durabile in `accounts` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi contul. Purjarea sterge DOAR creds efemere din `submissions`.
- **Auth API-key** (`auth.py`): identifica CONTUL ROAAUTO, separat de credentialele RAR. Stocam doar SHA-256 al cheii. Enforcement prin `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie merge pe cont id=1, cheie invalida → 401; `true` (prod) → cheie obligatorie pe `/v1/*` protejat. POST-urile + rutele de import sunt account-scoped; GET-urile de listare sunt si ele account-scoped (5.15/US-011: fragmentele `_fragments/submissions|trimitere|mapari|status|jurnal|nomenclator|trimiteri-versiune` sub `require_login` + scope, 404-before-leak pe id strain; `GET /v1/prezentari(/{id})`/`/v1/mapari`/`/v1/audit/export` filtrate pe cont). `GET /v1/nomenclator` ramane public intentionat (coduri RAR publice, fara PII). - **Auth API-key** (`auth.py`): identifica contul ROAAUTO (separat de creds RAR); stocam doar SHA-256. `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie = cont id=1, cheie invalida → 401; `true` (prod) → obligatorie pe `/v1/*` protejat. POST-urile, importul si GET-urile de listare (API + fragmente dashboard) sunt account-scoped, 404-before-leak pe id strain. `GET /v1/nomenclator` public intentionat (coduri RAR publice, fara PII).
- **Admin bootstrap fara hardcodare**: la `/signup`, primul user din TOATA baza (`count_admins()==0`, verificat in tranzactie) primeste `is_admin=1` automat; orice signup ulterior e neadmin. `is_admin` da acces la `/admin` (panou global, toate conturile) — verificat pe `account_id`-ul sesiunii, dar neconditionat DE care `account_id` a primit userul (semnul de "cine e admin" nu are legatura cu contul `id=1`). Contul `id=1` e doar bucket-ul de fallback pentru trafic API neautentificat in dev (`AUTOPASS_REQUIRE_API_KEY=false`) — separat de conceptul de admin. Fix manual daca primul signup s-a consumat deja gresit: `python3 -m tools.account set-admin --account N`. - **Admin bootstrap**: primul user din TOATA baza la `/signup` (`count_admins()==0`, in tranzactie) devine `is_admin=1`; restul nu. `is_admin` da acces la `/admin` (panou global) si e independent de contul id=1 (acela e doar fallback dev pentru API neautentificat). Fix manual: `python3 -m tools.account set-admin --account N`.
- **Mapare coloane retinuta per `(account_id, signature_coloane)`** (`column_mappings`): la urmatorul fisier cu aceleasi coloane, pentru acelasi cont, maparea se reaplica automat. Un cont poate avea mai multe formate memorate simultan. - **Mapare coloane memorata per `(account_id, signature_coloane)`** (`column_mappings`): reaplicata automat la fisiere cu aceleasi coloane; un cont poate avea mai multe formate.
- **Mapare operatie→cod**: prestatie poate veni cu `cod_prestatie` (cod RAR direct) sau `cod_op_service` (cod intern) + `denumire`. Nerezolvat → submission `needs_mapping` (nu se trimite), apare in editorul web cu sugestie fuzzy; la salvarea maparii se re-rezolva automat submission-urile blocate. - **Mapare operatie→cod**: prestatia vine cu `cod_prestatie` (cod RAR) sau `cod_op_service` + `denumire`. Nerezolvat → `needs_mapping` (nu se trimite), editor web cu sugestie fuzzy; salvarea maparii re-rezolva automat submission-urile blocate.
- **`cod_prestatie` e VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): un cod direct NECUNOSCUT in nomenclator NU se mai trimite raw — e promovat la `cod_op_service` (denumire=cod) si tratat ca operatie de mapat. Motiv (confirmat live 2026-06-23): RAR accepta NUMAI coduri din nomenclator (coloana `COD_PRESTATIE` max 5 car.); un cod necunoscut da **HTTP 500** (`ORA-12899`), iar RAR **NU e tranzactional** lasa un record PARTIAL `FINALIZATA` (terminal) chiar pe esec, pe care reconcilierea worker-ului l-ar marca fals `sent`. Comportamentul la cod necunoscut/nemapat: `on_unmapped_error` (camp boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` (intra in editor, `needs_mapping`) sau `true` (respinge fara enqueue `submission_id=null` + `erori`). Default = `accounts.on_unmapped_error_default` (implicit `false`/`0`); precedenta cerere > cont > `false`. - **`cod_prestatie` VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): cod necunoscut NU se trimite raw — e promovat la `cod_op_service` (denumire=cod) si intra la mapat. Motiv (confirmat live): RAR accepta doar coduri din nomenclator (max 5 car.); cod necunoscut HTTP 500 `ORA-12899`, iar RAR ne-tranzactional lasa record PARTIAL `FINALIZATA` pe care reconcilierea l-ar marca fals `sent`. La cod nemapat: `on_unmapped_error` (boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` editor/`needs_mapping`; `true` respins fara enqueue (`submission_id=null` + `erori`). Precedenta: cerere > `accounts.on_unmapped_error_default` > `false`.
- **WAF RAR da 403 fara User-Agent de browser** — toate apelurile httpx trimit `User-Agent: Mozilla/5.0` (`config.py`, confirmat live). - **WAF RAR da 403 fara User-Agent de browser** — httpx trimite mereu `User-Agent: Mozilla/5.0` (`config.py`).
- **422 fara echo de credentiale**: handler-ul global de validare in `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel ar reflecta `rar_credentials.password`). - **422 fara echo de credentiale**: handler-ul din `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel reflecta `rar_credentials.password`).
- **Retentie**: `submissions` sent + `import_batches` primesc `purge_after = now + 90 zile`; worker-ul purjeaza odata pe ora (T16, GDPR/L.142). - **Retentie**: `submissions` sent + `import_batches` `purge_after = now + 90 zile`; worker purjeaza orar (GDPR/L.142).
### Masina de stari submissions ### Masina de stari submissions
`queued → sending → sent` (succes, cu `id_prezentare` de la RAR). Ramuri: `needs_mapping` (cod nerezolvat), `needs_data` (RAR 400, validare continut), `error` (max retries / 4xx nerecuperabil / **RAR 500 cu mesaj — esec definitiv** / creds invalide / login 401 — NU se face retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema completa: `app/schema.sql`. `queued → sending → sent` (cu `id_prezentare` de la RAR). Ramuri: `needs_mapping`, `needs_data` (RAR 400), `error` (max retries / 4xx nerecuperabil / RAR 500 cu mesaj / creds invalide sau login 401 — fara retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema: `app/schema.sql`.
## Mod non-interactiv ## Mod non-interactiv
Vezi `/workspace/CLAUDE.md` (workspace-level): cand esti lansat cu `claude -p`, creeaza fisiere noi DOAR in `/workspace/.claude-work/<task>/`, nu in `/workspace`. Modificarile la fisiere existente se fac in locatia originala. Vezi `/workspace/CLAUDE.md`: cu `claude -p`, fisierele noi merg DOAR in `/workspace/.claude-work/<task>/`; modificarile la fisiere existente raman in locatia originala.
## Skill routing ## Skill routing
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill. Cand cererea se potriveste unui skill, invoca-l prin Skill tool; la indoiala, invoca-l.
Key routing rules: - Idei produs → /office-hours · Strategie → /plan-ceo-review · Arhitectura → /plan-eng-review
- Product ideas/brainstorming → invoke /office-hours - Design system/plan → /design-consultation, /plan-design-review · Pipeline complet → /autoplan
- Strategy/scope → invoke /plan-ceo-review - Bug-uri → /investigate · QA site → /qa, /qa-only · Code review → /review · Polish vizual → /design-review
- Architecture → invoke /plan-eng-review - Ship/deploy/PR → /ship, /land-and-deploy · Salvare/reluare context → /context-save, /context-restore · Spec → /spec
- Design system/plan review → invoke /design-consultation or /plan-design-review
- Full review pipeline → invoke /autoplan
- Bugs/errors → invoke /investigate
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec

View File

@@ -67,7 +67,7 @@ def create_account(
# Planul cerut: pastram doar valori valide; orice altceva -> NULL (defensiv). # Planul cerut: pastram doar valori valide; orice altceva -> NULL (defensiv).
req_plan = requested_plan if requested_plan in VALID_TIERS else None req_plan = requested_plan if requested_plan in VALID_TIERS else None
try: try:
# Trial Pro automat la creare (PRD 5.17 US-001): tier='free' + trial_until=now+30z. # Trial Pro automat la creare: tier='free' + trial_until=now+30z.
trial_until = ( trial_until = (
(datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S") (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
) )
@@ -151,7 +151,7 @@ def set_status(conn: sqlite3.Connection, account_id: int, status: str) -> None:
def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool: def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool:
"""Comutatorul Auto per cont (PRD 5.19 US-001): `accounts.auto_send_enabled` ca bool. """Comutatorul Auto per cont: `accounts.auto_send_enabled` ca bool.
Cont inexistent -> False (Auto OFF: sigur, tine randurile). Nu comita (ca set_active). Cont inexistent -> False (Auto OFF: sigur, tine randurile). Nu comita (ca set_active).
""" """
@@ -164,7 +164,7 @@ def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool:
def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> None: def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> None:
"""Comuta `accounts.auto_send_enabled` (PRD 5.19 US-001). Idempotent (set pe aceeasi """Comuta `accounts.auto_send_enabled`. Idempotent (set pe aceeasi
valoare nu arunca). Scoped pe cont. Nu comita — respecta conventia fisierului (apelantul valoare nu arunca). Scoped pe cont. Nu comita — respecta conventia fisierului (apelantul
comita, ca la set_active/set_status). Cont inexistent -> UPDATE fara efect (no-op sigur). comita, ca la set_active/set_status). Cont inexistent -> UPDATE fara efect (no-op sigur).
""" """
@@ -175,8 +175,8 @@ def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> N
def held_for_account(conn: sqlite3.Connection, account_id: int) -> int: def held_for_account(conn: sqlite3.Connection, account_id: int) -> int:
"""Snapshot-ul `held` la ingestie (PRD 5.19 US-001). SURSA UNICA (chokepoint) folosita de """Snapshot-ul `held` la ingestie. SURSA UNICA (chokepoint) folosita de
TOATE situ-rile de ingestie care scriu `status='queued'` (US-002). TOATE situ-rile de ingestie care scriu `status='queued'`.
Intoarce 0 (nu tine) daca `accounts.auto_send_enabled=1`, altfel 1 (tine randul). Cont Intoarce 0 (nu tine) daca `accounts.auto_send_enabled=1`, altfel 1 (tine randul). Cont
inexistent -> 1 (sigur: tine randul, nu-l trimite fara comutator explicit). inexistent -> 1 (sigur: tine randul, nu-l trimite fara comutator explicit).
@@ -215,7 +215,7 @@ def set_tier(
"UPDATE accounts SET tier=?, trial_until=? WHERE id=?", "UPDATE accounts SET tier=?, trial_until=? WHERE id=?",
(tier, trial_until, account_id), (tier, trial_until, account_id),
) )
# Audit in app_events (decizie PRD 5.17 US-008, fara PII nou) # Audit in app_events (fara PII nou)
try: try:
from .observ import log_event from .observ import log_event
log_event( log_event(

View File

@@ -1,4 +1,4 @@
"""Router import Treapta 2 — upload fisier xlsx/csv, preview, commit, export. """Router import — upload fisier xlsx/csv, preview, commit, export.
Endpointuri: Endpointuri:
POST /v1/import — upload fisier, staging in import_batches/import_rows POST /v1/import — upload fisier, staging in import_batches/import_rows
@@ -190,7 +190,7 @@ def _resolve_row_for_preview(
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val) denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}] mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
# obs derive-on-empty (D7/E3 PRD 5.15): daca obs e gol si avem operatie, # obs derive-on-empty: daca obs e gol si avem operatie,
# copiem denumirea operatiei in obs (nu o mutam — op_service ramane neatins). # copiem denumirea operatiei in obs (nu o mutam — op_service ramane neatins).
# DERIVE-ON-EMPTY: doar cand obs e gol, ca sa fie idempotent la re-preview/re-editare. # DERIVE-ON-EMPTY: doar cand obs e gol, ca sa fie idempotent la re-preview/re-editare.
obs_curent = str(mapped.get("obs") or "").strip() obs_curent = str(mapped.get("obs") or "").strip()
@@ -236,7 +236,7 @@ def _resolve_row_for_preview(
if all_flags and not reviewed: if all_flags and not reviewed:
# needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala. # needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala.
# Daca reviewed=True (operatorul a confirmat explicit valorile in modal), sarim # Daca reviewed=True (operatorul a confirmat explicit valorile in modal), sarim
# acest return si continuam spre ok/needs_data (US-007, PRD 5.12). # acest return si continuam spre ok/needs_data.
return { return {
"resolved_status": "needs_review", "resolved_status": "needs_review",
"resolved": mapped, "resolved": mapped,
@@ -244,8 +244,6 @@ def _resolve_row_for_preview(
"flags": all_flags, "flags": all_flags,
} }
# US-001 (PRD 5.11): ramura auto_send eliminata din preview.
if errors: if errors:
return { return {
"resolved_status": "needs_data", "resolved_status": "needs_data",
@@ -361,7 +359,7 @@ def apply_row_override(
new_override = _merge_override(current, fields) new_override = _merge_override(current, fields)
enc = encrypt_creds(new_override) if new_override else None enc = encrypt_creds(new_override) if new_override else None
# D#9 (PRD 5.12): resetam reviewed=0 la orice schimbare de valoare — operatorul # Resetam reviewed=0 la orice schimbare de valoare — operatorul
# trebuie sa reconfirme dupa editare. NU conditionam pe reviewed curent: orice override # trebuie sa reconfirme dupa editare. NU conditionam pe reviewed curent: orice override
# (chiar si revert la valoarea initiala) anuleaza confirmarea implicita. # (chiar si revert la valoarea initiala) anuleaza confirmarea implicita.
conn.execute("UPDATE import_rows SET override_json=?, reviewed=0 WHERE id=?", (enc, row["rid"])) conn.execute("UPDATE import_rows SET override_json=?, reviewed=0 WHERE id=?", (enc, row["rid"]))
@@ -965,7 +963,7 @@ def commit_import(
if batch["status"] == "committed": if batch["status"] == "committed":
raise HTTPException(status_code=409, detail="batch deja comis") raise HTTPException(status_code=409, detail="batch deja comis")
# D#8 (PRD 5.12): gate commit derivat din DB `reviewed` pe AMBELE canale. # Gate commit derivat din DB `reviewed` pe AMBELE canale.
# API: reviewed_rows pastrat (contract stabil) dar seteaza reviewed=1 in DB inainte # API: reviewed_rows pastrat (contract stabil) dar seteaza reviewed=1 in DB inainte
# de interogare. Randurile needs_review cu reviewed=1 sunt incluse in comit. # de interogare. Randurile needs_review cu reviewed=1 sunt incluse in comit.
if req.reviewed_rows: if req.reviewed_rows:
@@ -1032,7 +1030,7 @@ def commit_import(
if n_total_ok == 0: if n_total_ok == 0:
raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.") raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.")
# Rezolva mediul RAR tinta al lotului (US-009): cerut > default cont > ancora globala. # Rezolva mediul RAR tinta al lotului: cerut > default cont > ancora globala.
try: try:
env = rezolva_rar_env(conn, account_id, req.rar_env) env = rezolva_rar_env(conn, account_id, req.rar_env)
except ValueError as e: except ValueError as e:
@@ -1044,8 +1042,8 @@ def commit_import(
"disponibile": e.disponibile, "disponibile": e.disponibile,
}) })
# T3 (PRD 5.17): enforce volum plan — INAINTE de enqueue (invariant idempotenta). # Enforce volum plan — INAINTE de enqueue (invariant idempotenta).
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut). # Respingere TOTALA a lotului (nu enqueue partial tacut).
from ...config import get_settings as _get_settings from ...config import get_settings as _get_settings
from ...plans import PLANS, effective_tier, monthly_usage from ...plans import PLANS, effective_tier, monthly_usage
from ...observ import log_event as _log_event_plan from ...observ import log_event as _log_event_plan
@@ -1126,7 +1124,7 @@ def commit_import(
conn.execute("BEGIN IMMEDIATE") conn.execute("BEGIN IMMEDIATE")
try: try:
purge_after_sql = "datetime('now', '+90 days')" purge_after_sql = "datetime('now', '+90 days')"
# PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic # Snapshot `held` din comutatorul contului (chokepoint unic
# held_for_account) — 0 daca Auto ON, 1 daca Auto OFF. Per-cont -> o data. # held_for_account) — 0 daca Auto ON, 1 daca Auto OFF. Per-cont -> o data.
held_val = held_for_account(conn, acct) held_val = held_for_account(conn, acct)
@@ -1162,7 +1160,7 @@ def commit_import(
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val) denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}] mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
# obs derive-on-empty (D7/E3 PRD 5.15): copiere denumire in obs daca obs e gol. # obs derive-on-empty: copiere denumire in obs daca obs e gol.
# Identic cu logica din _resolve_row_for_preview (override aplicat tot ultimul). # Identic cu logica din _resolve_row_for_preview (override aplicat tot ultimul).
obs_curent = str(mapped.get("obs") or "").strip() obs_curent = str(mapped.get("obs") or "").strip()
if not obs_curent and operatie_val: if not obs_curent and operatie_val:
@@ -1272,7 +1270,7 @@ def commit_import(
class RandEditIn(BaseModel): class RandEditIn(BaseModel):
"""Campuri de continut editabile in preview. None = ne-trimis (neschimbat); """Campuri de continut editabile in preview. None = ne-trimis (neschimbat);
"" = sterge override-ul (revine la valoarea din fisier). "" = sterge override-ul (revine la valoarea din fisier).
obs = text liber fara validare de continut (US-005 PRD 5.15).""" obs = text liber fara validare de continut."""
vin: str | None = None vin: str | None = None
nr_inmatriculare: str | None = None nr_inmatriculare: str | None = None
data_prestatie: str | None = None data_prestatie: str | None = None

View File

@@ -57,7 +57,7 @@ def ping(
conn.close() conn.close()
autentificat_cu_cheie = acct is not None autentificat_cu_cheie = acct is not None
# Verificam daca contul are creds RAR stocate (per-env, US-013). # Verificam daca contul are creds RAR stocate (per mediu).
aid = account_or_default(account_id) aid = account_or_default(account_id)
conn = get_connection() conn = get_connection()
try: try:

View File

@@ -88,7 +88,7 @@ def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
] ]
# PRD 5.19 US-010: mesaj uman pentru un rand tinut manual (queued AND held=1). # Mesaj uman pentru un rand tinut manual (queued AND held=1).
# Reutilizeaza vocabularul existent AUTO_SEND_OPRIT (errors.py) — nu inventam al treilea # Reutilizeaza vocabularul existent AUTO_SEND_OPRIT (errors.py) — nu inventam al treilea
# vocabular "auto_send". Onestitate: raspunsul NU trebuie sa arate un "queued" curat fals. # vocabular "auto_send". Onestitate: raspunsul NU trebuie sa arate un "queued" curat fals.
MOTIV_HELD = ( MOTIV_HELD = (
@@ -107,8 +107,7 @@ def _motiv_clasificare(cl: dict) -> str | None:
None cand status='queued'. Acopera ramurile de blocaj: erori de continut None cand status='queued'. Acopera ramurile de blocaj: erori de continut
(needs_data) si coduri fara mapare RAR (needs_mapping). (needs_data) si coduri fara mapare RAR (needs_mapping).
Dupa US-001: needs_mapping apare EXCLUSIV cand unmapped e non-gol needs_mapping apare EXCLUSIV cand unmapped e non-gol.
(ramura auto_send_oprit era inaccesibila si a fost eliminata).
""" """
if cl["status"] == "queued": if cl["status"] == "queued":
return None return None
@@ -127,7 +126,7 @@ def _rezultat_enqueue(submission_id: int | None, cl: dict, held: int = 0, **extr
Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman) Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman)
pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None, EXCEPTAND pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None, EXCEPTAND
randurile tinute manual (PRD 5.19 US-010: `held=1` -> held=true + motiv non-null). randurile tinute manual (`held=1` -> held=true + motiv non-null).
""" """
tinut = bool(held) tinut = bool(held)
motiv = _motiv_clasificare(cl) or _motiv_held(cl["status"], tinut) motiv = _motiv_clasificare(cl) or _motiv_held(cl["status"], tinut)
@@ -179,7 +178,7 @@ def create_prezentari(
conn = get_connection() conn = get_connection()
results: list[SubmissionResult] = [] results: list[SubmissionResult] = []
try: try:
# load_mapping_meta incarca maparea op->cod RAR; dupa US-001, auto_send nu mai tine randuri. # load_mapping_meta incarca maparea op->cod RAR; auto_send nu mai tine randuri.
mapping_meta = load_mapping_meta(conn, acct) mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()} mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat. # Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
@@ -189,12 +188,12 @@ def create_prezentari(
text_rules = load_text_rules(conn, acct) text_rules = load_text_rules(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error) error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
# US-005: rezolva mediul RAR tinta (cerut > default cont > ancora globala). # Rezolva mediul RAR tinta (cerut > default cont > ancora globala).
# MediuIndisponibil -> 422 inainte de orice enqueue (respinge tot lotul). # MediuIndisponibil -> 422 inainte de orice enqueue (respinge tot lotul).
try: try:
env = rezolva_rar_env(conn, acct, req.rar_env) env = rezolva_rar_env(conn, acct, req.rar_env)
except MediuIndisponibil as e: except MediuIndisponibil as e:
# US-012: audit blocare mediu indisponibil (tip='rar_env_blocat'). # Audit blocare mediu indisponibil (tip='rar_env_blocat').
log_event( log_event(
"rar_env_blocat", "rar_env_blocat",
nivel="WARNING", nivel="WARNING",
@@ -220,8 +219,8 @@ def create_prezentari(
detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"), detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"),
) )
# T3 (PRD 5.17): enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta). # Enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta).
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut). # Respingere TOTALA a lotului (nu enqueue partial tacut).
from ...config import get_settings as _get_settings from ...config import get_settings as _get_settings
from ...plans import PLANS, effective_tier, monthly_usage from ...plans import PLANS, effective_tier, monthly_usage
_settings = _get_settings() _settings = _get_settings()
@@ -259,8 +258,8 @@ def create_prezentari(
), ),
) )
# PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic # Snapshot `held` din comutatorul contului (chokepoint unic held_for_account).
# held_for_account). 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de # 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de
# cont, nu de rand -> calculat o data. NU intra in payload/idempotenta. # cont, nu de rand -> calculat o data. NU intra in payload/idempotenta.
held_val = held_for_account(conn, acct) held_val = held_for_account(conn, acct)
@@ -291,9 +290,8 @@ def create_prezentari(
# on_unmapped_error=True: nu reactivam; randul ramane 'error'. # on_unmapped_error=True: nu reactivam; randul ramane 'error'.
results.append(_rezultat_respins(existing["id"], cl, rar_env=env)) results.append(_rezultat_respins(existing["id"], cl, rar_env=env))
continue continue
# PRD 5.19 US-002 (Eng Finding A — bug de bypass): la reactivare # La reactivare re-snapshot-am `held` din comutatorul contului. Fara
# re-snapshot-am `held` din comutatorul contului. Fara asta randul # asta randul pastra held VECHI si se auto-trimitea desi contul e Auto OFF.
# pastra held VECHI si se auto-trimitea desi contul e Auto OFF.
cur = conn.execute( cur = conn.execute(
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, " "UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
"rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, " "rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, "
@@ -304,7 +302,7 @@ def create_prezentari(
) )
if cur.rowcount == 1: if cur.rowcount == 1:
# Creds noi se propaga si in slotul durabil per-env al contului # Creds noi se propaga si in slotul durabil per-env al contului
# — ambele canale converg pe parola corectata (US-013, env-aware). # — ambele canale converg pe parola corectata (env-aware).
if req.rar_credentials is not None: if req.rar_credentials is not None:
conn.execute( conn.execute(
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?", f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
@@ -321,7 +319,7 @@ def create_prezentari(
"SELECT id, status, id_prezentare, held FROM submissions WHERE id=?", "SELECT id, status, id_prezentare, held FROM submissions WHERE id=?",
(existing["id"],), (existing["id"],),
).fetchone() ).fetchone()
# Echo onest pe dedup (PRD 5.19 US-002/US-010): propaga `held` al randului # Echo onest pe dedup: propaga `held` al randului
# existent — un rand tinut NU trebuie sa apara ca un "queued" curat fals. # existent — un rand tinut NU trebuie sa apara ca un "queued" curat fals.
dedup_held = bool(existing["held"]) dedup_held = bool(existing["held"])
results.append( results.append(
@@ -338,13 +336,13 @@ def create_prezentari(
continue continue
# Helper pur partajat cu dry-run: reproduce EXACT clasificarea # Helper pur partajat cu dry-run: reproduce EXACT clasificarea
# (canonicalize + mapare op->cod + validare; auto_send gate eliminat dupa US-001). # (canonicalize + mapare op->cod + validare; fara gate auto_send).
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules) cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
if cl["blocked_error"]: if cl["blocked_error"]:
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat). # on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
results.append(_rezultat_respins(None, cl, rar_env=env)) results.append(_rezultat_respins(None, cl, rar_env=env))
continue continue
# PRD 5.19 US-002: `held` = snapshot comutator cont (chokepoint held_for_account). # `held` = snapshot comutator cont (chokepoint held_for_account).
cur = conn.execute( cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc, rar_env, held) " "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc, rar_env, held) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
@@ -401,11 +399,11 @@ def valideaza_prezentari(
text_rules = load_text_rules(conn, acct) text_rules = load_text_rules(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error) error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
# US-005 (DX F5): rezolva env identic ca trimiterea reala si ecou-ieste in raspuns. # Rezolva env identic ca trimiterea reala si ecou-ieste in raspuns.
try: try:
env = rezolva_rar_env(conn, acct, req.rar_env) env = rezolva_rar_env(conn, acct, req.rar_env)
except MediuIndisponibil as e: except MediuIndisponibil as e:
# US-012: audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat'). # Audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat').
log_event( log_event(
"rar_env_blocat", "rar_env_blocat",
nivel="WARNING", nivel="WARNING",
@@ -464,7 +462,7 @@ def list_prezentari(
scope_sql, scope_params = account_scope_clause(account_id) scope_sql, scope_params = account_scope_clause(account_id)
# payload_json e plaintext (vezi submissions.payload_json); il citim doar ca # payload_json e plaintext (vezi submissions.payload_json); il citim doar ca
# sa derivam campurile afisabile prin helper-ul partajat, nu il expunem. # sa derivam campurile afisabile prin helper-ul partajat, nu il expunem.
# rar_env inclus (US-005): badge mediu in lista. # rar_env inclus: badge mediu in lista.
cols = ( cols = (
"id, status, id_prezentare, rar_status_code, retry_count, " "id, status, id_prezentare, rar_status_code, retry_count, "
"created_at, updated_at, payload_json, rar_env, held" "created_at, updated_at, payload_json, rar_env, held"
@@ -502,9 +500,9 @@ _PREZENTARE_FIELDS = frozenset({
# erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza # erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza
# "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API. # "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API.
"rar_error", "rar_error",
# US-005: mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API. # Mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API.
"rar_env", "rar_env",
# PRD 5.19 US-010: flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat). # Flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat).
"held", "held",
}) })
@@ -589,7 +587,7 @@ def trimite_acum_prezentare(
submission_id: int, submission_id: int,
account_id: int = Depends(resolve_account_id), account_id: int = Depends(resolve_account_id),
) -> dict: ) -> dict:
"""Elibereaza manual un rand tinut (PRD 5.19 US-010, paritate API cu /repune). """Elibereaza manual un rand tinut (paritate API cu /repune).
Scoped strict pe contul cheii API (account_id din sesiune/cheie, NICIODATA din body). Scoped strict pe contul cheii API (account_id din sesiune/cheie, NICIODATA din body).
404-before-leak pe id strain/inexistent. `held: 1 -> 0` DOAR daca randul e 404-before-leak pe id strain/inexistent. `held: 1 -> 0` DOAR daca randul e
@@ -639,7 +637,7 @@ AUDIT_COLUMNS = [
"odometru_final", "odometru_final",
"prestatii", "prestatii",
"rar_status_code", "rar_status_code",
# US-010 (PRD 5.20): mediul RAR tinta per trimitere. # Mediul RAR tinta per trimitere.
"rar_env", "rar_env",
"created_at", "created_at",
"updated_at", "updated_at",
@@ -694,7 +692,7 @@ def _audit_rows(conn, date_from: str | None, date_to: str | None, status: str, a
"odometru_final": p.get("odometru_final") or "", "odometru_final": p.get("odometru_final") or "",
"prestatii": codes, "prestatii": codes,
"rar_status_code": r["rar_status_code"] or "", "rar_status_code": r["rar_status_code"] or "",
# US-010 (PRD 5.20): mediul RAR tinta — coloana audit. # Mediul RAR tinta — coloana audit.
"rar_env": r["rar_env"] or "", "rar_env": r["rar_env"] or "",
"created_at": r["created_at"], "created_at": r["created_at"],
"updated_at": r["updated_at"], "updated_at": r["updated_at"],
@@ -826,7 +824,7 @@ def set_rar_creds(
req: RarCredsIn, req: RarCredsIn,
account_id: int = Depends(resolve_account_id), account_id: int = Depends(resolve_account_id),
) -> dict: ) -> dict:
"""Seteaza creds RAR durabile per-cont, in slotul per-mediu (US-013, env-aware). """Seteaza creds RAR durabile per-cont, in slotul per-mediu (env-aware).
Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV). Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV).
Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca
@@ -855,7 +853,7 @@ def delete_rar_creds(
account_id: int = Depends(resolve_account_id), account_id: int = Depends(resolve_account_id),
env: str | None = None, env: str | None = None,
) -> dict: ) -> dict:
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1). """Sterge creds RAR durabile per-cont (revenire la modelul efemer per submission).
`env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l. `env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l.
Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat). Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat).

View File

@@ -168,7 +168,7 @@ def resolve_account_id(
def require_api_access( def require_api_access(
account_id: int = Depends(resolve_account_id), account_id: int = Depends(resolve_account_id),
) -> int: ) -> int:
"""Dependency FastAPI (T4, PRD 5.17): verifica ca tier-ul efectiv permite accesul la API. """Dependency FastAPI: verifica ca tier-ul efectiv permite accesul la API.
Reguli: Reguli:
- enforce_plans=False (kill-switch): sare verificarea. - enforce_plans=False (kill-switch): sare verificarea.

View File

@@ -35,10 +35,10 @@ class Settings(BaseSettings):
# Retentie randuri blocate (error/needs_data/needs_mapping). Mai scurt decat 90z # Retentie randuri blocate (error/needs_data/needs_mapping). Mai scurt decat 90z
# ale `sent` — un blocat n-are valoare de audit. # ale `sent` — un blocat n-are valoare de audit.
blocked_retention_days: int = 30 blocked_retention_days: int = 30
# Prag zile (PRD 5.19 US-007): randurile tinute manual (`queued AND held=1`) mai vechi de # Prag zile: randurile tinute manual (`queued AND held=1`) mai vechi de
# atat declanseaza bannerul de avertizare L.142 (declararea are termen legal). # atat declanseaza bannerul de avertizare L.142 (declararea are termen legal).
held_warn_days: int = 7 held_warn_days: int = 7
# Retentie randuri tinute manual (PRD 5.19 US-008): un `queued AND held=1` mai vechi de atat # Retentie randuri tinute manual: un `queued AND held=1` mai vechi de atat
# expira la `error` (`TINUT_EXPIRAT`) + purge_after direct. Aliniat retentiei `sent` (90z). # expira la `error` (`TINUT_EXPIRAT`) + purge_after direct. Aliniat retentiei `sent` (90z).
held_retention_days: int = 90 held_retention_days: int = 90
@@ -67,7 +67,7 @@ class Settings(BaseSettings):
# False (dev): cookie fara Secure, functioneaza pe HTTP. # False (dev): cookie fara Secure, functioneaza pe HTTP.
session_https_only: bool = False session_https_only: bool = False
# --- Contact suport (US-001, PRD 5.12) --- # --- Contact suport ---
# Email/canal de contact afisat in mesaje catre utilizatori (ex. CUI duplicat la signup). # Email/canal de contact afisat in mesaje catre utilizatori (ex. CUI duplicat la signup).
# Nesetat -> fallback la formularea generica fara canal concret. # Nesetat -> fallback la formularea generica fara canal concret.
support_email: str | None = None support_email: str | None = None
@@ -115,14 +115,13 @@ class Settings(BaseSettings):
worker_retry_max_s: int = 300 worker_retry_max_s: int = 300
worker_max_retries: int = 8 # peste atat -> error + banner worker_max_retries: int = 8 # peste atat -> error + banner
# --- Planuri de cont (PRD 5.17) --- # --- Planuri de cont ---
# Enforcement DUR al limitelor de plan (volum + acces API). True (implicit) = activ. # Enforcement DUR al limitelor de plan (volum + acces API). True (implicit) = activ.
# False = kill-switch de operare: sare toate gate-urile de plan (util pentru debugging # False = kill-switch de operare: sare toate gate-urile de plan (util pentru debugging
# sau rollback rapid fara revert de cod). Enforcement DUR e activ implicit de la deploy # sau rollback rapid fara revert de cod). Nu exista conturi legacy, produs in teste.
# (decizie user 2026-06-28, decizia #22 autoplan): nu exista conturi legacy, produs in TESTE.
enforce_plans: bool = True enforce_plans: bool = True
# --- Embeddings (sugestie mapare, Stratul 2 PRD 5.14) --- # --- Embeddings (sugestie mapare) ---
# ACTIVAT implicit: editorul de mapari ofera sugestii semantice (model fastembed/ONNX). # ACTIVAT implicit: editorul de mapari ofera sugestii semantice (model fastembed/ONNX).
# Cost: prima folosire lazy-load-eaza modelul (~230MB pe disc) sincron in thread-ul de # Cost: prima folosire lazy-load-eaza modelul (~230MB pe disc) sincron in thread-ul de
# cerere -> prima cerere /mapari poate dura 30-120s pana modelul intra in memorie; cererile # cerere -> prima cerere /mapari poate dura 30-120s pana modelul intra in memorie; cererile
@@ -131,7 +130,7 @@ class Settings(BaseSettings):
# /mapari instant la prima cerere sau suita de teste rapida (cade pe GOLD/SILVER+fuzzy). # /mapari instant la prima cerere sau suita de teste rapida (cade pe GOLD/SILVER+fuzzy).
embeddings_enabled: bool = True embeddings_enabled: bool = True
# --- Seed corpus operatii etichetate (SILVER, PRD 5.18 US-004) --- # --- Seed corpus operatii etichetate (SILVER) ---
# ACTIVAT implicit: la init_db, populeaza mapping_suggestions din artefactul comis # ACTIVAT implicit: la init_db, populeaza mapping_suggestions din artefactul comis
# `app/data/operatii-etichetate.json` (INSERT OR IGNORE). Asa SILVER nu mai e gol in # `app/data/operatii-etichetate.json` (INSERT OR IGNORE). Asa SILVER nu mai e gol in
# productie -> sugestii exact-match + corpus k-NN reale. SUGGESTION-ONLY. # productie -> sugestii exact-match + corpus k-NN reale. SUGGESTION-ONLY.

View File

@@ -37,7 +37,7 @@ def init_db() -> None:
from .mapping import seed_nomenclator_if_empty from .mapping import seed_nomenclator_if_empty
seed_nomenclator_if_empty(conn) seed_nomenclator_if_empty(conn)
# Seed corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004). # Seed corpus operatii etichetate -> mapping_suggestions (SILVER).
# Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent. # Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent.
# DOAR daca mapping_suggestions e gol: seedul are ~17k randuri; re-rularea lui pe # DOAR daca mapping_suggestions e gol: seedul are ~17k randuri; re-rularea lui pe
# FIECARE boot (API + worker concurent) tinea write-lock-ul indelung -> al doilea # FIECARE boot (API + worker concurent) tinea write-lock-ul indelung -> al doilea
@@ -72,9 +72,9 @@ def _migrate(conn: sqlite3.Connection) -> None:
if "row_index" not in sub_cols: if "row_index" not in sub_cols:
conn.execute("ALTER TABLE submissions ADD COLUMN row_index INTEGER") conn.execute("ALTER TABLE submissions ADD COLUMN row_index INTEGER")
if "rar_env" not in sub_cols: if "rar_env" not in sub_cols:
# PRD 5.20 US-001. Mediul RAR tinta pe submission. Pe DB existent NU lasam # Mediul RAR tinta pe submission. Pe DB existent NU lasam
# randurile pe DEFAULT 'test': un rand prod pre-migrare etichetat 'test' ar fi # randurile pe DEFAULT 'test': un rand prod pre-migrare etichetat 'test' ar fi
# reconciliat de worker (US-006) contra endpoint TEST -> no-match -> re-send prod # reconciliat de worker contra endpoint TEST -> no-match -> re-send prod
# = DUPLICAT REAL IREVERSIBIL. Backfill din AUTOPASS_RAR_ENV global (ancora de # = DUPLICAT REAL IREVERSIBIL. Backfill din AUTOPASS_RAR_ENV global (ancora de
# migrare) + recompute idempotency_key env-aware. Ruleaza O SINGURA DATA (in # migrare) + recompute idempotency_key env-aware. Ruleaza O SINGURA DATA (in
# blocul de adaugare a coloanei); pe DB fresh coloana vine din schema.sql (fara rows). # blocul de adaugare a coloanei); pe DB fresh coloana vine din schema.sql (fara rows).
@@ -84,7 +84,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
) )
_backfill_submissions_rar_env(conn) _backfill_submissions_rar_env(conn)
if "held" not in sub_cols: if "held" not in sub_cols:
# PRD 5.19 US-001: flag rand tinut manual. Default 0 (randurile pre-migrare nu erau # Flag rand tinut manual. Default 0 (randurile pre-migrare nu erau
# tinute — pastreaza comportamentul; comutatorul de cont e OFF/held la ingestie NOUA). # tinute — pastreaza comportamentul; comutatorul de cont e OFF/held la ingestie NOUA).
conn.execute( conn.execute(
"ALTER TABLE submissions ADD COLUMN held INTEGER NOT NULL DEFAULT 0 " "ALTER TABLE submissions ADD COLUMN held INTEGER NOT NULL DEFAULT 0 "
@@ -93,11 +93,11 @@ def _migrate(conn: sqlite3.Connection) -> None:
# Coloane accounts # Coloane accounts
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()} acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
# AUTO-FIX 6a ELIMINAT (US-013): NU mai adaugam accounts.rar_creds_enc — coloana e dropata. # NU mai adaugam accounts.rar_creds_enc — coloana e dropata.
# _migrate_accounts_medii gestioneaza absenta coloana (guard la ~219: if "rar_creds_enc" not in acc_cols: return). # _migrate_accounts_medii gestioneaza absenta coloana (guard: if "rar_creds_enc" not in acc_cols: return).
# Medii RAR per cont (PRD 5.20 US-001): activare + slot creds + default, per mediu. # Medii RAR per cont: activare + slot creds + default, per mediu.
_migrate_accounts_medii(conn, acc_cols) _migrate_accounts_medii(conn, acc_cols)
# US-013: DROP coloana legacy accounts.rar_creds_enc dupa backfill complet. # DROP coloana legacy accounts.rar_creds_enc dupa backfill complet.
_drop_legacy_accounts_rar_creds(conn, acc_cols) _drop_legacy_accounts_rar_creds(conn, acc_cols)
if "active" not in acc_cols: if "active" not in acc_cols:
# Conturi existente raman active (default 1). # Conturi existente raman active (default 1).
@@ -121,7 +121,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
"CHECK (on_unmapped_error_default IN (0, 1))" "CHECK (on_unmapped_error_default IN (0, 1))"
) )
if "auto_send_enabled" not in acc_cols: if "auto_send_enabled" not in acc_cols:
# PRD 5.19 US-001: comutator Auto per cont. Default 0 (Auto OFF) — conturile existente # Comutator Auto per cont. Default 0 (Auto OFF) — conturile existente
# trec pe „tine randurile" la ingestie noua pana cand operatorul porneste Auto explicit. # trec pe „tine randurile" la ingestie noua pana cand operatorul porneste Auto explicit.
# Contul id=1 ramane pe default (0), fara tratament special. # Contul id=1 ramane pe default (0), fara tratament special.
conn.execute( conn.execute(
@@ -129,10 +129,10 @@ def _migrate(conn: sqlite3.Connection) -> None:
"CHECK (auto_send_enabled IN (0, 1))" "CHECK (auto_send_enabled IN (0, 1))"
) )
if "email" not in acc_cols: if "email" not in acc_cols:
# Email canonic de contact al firmei (US-001, PRD 5.12). Nullable pt. conturi legacy. # Email canonic de contact al firmei. Nullable pt. conturi legacy.
conn.execute("ALTER TABLE accounts ADD COLUMN email TEXT") conn.execute("ALTER TABLE accounts ADD COLUMN email TEXT")
if "tier" not in acc_cols: if "tier" not in acc_cols:
# Plan de cont (US-001, PRD 5.17). Legacy -> 'free' fara trial (enforcement DUR la deploy). # Plan de cont. Legacy -> 'free' fara trial (enforcement DUR la deploy).
conn.execute( conn.execute(
"ALTER TABLE accounts ADD COLUMN tier TEXT NOT NULL DEFAULT 'free' " "ALTER TABLE accounts ADD COLUMN tier TEXT NOT NULL DEFAULT 'free' "
"CHECK (tier IN ('free','standard','pro','premium'))" "CHECK (tier IN ('free','standard','pro','premium'))"
@@ -173,7 +173,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
if "override_json" not in irows_cols: if "override_json" not in irows_cols:
conn.execute("ALTER TABLE import_rows ADD COLUMN override_json TEXT") conn.execute("ALTER TABLE import_rows ADD COLUMN override_json TEXT")
if "reviewed" not in irows_cols: if "reviewed" not in irows_cols:
# Marcaj confirmare umana (US-007, PRD 5.12). NU intra in payload/idempotenta. # Marcaj confirmare umana. NU intra in payload/idempotenta.
# NOT NULL DEFAULT 0: valoare clara (0=neconfirmat), fara ambiguitate NULL vs 0. # NOT NULL DEFAULT 0: valoare clara (0=neconfirmat), fara ambiguitate NULL vs 0.
conn.execute( conn.execute(
"ALTER TABLE import_rows ADD COLUMN reviewed INTEGER NOT NULL DEFAULT 0" "ALTER TABLE import_rows ADD COLUMN reviewed INTEGER NOT NULL DEFAULT 0"
@@ -194,7 +194,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
"ON submissions(account_id, status)" "ON submissions(account_id, status)"
) )
if "idx_submissions_held" not in existing_idx: if "idx_submissions_held" not in existing_idx:
# PRD 5.19 US-001 (Eng MEDIUM): CREATE TABLE IF NOT EXISTS nu se declanseaza pe DB # CREATE TABLE IF NOT EXISTS nu se declanseaza pe DB
# existent -> indexul partial trebuie creat si aici, nu doar in schema.sql. # existent -> indexul partial trebuie creat si aici, nu doar in schema.sql.
conn.execute( conn.execute(
"CREATE INDEX IF NOT EXISTS idx_submissions_held ON submissions(held) WHERE held=1" "CREATE INDEX IF NOT EXISTS idx_submissions_held ON submissions(held) WHERE held=1"
@@ -202,7 +202,7 @@ def _migrate(conn: sqlite3.Connection) -> None:
def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> None: def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
"""PRD 5.20 US-001: coloane medii RAR per cont + backfill din ancora globala. """Coloane medii RAR per cont + backfill din ancora globala.
Adauga (idempotent): rar_test_enabled/rar_prod_enabled (bife activare), Adauga (idempotent): rar_test_enabled/rar_prod_enabled (bife activare),
rar_creds_test_enc/rar_creds_prod_enc (sloturi creds), rar_env_default. rar_creds_test_enc/rar_creds_prod_enc (sloturi creds), rar_env_default.
@@ -212,7 +212,7 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non
momentul migrarii (ancora) — il copiem in slotul acelui mediu, activam DOAR acel momentul migrarii (ancora) — il copiem in slotul acelui mediu, activam DOAR acel
mediu (celalalt dezactivat) si fixam default-ul pe el. Conturile fara creds raman mediu (celalalt dezactivat) si fixam default-ul pe el. Conturile fara creds raman
pe default-urile coloanei (prod on / test off). Migrarea NU presupune env-ul; se pe default-urile coloanei (prod on / test off). Migrarea NU presupune env-ul; se
bazeaza pe ancora globala, exact cum opera contul inainte de 5.20. bazeaza pe ancora globala.
""" """
newly_added = "rar_env_default" not in acc_cols newly_added = "rar_env_default" not in acc_cols
if "rar_test_enabled" not in acc_cols: if "rar_test_enabled" not in acc_cols:
@@ -252,12 +252,12 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non
def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None: def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
"""PRD 5.20 US-013: DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet. """DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet.
Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut). Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut).
La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese). La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese).
Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de
assert+backup+DROP izolat (fara re-backfill 6c), apelabil direct din teste. assert+backup+DROP izolat (fara re-backfill), apelabil direct din teste.
""" """
if "rar_creds_enc" not in acc_cols: if "rar_creds_enc" not in acc_cols:
return # garda one-way: coloana deja dropata sau DB fresh return # garda one-way: coloana deja dropata sau DB fresh
@@ -272,10 +272,10 @@ def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]
def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None: def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None:
"""Re-backfill (AUTO-FIX 6c) + delegate la _garda_si_drop. """Re-backfill + delegate la _garda_si_drop.
Re-backfill-ul 6c acopera creds setate via POST /v1/conturi/rar-creds intre US-001 Re-backfill-ul acopera creds setate via POST /v1/conturi/rar-creds intre adaugarea
si US-013 (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV. coloanei noi si drop-ul celei vechi (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV.
""" """
if sqlite3.sqlite_version_info < (3, 35, 0): if sqlite3.sqlite_version_info < (3, 35, 0):
print( print(
@@ -344,7 +344,7 @@ def _garda_si_drop(conn: sqlite3.Connection) -> None:
def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None: def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None:
"""PRD 5.20 US-001 (AUTO-FIX G + E4/3): backfill rar_env + recompute idempotency_key. """Backfill rar_env + recompute idempotency_key.
Ruleaza O SINGURA DATA, imediat dupa ce coloana `submissions.rar_env` a fost adaugata Ruleaza O SINGURA DATA, imediat dupa ce coloana `submissions.rar_env` a fost adaugata
pe un DB existent. Toate randurile pre-migrare au fost trimise (sau urmeaza) catre pe un DB existent. Toate randurile pre-migrare au fost trimise (sau urmeaza) catre

View File

@@ -1,13 +1,13 @@
"""Modul embedding in-proces pentru sugestie cod RAR -- L14-S4. """Modul embedding in-proces pentru sugestie cod RAR.
Design (PRD 5.14, Decision #16/#16b): Design:
- Model multilingv via fastembed/ONNX (~230MB pe disc, quantizat, fara torch) - Model multilingv via fastembed/ONNX (~230MB pe disc, quantizat, fara torch)
- Lazy load la prima folosire, NU la import si NU pe /healthz - Lazy load la prima folosire, NU la import si NU pe /healthz
- Worker NU incarca modelul (API-only) - Worker NU incarca modelul (API-only)
- Degradare gratioasa: daca modelul nu se incarca -> is_available()=False, - Degradare gratioasa: daca modelul nu se incarca -> is_available()=False,
suggest_nearest() -> [] fara exceptie, ingestia NU e blocata suggest_nearest() -> [] fara exceptie, ingestia NU e blocata
- Embeddings = DOAR sugestie (nu intra in lantul de enqueue/resolve_prestatii) - Embeddings = DOAR sugestie (nu intra in lantul de enqueue/resolve_prestatii)
- NU apelat din resolve_prestatii/load_mapping (wiring vine in L14-S6 DUPA 5.15) - NU apelat din resolve_prestatii/load_mapping
API public (nivel modul): API public (nivel modul):
index_corpus(items) -> None index_corpus(items) -> None
@@ -28,7 +28,7 @@ log = logging.getLogger(__name__)
# Modelul ales: paraphrase-multilingual-MiniLM-L12-v2 # Modelul ales: paraphrase-multilingual-MiniLM-L12-v2
# ~230MB pe disc (ONNX quantizat), 384 dim, multilingv (ro/en/etc.), suportat de # ~230MB pe disc (ONNX quantizat), 384 dim, multilingv (ro/en/etc.), suportat de
# fastembed, fara torch. (Estimarea initiala din PRD de ~50MB a fost gresita.) # fastembed, fara torch.
FASTEMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" FASTEMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
@@ -137,7 +137,7 @@ class EmbeddingEngine:
) -> list[dict]: ) -> list[dict]:
"""Returneaza top_k vecini cosine [{cod, is_nul, similaritate}]. """Returneaza top_k vecini cosine [{cod, is_nul, similaritate}].
`is_nul` (PRD 5.18 US-005): cand corpusul include exemple NUL (non-operatii), `is_nul`: cand corpusul include exemple NUL (non-operatii),
un vecin NUL = semnal de SUPRESIE, nu cod. Default False pe corpusuri vechi un vecin NUL = semnal de SUPRESIE, nu cod. Default False pe corpusuri vechi
fara `is_nul` in itemi. Returneaza [] daca backend-ul lipseste, corpus-ul e gol fara `is_nul` in itemi. Returneaza [] daca backend-ul lipseste, corpus-ul e gol
sau apare orice exceptie (degradare gratioasa -- nu blocheaza ingestia). sau apare orice exceptie (degradare gratioasa -- nu blocheaza ingestia).

View File

@@ -185,7 +185,7 @@ CATALOG: dict[str, dict[str, str]] = {
" cererii (request_id) afisat." " cererii (request_id) afisat."
), ),
}, },
# Coduri de plan (PRD 5.17) # Coduri de plan
"PLAN_LIMITA_LUNARA": { "PLAN_LIMITA_LUNARA": {
"problema": "Ai atins limita planului Gratuit (60 prestatii/luna)", "problema": "Ai atins limita planului Gratuit (60 prestatii/luna)",
"fix": ( "fix": (

View File

@@ -71,7 +71,7 @@ def canonicalize_row(raw: dict[str, Any]) -> dict[str, Any]:
def build_key(account_id: int | None, canon: dict[str, Any], rar_env: str = "test") -> str: def build_key(account_id: int | None, canon: dict[str, Any], rar_env: str = "test") -> str:
"""SHA-256 partajat canal-API + canal-import, env-aware (PRD 5.20 US-003). """SHA-256 partajat canal-API + canal-import, env-aware.
Aplica account_or_default inainte de hash: None si 1 colapseaza la aceeasi Aplica account_or_default inainte de hash: None si 1 colapseaza la aceeasi
cheie => acelasi rand logic din canale diferite nu se trimite de doua ori. cheie => acelasi rand logic din canale diferite nu se trimite de doua ori.

View File

@@ -1,4 +1,4 @@
"""Parser fisiere xlsx/csv pentru import prezentari (Treapta 2). """Parser fisiere xlsx/csv pentru import prezentari.
Arhitectura 2-treceri: Arhitectura 2-treceri:
Trecerea 1 — read_only=True: dim-check (FileTooLarge) + detectie multi-sheet. Trecerea 1 — read_only=True: dim-check (FileTooLarge) + detectie multi-sheet.

View File

@@ -187,7 +187,7 @@ def metrics() -> str:
conn = get_connection() conn = get_connection()
try: try:
rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall() rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall()
# Coada tinuta (PRD 5.19 US-007): observabilitate ops pt. default Auto OFF. # Coada tinuta: observabilitate ops pt. default Auto OFF.
# Gauge-uri DERIVATE (zero stare noua): total randuri tinute + varsta celui # Gauge-uri DERIVATE (zero stare noua): total randuri tinute + varsta celui
# mai vechi (din created_at), scoped global. Semnaleaza esecul silentios # mai vechi (din created_at), scoped global. Semnaleaza esecul silentios
# cand prezentari raman nedeclarate tacit (risc L.142). # cand prezentari raman nedeclarate tacit (risc L.142).

View File

@@ -52,7 +52,7 @@ def normalize_for_match(value: object) -> str:
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Pre-filtru determinist non-operatii (NUL) — US-001 PRD 5.18 # # Pre-filtru determinist non-operatii (NUL) #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# #
# Masuratoarea k-NN (memorie test-precizie-knn-embeddings) arata recall NUL doar # Masuratoarea k-NN (memorie test-precizie-knn-embeddings) arata recall NUL doar
@@ -88,7 +88,7 @@ _NUL_CTX_PIESA = re.compile(
def prefiltru_nul(denumire: object) -> bool: def prefiltru_nul(denumire: object) -> bool:
"""True daca operatia e gunoi evident (non-operatie de service) -> NUL determinist. """True daca operatia e gunoi evident (non-operatie de service) -> NUL determinist.
Ruleaza INAINTE de k-NN/embeddings in `enrich_suggestions` (US-006). Pur, fara DB. Ruleaza INAINTE de k-NN/embeddings in `enrich_suggestions`. Pur, fara DB.
Zero fals-pozitiv pe operatii reale (vezi comentariul de mai sus + tests). Zero fals-pozitiv pe operatii reale (vezi comentariul de mai sus + tests).
""" """
text = normalize_for_match(denumire) text = normalize_for_match(denumire)
@@ -306,7 +306,7 @@ def resolve_prestatii(
# Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul # Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul
# sursa. Payload-harmless (RAR citeste doar cod_prestatie). # sursa. Payload-harmless (RAR citeste doar cod_prestatie).
it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}" it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}"
# US-001 (PRD 5.11): regula_fara_autosend nu se mai seteaza; # regula_fara_autosend nu se mai seteaza;
# auto_send nu mai tine randul (has_no_auto_send neutralizat). # auto_send nu mai tine randul (has_no_auto_send neutralizat).
else: else:
it["cod_prestatie"] = None it["cod_prestatie"] = None
@@ -466,7 +466,7 @@ def classify_prezentare(
status = "needs_data" status = "needs_data"
rar_error = json.dumps(errors, ensure_ascii=False) rar_error = json.dumps(errors, ensure_ascii=False)
else: else:
# US-001 (PRD 5.11): ramura AUTO_SEND_OPRIT eliminata. # Ramura AUTO_SEND_OPRIT eliminata.
# Un cod rezolvat (mapare exacta sau regula text) -> queued direct. # Un cod rezolvat (mapare exacta sau regula text) -> queued direct.
status = "queued" status = "queued"
rar_error = None rar_error = None
@@ -482,7 +482,7 @@ def classify_prezentare(
def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool: def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool:
"""Neutralizat dupa US-001 (PRD 5.11): auto_send nu mai tine randuri in needs_mapping. """Neutralizat: auto_send nu mai tine randuri in needs_mapping.
Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea
ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat
@@ -647,7 +647,7 @@ def _corpus_signature_silver(rows: list) -> str:
def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> None: def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> None:
"""Construieste/actualizeaza corpusul embeddings din corpusul ETICHETAT (PRD 5.18 US-005). """Construieste/actualizeaza corpusul embeddings din corpusul ETICHETAT.
Sursa corpusului = `mapping_suggestions` (SILVER): exemple reale etichetate Sursa corpusului = `mapping_suggestions` (SILVER): exemple reale etichetate
{denumire_normalizata -> cod, is_nul}, NU cele 18 categorii generice din {denumire_normalizata -> cod, is_nul}, NU cele 18 categorii generice din
@@ -660,8 +660,8 @@ def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> Non
Cand e activat: indexeaza corpusul o singura data (lazy-load modelul ~230MB la Cand e activat: indexeaza corpusul o singura data (lazy-load modelul ~230MB la
prima chemare), re-indexeaza doar cand semnatura corpusului SILVER s-a schimbat. prima chemare), re-indexeaza doar cand semnatura corpusului SILVER s-a schimbat.
Itemii NUL (is_nul=1, cod NULL) raman in corpus: un vecin NUL e semnal de supresie Itemii NUL (is_nul=1, cod NULL) raman in corpus: un vecin NUL e semnal de supresie.
(US-006). Degradare gratioasa: orice eroare lasa corpusul gol -> enrich cade pe restul. Degradare gratioasa: orice eroare lasa corpusul gol -> enrich cade pe restul.
""" """
from .config import get_settings from .config import get_settings
if not get_settings().embeddings_enabled: if not get_settings().embeddings_enabled:
@@ -687,7 +687,7 @@ def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> Non
] ]
_emb.index_corpus(items, signature=sig) _emb.index_corpus(items, signature=sig)
except Exception: except Exception:
pass # degradare gratioasa (#16b): esecul indexarii nu blocheaza editorul pass # degradare gratioasa: esecul indexarii nu blocheaza editorul
def enrich_suggestions( def enrich_suggestions(
@@ -698,13 +698,13 @@ def enrich_suggestions(
) -> dict: ) -> dict:
"""Imbogateste sugestiile cu GOLD partajat, SILVER LLM si embeddings NN. """Imbogateste sugestiile cu GOLD partajat, SILVER LLM si embeddings NN.
Precedenta Eng-F2 (pentru sugestie-only, nu auto-send): Precedenta (pentru sugestie-only, nu auto-send):
shared GOLD > SILVER > embeddings shared GOLD > SILVER > embeddings
(Account GOLD = operations_mapping propriu = deja rezolvat inainte de needs_mapping; (Account GOLD = operations_mapping propriu = deja rezolvat inainte de needs_mapping;
nu apare in needs_mapping, deci nu e in precedenta de sugestie.) nu apare in needs_mapping, deci nu e in precedenta de sugestie.)
Ordine completa (PRD 5.18 US-006): Ordine completa:
pre-filtru NUL determinist -> (daca NUL: fara cod, `surse['nul']=True`) pre-filtru NUL determinist -> (daca NUL: fara cod, `surse['nul']=True`)
altfel GOLD partajat > exact (SILVER) > k-NN embeddings. altfel GOLD partajat > exact (SILVER) > k-NN embeddings.
@@ -715,11 +715,11 @@ def enrich_suggestions(
} }
INVARIANTE: INVARIANTE:
- Toate sursele = SUGGESTION-ONLY. NU intra in resolve_prestatii/load_mapping (#13). - Toate sursele = SUGGESTION-ONLY. NU intra in resolve_prestatii/load_mapping.
- Pre-filtru NUL (US-001) ruleaza PRIMUL: gunoiul evident (ITP/plata/discount...) e - Pre-filtru NUL ruleaza PRIMUL: gunoiul evident (ITP/plata/discount...) e
marcat non-operatie INAINTE de k-NN, fara sugestie de cod. marcat non-operatie INAINTE de k-NN, fara sugestie de cod.
- SILVER cu is_nul=1 (non-operatie/gunoi) NU produce sugestie (#4); vecin k-NN NUL idem. - SILVER cu is_nul=1 (non-operatie/gunoi) NU produce sugestie; vecin k-NN NUL idem.
- Degradare gratioasa pe embeddings (#16b): daca motorul nu e disponibil sau arunca, - Degradare gratioasa pe embeddings: daca motorul nu e disponibil sau arunca,
returneaza sugestia disponibila din celelalte surse, fara exceptie. returneaza sugestia disponibila din celelalte surse, fara exceptie.
- Import local shared_store/embeddings: evita ciclu la import-time (shared_store - Import local shared_store/embeddings: evita ciclu la import-time (shared_store
importa normalize_for_match din mapping). importa normalize_for_match din mapping).
@@ -730,7 +730,7 @@ def enrich_suggestions(
if not denumire: if not denumire:
return {"sugestie_principala": sugestie_principala, "surse": surse} return {"sugestie_principala": sugestie_principala, "surse": surse}
# 0. Pre-filtru NUL determinist (US-001) INAINTE de orice k-NN/lookup: non-operatie # 0. Pre-filtru NUL determinist INAINTE de orice k-NN/lookup: non-operatie
# evidenta -> fara cod, scurtcircuit (nu interogheaza embeddings/SILVER pe gunoi). # evidenta -> fara cod, scurtcircuit (nu interogheaza embeddings/SILVER pe gunoi).
if prefiltru_nul(denumire): if prefiltru_nul(denumire):
surse["nul"] = True surse["nul"] = True
@@ -758,7 +758,7 @@ def enrich_suggestions(
except Exception: except Exception:
pass # degradare gratioasa pass # degradare gratioasa
# 3. Embeddings NN (similaritate semantica, degradare gratioasa #16b) # 3. Embeddings NN (similaritate semantica, degradare gratioasa)
if include_embeddings: if include_embeddings:
try: try:
from . import embeddings as _emb from . import embeddings as _emb
@@ -768,7 +768,7 @@ def enrich_suggestions(
# ensure_embeddings_corpus (gated pe AUTOPASS_EMBEDDINGS_ENABLED); cand # ensure_embeddings_corpus (gated pe AUTOPASS_EMBEDDINGS_ENABLED); cand
# flagul e off, has_corpus() ramane False si calea e un no-op real. # flagul e off, has_corpus() ramane False si calea e un no-op real.
if _emb.has_corpus(): if _emb.has_corpus():
# F1 (US-005): corpusul k-NN e text NORMALIZAT (denumire_normalizata), # Corpusul k-NN e text NORMALIZAT (denumire_normalizata),
# deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si # deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si
# nu mai e configul sub care s-a masurat 94.3%. # nu mai e configul sub care s-a masurat 94.3%.
nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=1) nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=1)
@@ -776,7 +776,7 @@ def enrich_suggestions(
# Evita recomandari irelevante cand corpus-ul e mic/partial. # Evita recomandari irelevante cand corpus-ul e mic/partial.
if nn and nn[0].get("similaritate", 0) >= EMB_MIN_SIMILARITATE: if nn and nn[0].get("similaritate", 0) >= EMB_MIN_SIMILARITATE:
if nn[0].get("is_nul"): if nn[0].get("is_nul"):
# Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod (US-006). # Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod.
surse["nul"] = True surse["nul"] = True
elif nn[0].get("cod"): elif nn[0].get("cod"):
surse["embedding"] = str(nn[0]["cod"]) surse["embedding"] = str(nn[0]["cod"])
@@ -880,7 +880,7 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
stats["still_blocked"] += 1 stats["still_blocked"] += 1
continue continue
# US-001 (PRD 5.11): ramura auto_send eliminata din reresolve. # Ramura auto_send eliminata din reresolve.
# Un cod rezolvat -> queued direct (review_manual ramane 0). # Un cod rezolvat -> queued direct (review_manual ramane 0).
errors = validate_prezentare(content) errors = validate_prezentare(content)
@@ -892,10 +892,10 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
) )
stats["needs_data"] += 1 stats["needs_data"] += 1
else: else:
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la trecerea # Re-snapshot `held` din comutatorul contului la trecerea
# needs_mapping -> queued (chokepoint held_for_account, `acct` deja normalizat # needs_mapping -> queued (chokepoint held_for_account, `acct` deja normalizat
# prin account_or_default la intrarea in reresolve_account). Altfel un rand # prin account_or_default la intrarea in reresolve_account). Altfel un rand
# deblocat din needs_mapping ar pleca automat desi contul e Auto OFF (R2). # deblocat din needs_mapping ar pleca automat desi contul e Auto OFF.
conn.execute( conn.execute(
"UPDATE submissions SET status='queued', payload_json=?, rar_error=NULL, " "UPDATE submissions SET status='queued', payload_json=?, rar_error=NULL, "
"retry_count=0, next_attempt_at=NULL, held=?, updated_at=datetime('now') WHERE id=?", "retry_count=0, next_attempt_at=NULL, held=?, updated_at=datetime('now') WHERE id=?",

View File

@@ -109,7 +109,7 @@ class SubmissionResult(BaseModel):
# RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit. `deduped` pastreaza # RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit. `deduped` pastreaza
# semantica actuala (clientii vechi care testeaza `deduped` nu se sparg). # semantica actuala (clientii vechi care testeaza `deduped` nu se sparg).
reactivated: bool = False reactivated: bool = False
# Ecou PUR de coada (PRD 5.19 US-010): True cand randul e `queued AND held=1` # Ecou PUR de coada: True cand randul e `queued AND held=1`
# (tinut manual, cont Auto OFF) -> NU a plecat la RAR, asteapta eliberarea umana. # (tinut manual, cont Auto OFF) -> NU a plecat la RAR, asteapta eliberarea umana.
# NU influenteaza payload/idempotenta; onestitate fata de integratorul API (invariant 5.7). # NU influenteaza payload/idempotenta; onestitate fata de integratorul API (invariant 5.7).
held: bool = False held: bool = False

View File

@@ -1,4 +1,4 @@
"""Seeder corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004). """Seeder corpus operatii etichetate -> mapping_suggestions (SILVER).
Artefactul `app/data/operatii-etichetate.json` e produs offline de Artefactul `app/data/operatii-etichetate.json` e produs offline de
`tools/mapare-llm/genereaza_seed.py` (etichetare LM Studio, o singura data) si comis `tools/mapare-llm/genereaza_seed.py` (etichetare LM Studio, o singura data) si comis

View File

@@ -1,6 +1,6 @@
"""Constructor payload postPrezentare (T4). """Constructor payload postPrezentare.
Reguli din contract (docs/api-rar-contract.md), confirmate live in T1: Reguli din contract (docs/api-rar-contract.md), confirmate live:
- status mereu "FINALIZATA". - status mereu "FINALIZATA".
- tipPrestatie NU se trimite (server-generated GENERIC). - tipPrestatie NU se trimite (server-generated GENERIC).
- odometruFinal ca string; odometruInitial ca string cand e prezent, altfel null. - odometruFinal ca string; odometruInitial ca string cand e prezent, altfel null.

View File

@@ -1,8 +1,8 @@
"""Extragere payload submission -> campuri afisabile. """Extragere payload submission -> campuri afisabile.
Helper PUR partajat intre canalul web (dashboard Trimiteri) si canalul API Helper PUR partajat intre canalul web (dashboard Trimiteri) si canalul API
(`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua (decizie (`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua.
eng review, DRY). Fara DB, fara request — primeste `payload_json` (text JSON Fara DB, fara request — primeste `payload_json` (text JSON
plaintext, vezi `submissions.payload_json`) sau un dict deja parsat. plaintext, vezi `submissions.payload_json`) sau un dict deja parsat.
Defensiv prin constructie: nu arunca niciodata pe payload malformat — degradeaza Defensiv prin constructie: nu arunca niciodata pe payload malformat — degradeaza
@@ -126,7 +126,7 @@ def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]:
op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else "" op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else ""
# obs: text liber observatii (camp RAR, optional). Conventie goala "" (nu EMPTY). # obs: text liber observatii (camp RAR, optional). Conventie goala "" (nu EMPTY).
# US-005 PRD 5.15: obs traieste in payload_json (nu coloana separata). # Traieste in payload_json, nu in coloana separata.
obs = _clean_str(data.get("obs")) obs = _clean_str(data.get("obs"))
return { return {

View File

@@ -3,11 +3,11 @@
Pattern ca app/errors.py: catalog + helperi. Consumat de rutele de ingestie si dashboard. Pattern ca app/errors.py: catalog + helperi. Consumat de rutele de ingestie si dashboard.
Nu importa DB, HTTP, sau orice alt modul intern cu efecte secundare. Nu importa DB, HTTP, sau orice alt modul intern cu efecte secundare.
Decizii implementare (PRD 5.17 / autoplan 2026-06-28): Note implementare:
- FREE_MONTHLY_LIMIT: constanta unica (T-CEO-2), tunabila fara arqueologie de cod. - FREE_MONTHLY_LIMIT: constanta unica, tunabila fara arheologie de cod.
- CONSUMED_STATUSES: decizie #20 — prestatie consumata = acceptata in coada. - CONSUMED_STATUSES: prestatie consumata = acceptata in coada.
- effective_tier: `now` injectabil (decizie #2) pentru teste deterministe. - effective_tier: `now` injectabil pentru teste deterministe.
- monthly_usage: pattern E7/5.15 (strftime localtime), `now` injectabil. - monthly_usage: strftime localtime, `now` injectabil.
""" """
from __future__ import annotations from __future__ import annotations
@@ -16,11 +16,10 @@ import sqlite3
from datetime import datetime, timezone from datetime import datetime, timezone
# Limita lunara pentru planul Gratuit. # Limita lunara pentru planul Gratuit.
# Decizie user T-CEO-2 (2026-06-28): o singura constanta, referita din PLANS. # O singura constanta, referita din PLANS. Tunabila fara a modifica logica de enforcement.
# Tunabila fara a modifica logica de enforcement.
FREE_MONTHLY_LIMIT: int = 60 FREE_MONTHLY_LIMIT: int = 60
# Statusurile care consuma din cota lunara (decizie #20, 2026-06-28). # Statusurile care consuma din cota lunara.
# Prestatie consumata = acceptata in coada (queued/sending/sent), nu cele respinse/blocate. # Prestatie consumata = acceptata in coada (queued/sending/sent), nu cele respinse/blocate.
# Rationale: limita e pe ce trimitem la RAR, nu pe incercari esuate sau blocate. # Rationale: limita e pe ce trimitem la RAR, nu pe incercari esuate sau blocate.
CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent") CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent")
@@ -30,7 +29,7 @@ CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent")
# monthly_limit -- None = nelimitat; int = limita prestatii/luna # monthly_limit -- None = nelimitat; int = limita prestatii/luna
# api_access -- True = acces import prin API (/v1/*); False = doar web dashboard # api_access -- True = acces import prin API (/v1/*); False = doar web dashboard
# #
# Aliniat landing-ului comercial (PRD 5.17 US-001): # Aliniat landing-ului comercial:
# Gratuit: 60/luna, fara API # Gratuit: 60/luna, fara API
# Standard: nelimitat, fara API # Standard: nelimitat, fara API
# Pro: nelimitat, cu API # Pro: nelimitat, cu API

View File

@@ -1,4 +1,4 @@
"""Medii RAR per cont (PRD 5.20): disponibilitate + default efectiv. """Medii RAR per cont: disponibilitate + default efectiv.
Sursa UNICA de adevar pentru REQ-DISP / REQ-DEFAULT: vizibilitatea selector/toggle Sursa UNICA de adevar pentru REQ-DISP / REQ-DEFAULT: vizibilitatea selector/toggle
in UI, validarea tintei in API si decizia worker-ului citesc TOATE de aici, ca sa in UI, validarea tintei in API si decizia worker-ului citesc TOATE de aici, ca sa
@@ -92,7 +92,7 @@ def rar_env_efectiv_cont(conn: sqlite3.Connection, account_id: int) -> str | Non
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Exceptie si rezolvator de mediu tinta (US-004, dependent de US-002) # # Exceptie si rezolvator de mediu tinta #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
class MediuIndisponibil(Exception): class MediuIndisponibil(Exception):
@@ -126,7 +126,7 @@ def rezolva_rar_env(
4. `cerut` None -> incearca rar_env_efectiv_cont (default-ul contului). 4. `cerut` None -> incearca rar_env_efectiv_cont (default-ul contului).
5. Daca contul nu are niciun mediu disponibil (rar_env_efectiv_cont == None) 5. Daca contul nu are niciun mediu disponibil (rar_env_efectiv_cont == None)
-> cade pe ancora globala get_settings().rar_env, normalizata la VALID_ENVS. -> cade pe ancora globala get_settings().rar_env, normalizata la VALID_ENVS.
Acest fallback e intentionat (PRD 5.20 §2 Non-Goals): AUTOPASS_RAR_ENV ramane Acest fallback e intentionat: AUTOPASS_RAR_ENV ramane
ancora de migrare si fallback pentru actiuni fara cont (keepalive, canal API cu ancora de migrare si fallback pentru actiuni fara cont (keepalive, canal API cu
creds efemere pe conturi nou-create fara medii configurate). creds efemere pe conturi nou-create fara medii configurate).

View File

@@ -1,6 +1,5 @@
-- Schema SQLite (WAL) pentru gateway RAR AUTOPASS. -- Schema SQLite (WAL) pentru gateway RAR AUTOPASS.
-- Vezi plan.md sect. 5 + plan-treapta2.md sect. 4. -- Conturi cu creds RAR durabile, tabele import, atestari.
-- Treapta 2: adauga conturi cu creds RAR durabile, tabele import, atestari.
PRAGMA journal_mode = WAL; PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
@@ -10,7 +9,7 @@ CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
cui TEXT, cui TEXT,
email TEXT, -- email canonic de contact al firmei (US-001, PRD 5.12); nullable pt. conturi legacy email TEXT, -- email canonic de contact al firmei; nullable pt. conturi legacy
active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3 active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3
-- Stare de ciclu de viata explicita (5.5). Superset al lui `active`: mentinem invariantul -- Stare de ciclu de viata explicita (5.5). Superset al lui `active`: mentinem invariantul
-- active=1 <=> status='active' (vezi accounts.set_status / set_active). Worker gate-uieste pe status. -- active=1 <=> status='active' (vezi accounts.set_status / set_active). Worker gate-uieste pe status.
@@ -19,8 +18,8 @@ CREATE TABLE IF NOT EXISTS accounts (
-- vezi accounts.delete_account — randul ramane doar pentru audit). -- vezi accounts.delete_account — randul ramane doar pentru audit).
status TEXT NOT NULL DEFAULT 'active' status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('pending','active','blocked','archived','deleted')), CHECK (status IN ('pending','active','blocked','archived','deleted')),
-- Medii RAR per cont (PRD 5.20 US-001). Fiecare mediu = bifa de activare + slot creds. -- Medii RAR per cont. Fiecare mediu = bifa de activare + slot creds.
-- accounts.rar_creds_enc (legacy env-less) a fost dropata in US-013 (SQLite DROP COLUMN). -- accounts.rar_creds_enc (legacy env-less) a fost dropata (SQLite DROP COLUMN).
-- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou = -- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou =
-- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare. -- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare.
rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)), rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)),
@@ -33,7 +32,7 @@ CREATE TABLE IF NOT EXISTS accounts (
-- 1 (respinge cererea fara enqueue). Override per-cerere via PrezentareRequest.on_unmapped_error. -- 1 (respinge cererea fara enqueue). Override per-cerere via PrezentareRequest.on_unmapped_error.
on_unmapped_error_default INTEGER NOT NULL DEFAULT 0 on_unmapped_error_default INTEGER NOT NULL DEFAULT 0
CHECK (on_unmapped_error_default IN (0, 1)), CHECK (on_unmapped_error_default IN (0, 1)),
-- Comutator Auto per cont (PRD 5.19 US-001). AL TREILEA `auto_send` din schema (R6): -- Comutator Auto per cont. AL TREILEA `auto_send` din schema:
-- DISTINCT de operations_mapping.auto_send si operation_text_rules.auto_send, care sunt -- DISTINCT de operations_mapping.auto_send si operation_text_rules.auto_send, care sunt
-- PER-OPERATIE. Acesta e PER-CONT si guverneaza DOAR: (a) valoarea implicita a lui -- PER-OPERATIE. Acesta e PER-CONT si guverneaza DOAR: (a) valoarea implicita a lui
-- submissions.held la ingestie (held = 0 daca 1, altfel 1 — vezi accounts.held_for_account); -- submissions.held la ingestie (held = 0 daca 1, altfel 1 — vezi accounts.held_for_account);
@@ -104,7 +103,7 @@ CREATE TABLE IF NOT EXISTS submissions (
status TEXT NOT NULL DEFAULT 'queued' status TEXT NOT NULL DEFAULT 'queued'
CHECK (status IN ('queued','sending','sent','needs_mapping','needs_data','error')), CHECK (status IN ('queued','sending','sent','needs_mapping','needs_data','error')),
payload_json TEXT NOT NULL, payload_json TEXT NOT NULL,
-- Mediul RAR tinta al acestei trimiteri (PRD 5.20 US-001). DEFAULT 'test' e doar plasa -- Mediul RAR tinta al acestei trimiteri. DEFAULT 'test' e doar plasa
-- pentru randuri net-noi care nu seteaza explicit; fiecare INSERT (API/import/web) seteaza -- pentru randuri net-noi care nu seteaza explicit; fiecare INSERT (API/import/web) seteaza
-- rar_env explicit. Backfill din AUTOPASS_RAR_ENV global la migrare (NU lasa pe DEFAULT). -- rar_env explicit. Backfill din AUTOPASS_RAR_ENV global la migrare (NU lasa pe DEFAULT).
rar_env TEXT NOT NULL DEFAULT 'test' CHECK (rar_env IN ('test', 'prod')), rar_env TEXT NOT NULL DEFAULT 'test' CHECK (rar_env IN ('test', 'prod')),
@@ -113,12 +112,12 @@ CREATE TABLE IF NOT EXISTS submissions (
rar_error TEXT, rar_error TEXT,
id_prezentare INTEGER, -- data.id intors de RAR la succes id_prezentare INTEGER, -- data.id intors de RAR la succes
retry_count INTEGER NOT NULL DEFAULT 0, retry_count INTEGER NOT NULL DEFAULT 0,
next_attempt_at TEXT, -- backoff: randul nu se ia inainte de acest moment (T2) next_attempt_at TEXT, -- backoff: randul nu se ia inainte de acest moment
sending_since TEXT, -- pentru lease/timeout pe randuri 'sending' orfane (T2) sending_since TEXT, -- pentru lease/timeout pe randuri 'sending' orfane
purge_after TEXT, -- sent + 90z (T16) purge_after TEXT, -- sent + 90z
batch_id INTEGER, -- import batch (T7; NULL = canal API) batch_id INTEGER, -- import batch (NULL = canal API)
row_index INTEGER, -- rand in batch (T7; NULL = canal API) row_index INTEGER, -- rand in batch (NULL = canal API)
-- Randuri tinute manual (PRD 5.19 US-001). rand `queued AND held=1` = "In asteptare -- Randuri tinute manual. rand `queued AND held=1` = "In asteptare
-- (manual)": tinut pana la eliberare umana (trimitere manuala per rand/bulk: held 1 -> 0). -- (manual)": tinut pana la eliberare umana (trimitere manuala per rand/bulk: held 1 -> 0).
-- Worker-ul (claim_one) ia doar `queued AND held=0`. held NU intra in payload/idempotenta -- Worker-ul (claim_one) ia doar `queued AND held=0`. held NU intra in payload/idempotenta
-- si NU pleaca la RAR — e pur control de coada (ca import_rows.reviewed). Snapshot la -- si NU pleaca la RAR — e pur control de coada (ca import_rows.reviewed). Snapshot la
@@ -134,7 +133,7 @@ CREATE INDEX IF NOT EXISTS idx_submissions_account_status ON submissions(account
-- ruleaza executescript(schema.sql) INAINTE de _migrate; pe un DB existent pre-`held`/`batch_id` -- ruleaza executescript(schema.sql) INAINTE de _migrate; pe un DB existent pre-`held`/`batch_id`
-- coloana inca nu exista in momentul executarii schemei, deci un CREATE INDEX pe ea ar esua -- coloana inca nu exista in momentul executarii schemei, deci un CREATE INDEX pe ea ar esua
-- (no such column). Indexul partial `idx_submissions_held ON submissions(held) WHERE held=1` -- (no such column). Indexul partial `idx_submissions_held ON submissions(held) WHERE held=1`
-- (PRD 5.19 US-001) traieste doar in _migrate, ca `idx_submissions_batch`. -- traieste doar in _migrate, ca `idx_submissions_batch`.
-- Mapare coloane fisier -> campuri canonice (retinuta per cont, semnatura coloane). -- Mapare coloane fisier -> campuri canonice (retinuta per cont, semnatura coloane).
CREATE TABLE IF NOT EXISTS column_mappings ( CREATE TABLE IF NOT EXISTS column_mappings (
@@ -162,7 +161,7 @@ CREATE TABLE IF NOT EXISTS import_batches (
already_sent INTEGER NOT NULL DEFAULT 0, already_sent INTEGER NOT NULL DEFAULT 0,
duplicate_in_file INTEGER NOT NULL DEFAULT 0, duplicate_in_file INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
purge_after TEXT -- created_at + 90z (T16) purge_after TEXT -- created_at + 90z
); );
-- Randuri din lot de import (PII criptat cu Fernet). -- Randuri din lot de import (PII criptat cu Fernet).
@@ -171,8 +170,8 @@ CREATE TABLE IF NOT EXISTS import_rows (
batch_id INTEGER NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE, batch_id INTEGER NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE,
row_index INTEGER NOT NULL, row_index INTEGER NOT NULL,
raw_json TEXT NOT NULL, -- PII criptat (Fernet, ca submissions) raw_json TEXT NOT NULL, -- PII criptat (Fernet, ca submissions)
override_json TEXT, -- patch CANONIC editat in preview, criptat Fernet (3.6, Approach B); NULL = fara editare override_json TEXT, -- patch CANONIC editat in preview, criptat Fernet; NULL = fara editare
reviewed INTEGER NOT NULL DEFAULT 0, -- US-007 (PRD 5.12): 0=neconfirmat, 1=confirmat de operator; NU intra in payload/idempotenta reviewed INTEGER NOT NULL DEFAULT 0, -- 0=neconfirmat, 1=confirmat de operator; NU intra in payload/idempotenta
resolved_status TEXT NOT NULL DEFAULT 'pending' resolved_status TEXT NOT NULL DEFAULT 'pending'
CHECK (resolved_status IN ( CHECK (resolved_status IN (
'pending','ok','needs_mapping','needs_data', 'pending','ok','needs_mapping','needs_data',
@@ -207,15 +206,15 @@ CREATE TABLE IF NOT EXISTS users (
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (datetime('now'))
); );
-- Jurnal de aplicatie la nivel de eveniment (PRD 5.6). Dublu canal: aici (vizibil -- Jurnal de aplicatie la nivel de eveniment. Dublu canal: aici (vizibil
-- in tab "Jurnal") + log text rotativ (depanare). `tip` e text liber documentat -- in tab "Jurnal") + log text rotativ (depanare). `tip` e text liber documentat
-- (lista extensibila, decizie §5) — adaugam tipuri fara migrare. Toate valorile -- (lista extensibila) — adaugam tipuri fara migrare. Toate valorile
-- sunt REDACTATE la scriere (app/observ.py via app/security.py): parole/token -> -- sunt REDACTATE la scriere (app/observ.py via app/security.py): parole/token ->
-- ***REDACTED***, VIN/nr partial. `context_json` = metadate (NU payload PII integral). -- ***REDACTED***, VIN/nr partial. `context_json` = metadate (NU payload PII integral).
CREATE TABLE IF NOT EXISTS app_events ( CREATE TABLE IF NOT EXISTS app_events (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL DEFAULT (datetime('now')), ts TEXT NOT NULL DEFAULT (datetime('now')),
request_id TEXT, -- corelare cu raspunsul clientului (US-002) request_id TEXT, -- corelare cu raspunsul clientului
account_id INTEGER, -- NULL = eveniment de sistem (fara cont) account_id INTEGER, -- NULL = eveniment de sistem (fara cont)
sursa TEXT NOT NULL DEFAULT 'api' sursa TEXT NOT NULL DEFAULT 'api'
CHECK (sursa IN ('api','worker')), CHECK (sursa IN ('api','worker')),
@@ -224,14 +223,14 @@ CREATE TABLE IF NOT EXISTS app_events (
cod TEXT, -- cod din catalogul de erori (app/errors.py) daca aplica cod TEXT, -- cod din catalogul de erori (app/errors.py) daca aplica
mesaj TEXT, -- mesaj scurt redactat mesaj TEXT, -- mesaj scurt redactat
context_json TEXT, -- JSON metadate redactate (submission_id, count, status...) context_json TEXT, -- JSON metadate redactate (submission_id, count, status...)
purge_after TEXT -- ts + log_retention_days (US-008) purge_after TEXT -- ts + log_retention_days
); );
CREATE INDEX IF NOT EXISTS idx_app_events_ts ON app_events(ts); CREATE INDEX IF NOT EXISTS idx_app_events_ts ON app_events(ts);
CREATE INDEX IF NOT EXISTS idx_app_events_account ON app_events(account_id, ts); CREATE INDEX IF NOT EXISTS idx_app_events_account ON app_events(account_id, ts);
CREATE INDEX IF NOT EXISTS idx_app_events_tip ON app_events(tip); CREATE INDEX IF NOT EXISTS idx_app_events_tip ON app_events(tip);
-- Reguli automate de mapare pe text (substring, per cont). PRD 5.8 US-001. -- Reguli automate de mapare pe text (substring, per cont).
-- auto_send DEFAULT 0 (decizie CEO 2026-06-24): substring are blast-radius mai mare -- auto_send DEFAULT 0: substring are blast-radius mai mare
-- decat maparea exacta; o regula noua rezolva codul dar tine randul pentru verificare -- decat maparea exacta; o regula noua rezolva codul dar tine randul pentru verificare
-- umana pana cand operatorul activeaza explicit "In coada". -- umana pana cand operatorul activeaza explicit "In coada".
CREATE TABLE IF NOT EXISTS operation_text_rules ( CREATE TABLE IF NOT EXISTS operation_text_rules (
@@ -246,7 +245,7 @@ CREATE TABLE IF NOT EXISTS operation_text_rules (
); );
CREATE INDEX IF NOT EXISTS idx_text_rules_account ON operation_text_rules(account_id); CREATE INDEX IF NOT EXISTS idx_text_rules_account ON operation_text_rules(account_id);
-- Sugestii de mapare (strat SILVER, L14-S3 PRD 5.14). -- Sugestii de mapare (strat SILVER).
-- Etichete LLM/embedding — bootstrap; citita DOAR de suggest_codes/pending_unmapped, -- Etichete LLM/embedding — bootstrap; citita DOAR de suggest_codes/pending_unmapped,
-- NICIODATA de load_mapping/resolve_prestatii (separare structurala #13). -- NICIODATA de load_mapping/resolve_prestatii (separare structurala #13).
-- Cheia = denumire normalizata (fara diacritice, uppercase, spatii colapsate). -- Cheia = denumire normalizata (fara diacritice, uppercase, spatii colapsate).
@@ -265,7 +264,7 @@ CREATE TABLE IF NOT EXISTS mapping_suggestions (
CREATE INDEX IF NOT EXISTS idx_mapping_suggestions_cod CREATE INDEX IF NOT EXISTS idx_mapping_suggestions_cod
ON mapping_suggestions(cod_prestatie) WHERE cod_prestatie IS NOT NULL; ON mapping_suggestions(cod_prestatie) WHERE cod_prestatie IS NOT NULL;
-- Mapari validate de oameni (strat GOLD partajat cross-account, L14-S3 PRD 5.14). -- Mapari validate de oameni (strat GOLD partajat cross-account).
-- Confirmarile umane din ORICE cont contribuie la acest store (#8). -- Confirmarile umane din ORICE cont contribuie la acest store (#8).
-- cross-account = suggestion-only (pre-completeaza editorul, F-A/#11), NU auto-send. -- cross-account = suggestion-only (pre-completeaza editorul, F-A/#11), NU auto-send.
-- Auto-send DOAR din operations_mapping (GOLD propriu per-cont, Eng-F2). -- Auto-send DOAR din operations_mapping (GOLD propriu per-cont, Eng-F2).

View File

@@ -1,17 +1,17 @@
"""Store partajat pentru sugestii (SILVER) si mapari validate de oameni (GOLD cross-account). """Store partajat pentru sugestii (SILVER) si mapari validate de oameni (GOLD cross-account).
Straturi (L14-S3, PRD 5.14): Straturi:
- mapping_suggestions (SILVER): sugestii LLM/embedding, citite DOAR de suggest/pending_unmapped, - mapping_suggestions (SILVER): sugestii LLM/embedding, citite DOAR de suggest/pending_unmapped,
NICIODATA de load_mapping/resolve_prestatii (separare structurala #13). NICIODATA de load_mapping/resolve_prestatii (separare structurala).
- shared_mappings (GOLD partajat): mapari validate de om din orice cont; pot pre-completa - shared_mappings (GOLD partajat): mapari validate de om din orice cont; pot pre-completa
editorul (suggestion-only cross-account, F-A/#11); auto-send DOAR GOLD propriu editorul (suggestion-only cross-account); auto-send DOAR GOLD propriu
(operations_mapping per-cont, Eng-F2). (operations_mapping per-cont).
Invariante: Invariante:
- INSERT OR IGNORE la seed: nu suprascrie randuri existente (#2). - INSERT OR IGNORE la seed: nu suprascrie randuri existente.
- NUL = is_nul=1, cod_prestatie NULL; NU se promoveaza la cod RAR (#4). - NUL = is_nul=1, cod_prestatie NULL; NU se promoveaza la cod RAR.
- source/confidence pe fiecare rand (provenienta + rollback batch model prost, #5). - source/confidence pe fiecare rand (provenienta + rollback batch model prost).
- Wiring in resolve_prestatii/load_mapping vine in L14-S6; modulul de fata e API pur. - Wiring in resolve_prestatii/load_mapping nu e cablat inca; modulul de fata e API pur.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -104,7 +104,7 @@ def requeue_submission(conn, account_id: int, sid: int) -> dict:
valid_codes = load_nomenclator_codes(conn) or None valid_codes = load_nomenclator_codes(conn) or None
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes) cl = classify_prezentare(content, mapping, mapping_meta, valid_codes)
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la re-punere in coada # Re-snapshot `held` din comutatorul contului la re-punere in coada
# (paritate cu create_prezentari/reresolve_account). Fara asta un rand repus pastra # (paritate cu create_prezentari/reresolve_account). Fara asta un rand repus pastra
# `held` VECHI si s-ar auto-trimite desi contul e Auto OFF (sau ar ramane blocat cand e ON). # `held` VECHI si s-ar auto-trimite desi contul e Auto OFF (sau ar ramane blocat cand e ON).
held_val = held_for_account(conn, account_or_default(account_id)) held_val = held_for_account(conn, account_or_default(account_id))

View File

@@ -1,4 +1,4 @@
"""Validare de domeniu pentru prezentari (T3). """Validare de domeniu pentru prezentari.
Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE
de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE

View File

@@ -127,7 +127,7 @@ def _apply_lifecycle(conn, ids: list[int], action: str) -> None:
for aid in ids: for aid in ids:
try: try:
if action == "activate": if action == "activate":
# Gate US-002: nu activam conturi fara identitate completa (companie+email+CUI) # Nu activam conturi fara identitate completa (companie+email+CUI)
acct_row = conn.execute( acct_row = conn.execute(
"SELECT id, name, cui, email FROM accounts WHERE id=?", (aid,) "SELECT id, name, cui, email FROM accounts WHERE id=?", (aid,)
).fetchone() ).fetchone()

View File

@@ -79,7 +79,6 @@ async def signup_post(
name=name, cui=cui, email=email, plan=requested_plan, name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422) ), status_code=422)
# CUI obligatoriu la signup (US-001, PRD 5.12)
cui_norm = cui.strip().upper() if cui else "" cui_norm = cui.strip().upper() if cui else ""
if not cui_norm: if not cui_norm:
return _TMPL.TemplateResponse(request, "signup.html", _ctx( return _TMPL.TemplateResponse(request, "signup.html", _ctx(

View File

@@ -72,7 +72,7 @@ ETICHETE_SCURTE: dict[str, str] = {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Randuri tinute manual (held) — US-006, PRD 5.19 # Randuri tinute manual (held).
# #
# `held` NU e o stare noua in masina de stari: un rand cu `status='queued' AND # `held` NU e o stare noua in masina de stari: un rand cu `status='queued' AND
# held=1` este in coada, dar tinut pana la eliberarea umana. Il afisam distinct # held=1` este in coada, dar tinut pana la eliberarea umana. Il afisam distinct
@@ -96,9 +96,8 @@ def eticheta_scurta(status: str, held: bool = False) -> str:
Returneaza eticheta compacta (pill) pentru o stare de submission. Returneaza eticheta compacta (pill) pentru o stare de submission.
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
intoarce "Manual" (distinct de "In coada") — vezi PRD 5.19 D5. Pentru orice intoarce "Manual" (distinct de "In coada"). Pentru orice alta stare `held`
alta stare `held` este ignorat, iar apelul fara `held` ramane identic cu cel este ignorat.
dinainte de US-006.
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
stari noi adaugate in schema fara mapare corespunzatoare. stari noi adaugate in schema fara mapare corespunzatoare.
@@ -120,8 +119,7 @@ def eticheta_stare(status: str, held: bool = False) -> Eticheta:
Cand `status='queued'` si `held` este adevarat, randul e tinut manual si Cand `status='queued'` si `held` este adevarat, randul e tinut manual si
intoarce eticheta "In asteptare (manual)" cu clasa de avertizare (amber / intoarce eticheta "In asteptare (manual)" cu clasa de avertizare (amber /
--warn) — vezi PRD 5.19 US-006/D5. Pentru orice alta stare `held` este --warn). Pentru orice alta stare `held` este ignorat.
ignorat, iar apelul fara `held` ramane identic cu cel dinainte de US-006.
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
stari noi adaugate in schema fara mapare corespunzatoare. stari noi adaugate in schema fara mapare corespunzatoare.
@@ -385,7 +383,7 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str:
Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman
sau parse_erori care asteapta un JSON string si ar produce repr Python brut sau parse_erori care asteapta un JSON string si ar produce repr Python brut
prin fallback ``raw[:160]`` (bug documentat in PRD 5.11 US-003). prin fallback ``raw[:160]``.
Logica de prioritate: Logica de prioritate:
- already_sent / duplicate_in_file -> "" (template le afiseaza separat) - already_sent / duplicate_in_file -> "" (template le afiseaza separat)
@@ -436,7 +434,7 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Etichete mediu RAR (Test / Productie) — US-010 PRD 5.20 # Etichete mediu RAR (Test / Productie)
# #
# Conventia de culori (semantica risc L.142): # Conventia de culori (semantica risc L.142):
# prod = env-badge-prod (fill atentie, text alb) — declaratie REALA, ireversibila. # prod = env-badge-prod (fill atentie, text alb) — declaratie REALA, ireversibila.

View File

@@ -1,6 +1,6 @@
<div id="acasa-section"> <div id="acasa-section">
{# === Banner ne-blocant: cont incomplet (US-002) === {# === Banner ne-blocant: cont incomplet ===
Apare cand accounts.name / email / CUI sunt necompletate (conturi legacy sau create din CLI). Apare cand accounts.name / email / CUI sunt necompletate (conturi legacy sau create din CLI).
NU blocheaza importul sau uploadul — doar orienteaza operatorul sa completeze datele. NU blocheaza importul sau uploadul — doar orienteaza operatorul sa completeze datele.
Dispare automat dupa ce contul devine complet (re-render la urmatoarea navigare/reload). Dispare automat dupa ce contul devine complet (re-render la urmatoarea navigare/reload).
@@ -13,7 +13,7 @@
</div> </div>
{% endif %} {% endif %}
{# === Container colapsabil: stepper + upload intr-un singur element <details> (US-006). {# === Container colapsabil: stepper + upload intr-un singur element <details>.
Serverul seteaza atributul `open` din are_trimiteri: Serverul seteaza atributul `open` din are_trimiteri:
are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS) are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS)
are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click) are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click)

View File

@@ -1,10 +1,10 @@
{# {#
_auto_send_toggle.html — comutatorul "Trimite automat la RAR" (PRD 5.19 US-004). _auto_send_toggle.html — comutatorul "Trimite automat la RAR".
Plasat in clusterul de header (base.html, langa .rar-chip) — D1. NU face parte din Plasat in clusterul de header (base.html, langa .rar-chip). NU face parte din
swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus (D3). swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus.
Non-optimist (D2): la esec POST bifa revine + toast eroare (hx-on::response-error). Non-optimist: la esec POST bifa revine + toast eroare (hx-on::response-error).
account_id vine din SESIUNE server-side (ruta /auto-send), niciodata din formular. account_id vine din SESIUNE server-side (ruta /auto-send), niciodata din formular.
Raspunsul POST re-randeaza acest fragment (outerHTML pe #auto-send-toggle-wrap): Raspunsul POST re-randeaza acest fragment (outerHTML pe #auto-send-toggle-wrap):

View File

@@ -1,10 +1,10 @@
{# {#
_bulk_held_banner.html — banner bulk "Trimite toate (N)" (PRD 5.19 US-005). _bulk_held_banner.html — banner bulk "Trimite toate (N)".
N = randuri tinute ale contului (queued AND held=1). Randat: N = randuri tinute ale contului (queued AND held=1). Randat:
- initial in _coada.html (banner_oob nesetat -> fara OOB); - initial in _coada.html (banner_oob nesetat -> fara OOB);
- RE-RANDAT prin OOB (banner_oob=true) din _submissions.html la FIECARE reincarcare a - RE-RANDAT prin OOB (banner_oob=true) din _submissions.html la FIECARE reincarcare a
listei, ca sa nu ramana stale dupa eliberari (fix /code-review #3). listei, ca sa nu ramana stale dupa eliberari.
Wrapper cu id STABIL #bulk-held-banner-wrap: OOB outerHTML il inlocuieste mereu (chiar si Wrapper cu id STABIL #bulk-held-banner-wrap: OOB outerHTML il inlocuieste mereu (chiar si
gol la N=0), ca id-ul sa ramana tinta pentru urmatorul OOB. Confirmare tipata prin modal gol la N=0), ca id-ul sa ramana tinta pentru urmatorul OOB. Confirmare tipata prin modal

View File

@@ -20,7 +20,7 @@
{% set _sec_id = chips_section_id or 'chips-section' %} {% set _sec_id = chips_section_id or 'chips-section' %}
{% set _chips = prestatii_chips or [] %} {% set _chips = prestatii_chips or [] %}
{% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %} {% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %}
{# US-009: chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu. {# chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu.
Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #} Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #}
{% set _sub_id = chips_submission_id if chips_submission_id is defined else none %} {% set _sub_id = chips_submission_id if chips_submission_id is defined else none %}
@@ -72,7 +72,7 @@
&times; &times;
</button> </button>
</span> </span>
{# US-009: "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut {# "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut
(in modalul de detaliu, nu la re-randarea stateless via /form-chips). (in modalul de detaliu, nu la re-randarea stateless via /form-chips).
Reuse EXACT save_mapping + reresolve_account via endpoint dedicat. Reuse EXACT save_mapping + reresolve_account via endpoint dedicat.
hx-include="closest form" propaga csrf_token din form-ul parinte. #} hx-include="closest form" propaga csrf_token din form-ul parinte. #}
@@ -117,9 +117,9 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{# ===== US-005 (5.16): Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #} {# ===== Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #}
{# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat). {# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat).
T-7 (5.16): containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea Containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea
un chenar gol nefinisat sub randurile de operatie. #} un chenar gol nefinisat sub randurile de operatie. #}
{% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %} {% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %}
{% if _extra_chips %} {% if _extra_chips %}

View File

@@ -6,13 +6,13 @@
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);" style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);"
{% if oob %}hx-swap-oob="outerHTML"{% endif %}> {% if oob %}hx-swap-oob="outerHTML"{% endif %}>
<div class="card"> <div class="card">
{# US-002 (5.16): titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat — {# Titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat — lista incepe
lista incepe direct sub filtre. Heading pastrat sr-only pentru a11y (section direct sub filtre. Heading pastrat sr-only pentru a11y (section
aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #} aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #}
<h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2> <h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2>
{# US-005 (PRD 5.19): bulk "Trimite toate (N)". Randat initial aici; re-randat prin OOB {# Bulk "Trimite toate (N)". Randat initial aici; re-randat prin OOB
din _submissions.html la fiecare reincarcare a listei (fix staleness /code-review #3). #} din _submissions.html la fiecare reincarcare a listei (evita staleness). #}
{% include '_bulk_held_banner.html' %} {% include '_bulk_held_banner.html' %}
{% if blocate_total %} {% if blocate_total %}
@@ -44,8 +44,8 @@
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}"> <input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #} {# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
<input type="hidden" id="f-page" name="page" value="1"> <input type="hidden" id="f-page" name="page" value="1">
{# US-007 (5.19): filtrul "doar tinute" — persistat prin OOB din _submissions.html, ca sa {# Filtrul "doar tinute" — persistat prin OOB din _submissions.html, ca sa
nu se piarda la reincarcare (trimiteriChanged) sau paginare (fix /code-review #2). #} nu se piarda la reincarcare (trimiteriChanged) sau paginare. #}
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}"> <input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}">
{# === STANGA: Quick-pills de data (preset interval) + buton Custom === {# === STANGA: Quick-pills de data (preset interval) + buton Custom ===

View File

@@ -1,7 +1,7 @@
{# _editare_preview_modal.html — fragment de editare rand preview in modalul global. {# _editare_preview_modal.html — fragment de editare rand preview in modalul global.
US-006 (PRD 5.12): swap-uit in #detaliu-modal-body de butonul Editeaza din preview. Swap-uit in #detaliu-modal-body de butonul Editeaza din preview.
US-007 (PRD 5.12): butonul 'Confirma valorile' apare DOAR pe randurile needs_review Butonul 'Confirma valorile' apare DOAR pe randurile needs_review si trimite CSRF
(T2): trimite CSRF POST la /confirma-review, inchide modalul via HX-Trigger-After-Settle. POST la /confirma-review, inchide modalul via HX-Trigger-After-Settle.
Necesita din context: Necesita din context:
import_id — id batch import import_id — id batch import
@@ -52,7 +52,7 @@
</form> </form>
{% if is_needs_review %} {% if is_needs_review %}
{# T2 (US-007): Butonul 'Confirma valorile' apare DOAR pe randurile needs_review. {# Butonul 'Confirma valorile' apare DOAR pe randurile needs_review.
POST separat (form propriu) la /confirma-review cu CSRF. Raspunsul inchide POST separat (form propriu) la /confirma-review cu CSRF. Raspunsul inchide
modalul via HX-Trigger-After-Settle: inchideModal + swap OOB randul si countorii. #} modalul via HX-Trigger-After-Settle: inchideModal + swap OOB randul si countorii. #}
<form hx-post="/_import/{{ import_id }}/rand/{{ row_index }}/confirma-review" <form hx-post="/_import/{{ import_id }}/rand/{{ row_index }}/confirma-review"

View File

@@ -1,6 +1,6 @@
{# _form_editare.html — partial partajat slim: campurile vehicul/data/odo + obs + chips prestatii. {# _form_editare.html — partial partajat slim: campurile vehicul/data/odo + obs + chips prestatii.
US-007 (PRD 5.15): redesign slim cu VIN unic, Observatii textarea, chips prestatii (E4), Redesign slim cu VIN unic, Observatii textarea, chips prestatii, si reveal dinamic
si reveal dinamic odometru initial cand chips contin R-ODO/I-ODO (D10c, E6 server-driven). odometru initial cand chips contin R-ODO/I-ODO (server-driven).
Inclus cu {% include "_form_editare.html" %} INSIDE un <form> element al Inclus cu {% include "_form_editare.html" %} INSIDE un <form> element al
template-ului parinte. Acel parinte pune form-ul, CSRF-ul si orice campuri template-ului parinte. Acel parinte pune form-ul, CSRF-ul si orice campuri
@@ -36,7 +36,7 @@
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }} err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
</div> </div>
{# === 3. Observatii (obs) — textarea liber, US-005 === #} {# === 3. Observatii (obs) — textarea liber === #}
<div class="camp-slim"> <div class="camp-slim">
<label for="c-obs">Observatii (operatiile efectuate)</label> <label for="c-obs">Observatii (operatiile efectuate)</label>
<textarea id="c-obs" name="obs" rows="2" <textarea id="c-obs" name="obs" rows="2"
@@ -44,7 +44,7 @@
placeholder="ex: Revizie; schimbare placute frana">{{ obs_val or '' }}</textarea> placeholder="ex: Revizie; schimbare placute frana">{{ obs_val or '' }}</textarea>
</div> </div>
{# === 4. Prestatii chips (E4 server-driven, US-007) === #} {# === 4. Prestatii chips (server-driven) === #}
{% set form_chips_url = form_chips_url or '/form-chips' %} {% set form_chips_url = form_chips_url or '/form-chips' %}
{% set chips_section_id = 'chips-section' %} {% set chips_section_id = 'chips-section' %}
{% include "_chips_prestatii.html" %} {% include "_chips_prestatii.html" %}

View File

@@ -10,7 +10,7 @@
{% else %} {% else %}
<span class="muted" style="font-size:12px;">doar evenimentele contului tau</span> <span class="muted" style="font-size:12px;">doar evenimentele contului tau</span>
{% endif %} {% endif %}
{# Badge mediu RAR activ (US-010 PRD 5.20) — mediul implicit al contului #} {# Badge mediu RAR activ — mediul implicit al contului #}
{% if env_default | default('') %} {% if env_default | default('') %}
{% set _eb = eticheta_env(env_default) %} {% set _eb = eticheta_env(env_default) %}
<span class="{{ _eb[1] }}" style="margin-left:auto;" title="Mediu RAR activ">{{ _eb[0] }}</span> <span class="{{ _eb[1] }}" style="margin-left:auto;" title="Mediu RAR activ">{{ _eb[0] }}</span>

View File

@@ -1,12 +1,12 @@
{# Macro-uri partajate intre template-urile de import si mapari. #} {# Macro-uri partajate intre template-urile de import si mapari. #}
{# US-002 (PRD 5.11): autosend_toggle neutralizat — auto_send nu mai tine randuri (US-001). {# autosend_toggle neutralizat — auto_send nu mai tine randuri.
Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html) Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html)
dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #} dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #}
{% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %} {% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %}
{# US-005 (PRD 5.12): macro `camp` partajat — extras din _trimitere_detaliu.html si {# macro `camp` partajat — extras din _trimitere_detaliu.html si
_preview_rand.html. Suporta tip='date' (calendar nativ, D#10/R3) si tip='text' (default). _preview_rand.html. Suporta tip='date' (calendar nativ) si tip='text' (default).
Parametri: Parametri:
nome — name="" al input-ului (si cheie in err_map/fix_map) nome — name="" al input-ului (si cheie in err_map/fix_map)
@@ -19,14 +19,14 @@
id_prefix — prefix pentru id="" al input-ului (default 'c'; preview poate folosi 'e-N') id_prefix — prefix pentru id="" al input-ului (default 'c'; preview poate folosi 'e-N')
#} #}
{% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c', slim=False, mono=False) %} {% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c', slim=False, mono=False) %}
{# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim) din US-002 PRD 5.15: {# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim):
label 11px muted deasupra, input ~30px, fundal --card2. label 11px muted deasupra, input ~30px, fundal --card2.
mono=True (valid numai cu slim=True): adauga clasa 'camp-mono' pe input pentru campuri mono=True (valid numai cu slim=True): adauga clasa 'camp-mono' pe input pentru campuri
VIN/odometru/nr (IBM Plex Mono, prin .camp-slim .camp-mono din base.html). #} VIN/odometru/nr (IBM Plex Mono, prin .camp-slim .camp-mono din base.html). #}
<div {% if slim %}class="camp-slim"{% else %}style="margin-bottom:10px;"{% endif %}> <div {% if slim %}class="camp-slim"{% else %}style="margin-bottom:10px;"{% endif %}>
<label for="{{ id_prefix }}-{{ nome }}"{% if not slim %} class="muted" style="font-size:12px; display:block;"{% endif %}>{{ eticheta }}</label> <label for="{{ id_prefix }}-{{ nome }}"{% if not slim %} class="muted" style="font-size:12px; display:block;"{% endif %}>{{ eticheta }}</label>
{% if tip == 'date' %} {% if tip == 'date' %}
{# D#10/R3: degradare grijulie pentru valori ne-YYYY-MM-DD. {# Degradare grijulie pentru valori ne-YYYY-MM-DD.
Daca valoarea nu e in formatul corect, inputul ramane gol + hint + hidden cu valoarea bruta Daca valoarea nu e in formatul corect, inputul ramane gol + hint + hidden cu valoarea bruta
(ca sa nu se piarda tacut la submit). #} (ca sa nu se piarda tacut la submit). #}
{%- set _dp_ok = (valoare and valoare|length == 10 and valoare[4:5] == '-' and valoare[7:8] == '-') -%} {%- set _dp_ok = (valoare and valoare|length == 10 and valoare[4:5] == '-' and valoare[7:8] == '-') -%}
@@ -57,7 +57,7 @@
</div> </div>
{% endmacro %} {% endmacro %}
{# PRD 5.13 — sistem butoane de actiune responsive. {# Sistem butoane de actiune responsive.
CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group) CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group)
este definit in base.html. este definit in base.html.
Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #} Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #}

View File

@@ -54,7 +54,7 @@
<div class="muted">{{ e.denumire or '(fara denumire)' }}</div> <div class="muted">{{ e.denumire or '(fara denumire)' }}</div>
</td> </td>
<td class="muted" style="font-size:12px;" data-eticheta="Sugestii"> <td class="muted" style="font-size:12px;" data-eticheta="Sugestii">
{# 5.18 US-007: badge sursa pe sugestia sistemului — confirmat (GOLD) / similar {# Badge sursa pe sugestia sistemului — confirmat (GOLD) / similar
(SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #} (SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #}
{% if e.sugestie_principala %} {% if e.sugestie_principala %}
{% if e.sugestie_principala.sursa == 'gold_partajat' %} {% if e.sugestie_principala.sursa == 'gold_partajat' %}

View File

@@ -82,7 +82,7 @@
</span> </span>
</div> </div>
{# Tabel mapare: coloana din fisier | exemplu | camp RAR (mockup 5.16 / US-013) #} {# Tabel mapare: coloana din fisier | exemplu | camp RAR #}
<div class="tablewrap" style="margin-bottom:16px;"> <div class="tablewrap" style="margin-bottom:16px;">
<table style="border-collapse:collapse; width:100%;"> <table style="border-collapse:collapse; width:100%;">
<thead> <thead>

View File

@@ -1,5 +1,5 @@
{# {#
_modal_confirma_trimitere.html — modal REAL de confirmare tipata (PRD 5.19 D4/F4/F5). _modal_confirma_trimitere.html — modal REAL de confirmare tipata.
`hx-confirm` nativ (doar OK/Cancel) NU e suficient pentru o actiune ireversibila care `hx-confirm` nativ (doar OK/Cancel) NU e suficient pentru o actiune ireversibila care
trimite N prezentari catre RAR. Acest modal arata numarul + mediul tinta (Testare / trimite N prezentari catre RAR. Acest modal arata numarul + mediul tinta (Testare /

View File

@@ -14,7 +14,7 @@
Preview — Preview —
<span class="muted" style="font-weight:400;">{{ filename or ("import #" ~ import_id) }}</span> <span class="muted" style="font-weight:400;">{{ filename or ("import #" ~ import_id) }}</span>
</h2> </h2>
{# Badge mediu RAR (US-009): vizibil intotdeauna in preview (claritate tinta) #} {# Badge mediu RAR: vizibil intotdeauna in preview (claritate tinta) #}
{% if rar_env %} {% if rar_env %}
<span class="pill" style="font-size:var(--fs-xs); <span class="pill" style="font-size:var(--fs-xs);
{% if rar_env == 'prod' %}background:color-mix(in srgb,#B4452F 15%,var(--card)); border-color:#B4452F; color:#B4452F; font-weight:700; {% if rar_env == 'prod' %}background:color-mix(in srgb,#B4452F 15%,var(--card)); border-color:#B4452F; color:#B4452F; font-weight:700;
@@ -71,7 +71,7 @@
</div> </div>
<!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload). <!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload).
US-004: un singur <form> cu un select per operatie + un singur buton Salveaza. --> Un singur <form> cu un select per operatie + un singur buton Salveaza. -->
{% if unmapped_ops %} {% if unmapped_ops %}
<div class="card" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:14px;"> <div class="card" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:14px;">
<h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3> <h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3>
@@ -98,7 +98,7 @@
{% if e.suggestions or e.sugestie_principala or (e.surse_sugestie and e.surse_sugestie.nul) %} {% if e.suggestions or e.sugestie_principala or (e.surse_sugestie and e.surse_sugestie.nul) %}
<div class="muted" style="font-size:12px; margin-top:4px;"> <div class="muted" style="font-size:12px; margin-top:4px;">
sugestii: sugestii:
{# 5.18 US-007: badge sursa pe sugestia sistemului — confirmat (GOLD) / similar {# Badge sursa pe sugestia sistemului — confirmat (GOLD) / similar
(SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #} (SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #}
{% if e.sugestie_principala %} {% if e.sugestie_principala %}
{% if e.sugestie_principala.sursa == 'gold_partajat' %} {% if e.sugestie_principala.sursa == 'gold_partajat' %}
@@ -134,7 +134,7 @@
</div> </div>
{% endif %} {% endif %}
<!-- Banner discoverability (T1, US-007): vizibil cand exista randuri needs_review. <!-- Banner discoverability: vizibil cand exista randuri needs_review.
Explica operatorului ca randurile cu 'Verifica valori' nu pleaca la RAR Explica operatorului ca randurile cu 'Verifica valori' nu pleaca la RAR
pana le deschide in modal si apasa 'Confirma valorile'. Dispare via OOB pana le deschide in modal si apasa 'Confirma valorile'. Dispare via OOB
cand summary.needs_review == 0. --> cand summary.needs_review == 0. -->
@@ -213,7 +213,7 @@
hx-target="#import-section" hx-target="#import-section"
hx-swap="outerHTML"> hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
{# Mediu RAR ales la upload — propagar la commit (US-009) #} {# Mediu RAR ales la upload — propagar la commit #}
<input type="hidden" name="rar_env" value="{{ rar_env or '' }}"> <input type="hidden" name="rar_env" value="{{ rar_env or '' }}">
<div class="sticky-bar"> <div class="sticky-bar">
<div style="flex:1; min-width:280px;"> <div style="flex:1; min-width:280px;">
@@ -285,7 +285,7 @@ din <span id="n-hint-ok">{{ summary.get('ok', 0) }}</span> gata de trimis
} }
/* Actualizeaza N dupa editare/confirmare rand (OOB). /* Actualizeaza N dupa editare/confirmare rand (OOB).
US-007: reviewed_rows (checkboxe) eliminate; N = randurile ok din DB, reviewed_rows (checkboxe) eliminate; N = randurile ok din DB,
actualizate via OOB (#preview-ok-count[data-ok]) dupa /confirma-review sau /editeaza. */ actualizate via OOB (#preview-ok-count[data-ok]) dupa /confirma-review sau /editeaza. */
function updateN() { function updateN() {
var total = getOk(); var total = getOk();

View File

@@ -1,7 +1,7 @@
{# {#
_preview_rand.html — un singur rand de preview import. _preview_rand.html — un singur rand de preview import.
US-006 (PRD 5.12): editarea inline (tr.preview-edit + mutual-exclusion script) Editarea inline (tr.preview-edit + mutual-exclusion script) a fost eliminata.
a fost eliminata. Butonul Editeaza deschide MODALUL global (#detaliu-modal-body). Butonul Editeaza deschide MODALUL global (#detaliu-modal-body).
Parametri: Parametri:
editing — ELIMINAT (ignorat, pastrat pentru compatibilitate apeluri vechi) editing — ELIMINAT (ignorat, pastrat pentru compatibilitate apeluri vechi)
@@ -39,7 +39,7 @@
<span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;" <span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;"
{% if _nota %}title="{{ _nota }}"{% endif %}> {% if _nota %}title="{{ _nota }}"{% endif %}>
<span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span> <span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span>
{# Badge mediu RAR (US-010 PRD 5.20) — rar_env disponibil din contextul batch-ului. #} {# Badge mediu RAR — rar_env disponibil din contextul batch-ului. #}
{% if rar_env | default('') %} {% if rar_env | default('') %}
{% set _eb = eticheta_env(rar_env) %} {% set _eb = eticheta_env(rar_env) %}
<div style="margin-top:3px;"><span class="{{ _eb[1] }}">{{ _eb[0] }}</span></div> <div style="margin-top:3px;"><span class="{{ _eb[1] }}">{{ _eb[0] }}</span></div>

View File

@@ -14,10 +14,10 @@
</div> </div>
{% endif %} {% endif %}
{# === US-007 (PRD 5.19): banner coada tinuta imbatranita — mitigare OBLIGATORIE pt. default OFF. {# Banner coada tinuta imbatranita — mitigare pt. default OFF.
Cand exista randuri `queued AND held=1` mai vechi de held_aged_days, semnalul de conformitate Cand exista randuri `queued AND held=1` mai vechi de held_aged_days, semnalul de conformitate
L.142 (declarare obligatorie). Derivat (zero stare noua), varsta pe created_at. L.142 (declarare obligatorie). Derivat (zero stare noua), varsta pe created_at.
Prioritate bannere (D7 — un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe Prioritate bannere (un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe
mobil): cont inactiv (sus) + strip RAR jos raman separate; intre notice-uri, held-aged > mobil): cont inactiv (sus) + strip RAR jos raman separate; intre notice-uri, held-aged >
trial (risc legal > notita trial) -> trial e suprimat cand held-aged e activ. #} trial (risc legal > notita trial) -> trial e suprimat cand held-aged e activ. #}
{% set _held_aged_activ = (held_aged_count|default(0) > 0) and account_active %} {% set _held_aged_activ = (held_aged_count|default(0) > 0) and account_active %}
@@ -40,7 +40,7 @@
</div> </div>
{% endif %} {% endif %}
{# US-006 (5.17) — Banner one-time trial->Gratuit (T-DES-1): afisat la prima incarcare {# Banner one-time trial->Gratuit: afisat la prima incarcare
dupa expirarea trial-ului. Discret, non-blocant; dismissibil via sessionStorage. dupa expirarea trial-ului. Discret, non-blocant; dismissibil via sessionStorage.
Nu acopera stripul de sanatate (apare inainte de health strip, la acelasi nivel). #} Nu acopera stripul de sanatate (apare inainte de health strip, la acelasi nivel). #}
{% if trial_expirat_recent|default(false) and not _held_aged_activ %} {% if trial_expirat_recent|default(false) and not _held_aged_activ %}
@@ -60,7 +60,7 @@
<script>(function(){ if(sessionStorage.getItem('tfx')){ var el=document.getElementById('banner-trial-expirat'); if(el) el.style.display='none'; } })();</script> <script>(function(){ if(sessionStorage.getItem('tfx')){ var el=document.getElementById('banner-trial-expirat'); if(el) el.style.display='none'; } })();</script>
{% endif %} {% endif %}
{# === US-003 (PRD 5.16): Banda de stare RAR — NUMAI cand BLOCAT (rosu, lat de 100%). {# === Banda de stare RAR — NUMAI cand BLOCAT (rosu, lat de 100%).
OK = dot verde in antet (base.html); banda nu mai apare cand totul e ok. OK = dot verde in antet (base.html); banda nu mai apare cand totul e ok.
Elementul id="strip-sanatate" ramane in DOM mereu, dar goleste continutul cand OK, Elementul id="strip-sanatate" ramane in DOM mereu, dar goleste continutul cand OK,
astfel "hidden" + fara continut eroare in sursa = nu pica testele de prezenta id-ului. astfel "hidden" + fara continut eroare in sursa = nu pica testele de prezenta id-ului.
@@ -84,7 +84,7 @@
</div> </div>
{% endif %} {% endif %}
{# === US-002 (PRD 5.16): 5 carduri-contor separate (desktop) + bara compacta (mobil <=560px). {# === 5 carduri-contor separate (desktop) + bara compacta (mobil <=560px).
Total / Luna asta / Azi / In coada / De corectat. Total / Luna asta / Azi / In coada / De corectat.
#} #}
{# Desktop: 5 carduri side-by-side. display:flex + layout stau in CSS (.contoare-desktop in {# Desktop: 5 carduri side-by-side. display:flex + layout stau in CSS (.contoare-desktop in
@@ -109,7 +109,7 @@
<div class="contor-label">Azi</div> <div class="contor-label">Azi</div>
</div> </div>
{# In coada — contorul "In asteptare (manual)" se pliaza aici (US-006/D6): nu adaugam a 6-a {# In coada — contorul "In asteptare (manual)" se pliaza aici: nu adaugam a 6-a
celula, ci o sub-linie amber cand exista randuri tinute (queued AND held=1). #} celula, ci o sub-linie amber cand exista randuri tinute (queued AND held=1). #}
<div class="contor-card" style="flex:1; min-width:80px;"> <div class="contor-card" style="flex:1; min-width:80px;">
<div class="contor-cifra s-queued">{{ counts_queued }}</div> <div class="contor-cifra s-queued">{{ counts_queued }}</div>
@@ -156,11 +156,11 @@
</a> </a>
</div> </div>
{# === US-011 (5.20): Indicator mediu RAR activ per cont. {# === Indicator mediu RAR activ per cont.
La 0 medii: nu afisa (cont fara configuratie RAR). La 0 medii: nu afisa (cont fara configuratie RAR).
La 1 mediu: eticheta statica (fara toggle). La 1 mediu: eticheta statica (fara toggle).
La 2 medii: buton de comutare HTMX (toggle conditionat). La 2 medii: buton de comutare HTMX (toggle conditionat).
US-010: culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste). Culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste).
#} #}
{% set _medii = medii_disponibile | default([]) %} {% set _medii = medii_disponibile | default([]) %}
{% set _env = env_default | default('prod') %} {% set _env = env_default | default('prod') %}
@@ -194,7 +194,7 @@
{% endif %} {% endif %}
{# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping === {# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping ===
Pastrata exact ca inainte (US-005): tab_activ determina marcajul activ. Pastrata exact ca inainte: tab_activ determina marcajul activ.
#} #}
{% set _tab = tab_activ | default('acasa') %} {% set _tab = tab_activ | default('acasa') %}
<nav class="status-nav" aria-label="Navigatie rapida" <nav class="status-nav" aria-label="Navigatie rapida"
@@ -207,7 +207,7 @@
class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a> class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a>
</nav> </nav>
{# US-006 (5.17) + T-6 (5.16): linia de plan in CORP apare DOAR in starea de avertizare {# Linia de plan in CORP apare DOAR in starea de avertizare
(>=80% -> --warn; limita atinsa -> --err). Consumul normal (N/60) traieste in badge-ul (>=80% -> --warn; limita atinsa -> --err). Consumul normal (N/60) traieste in badge-ul
din antet + linia din meniul burger, nu ca rand permanent in corp (densitate redusa). din antet + linia din meniul burger, nu ca rand permanent in corp (densitate redusa).
Ierarhie: nu concureaza cu stripul de sanatate (zero-silent-failures pastrat). #} Ierarhie: nu concureaza cu stripul de sanatate (zero-silent-failures pastrat). #}

View File

@@ -1,5 +1,5 @@
{# {#
_stepper.html — Antet wizard import COMPACT (PUR vizual). PRD 5.13. _stepper.html — Antet wizard import COMPACT (PUR vizual).
Parametru: `pas` (integer 1-4). Clasele .stepper-* sunt definite in base.html. Parametru: `pas` (integer 1-4). Clasele .stepper-* sunt definite in base.html.
>=1024px: bara slim orizontala (.stepper-track). <1024px: forma colapsata >=1024px: bara slim orizontala (.stepper-track). <1024px: forma colapsata
"Pasul N din 4 - Titlu" + bara de progres (.stepper-collapsed). "Pasul N din 4 - Titlu" + bara de progres (.stepper-collapsed).

View File

@@ -6,7 +6,7 @@
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true"> <input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
{# OOB: sincronizeaza filtrul "doar tinute" (#f-held) cu starea randata acum, ca reincarcarile {# OOB: sincronizeaza filtrul "doar tinute" (#f-held) cu starea randata acum, ca reincarcarile
(trimiteriChanged / paginare) sa-l pastreze in loc sa revina la lista completa (/code-review #2). #} (trimiteriChanged / paginare) sa-l pastreze in loc sa revina la lista completa. #}
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}" hx-swap-oob="true"> <input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}" hx-swap-oob="true">
{# OOB: banner bulk "Trimite toate (N)" re-randat la fiecare reincarcare a listei (fix staleness). #} {# OOB: banner bulk "Trimite toate (N)" re-randat la fiecare reincarcare a listei (fix staleness). #}
@@ -20,7 +20,7 @@
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span> <span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
{% if bulk_message %} {% if bulk_message %}
{# Sumar actiune bulk (US-010 PRD 5.15): afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #} {# Sumar actiune bulk: afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #}
<div class="bulk-message" role="status" aria-live="polite" <div class="bulk-message" role="status" aria-live="polite"
style="font-size:13px; color:var(--ink); background:var(--card2); style="font-size:13px; color:var(--ink); background:var(--card2);
border:1px solid var(--line); border-radius:6px; border:1px solid var(--line); border-radius:6px;
@@ -29,8 +29,8 @@
</div> </div>
{% endif %} {% endif %}
{# US-007 (5.19): chip filtru activ "doar tinute" cu buton de golire (fix /code-review #2 — {# Chip filtru activ "doar tinute" cu buton de golire — filtrul held e sticky
filtrul held e sticky prin OOB, deci ii dam un off-switch explicit). #} prin OOB, deci ii dam un off-switch explicit. #}
{% if f_held %} {% if f_held %}
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:var(--fs-sm);"> <div style="display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:var(--fs-sm);">
<span style="display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:99px; <span style="display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:99px;
@@ -47,7 +47,7 @@
{% endif %} {% endif %}
{% if rows %} {% if rows %}
{# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix, US-010), {# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix),
(2) sterge selectate (sterge-bulk). Selectia opereaza DOAR pe randuri blocate (2) sterge selectate (sterge-bulk). Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only). (gestionabil); sent/sending/queued nu au checkbox (read-only).
Butonul "Aplica cod" foloseste hx-post propriu (override form action). Butonul "Aplica cod" foloseste hx-post propriu (override form action).
@@ -62,7 +62,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div style="display:flex; justify-content:flex-end; align-items:center; <div style="display:flex; justify-content:flex-end; align-items:center;
gap:6px; margin-bottom:8px; flex-wrap:wrap;"> gap:6px; margin-bottom:8px; flex-wrap:wrap;">
{# Bulk-fix: input cod + buton aplica (US-010 PRD 5.15) #} {# Bulk-fix: input cod + buton aplica #}
<input type="text" name="cod_prestatie" id="bulk-fix-cod" <input type="text" name="cod_prestatie" id="bulk-fix-cod"
placeholder="Cod RAR (ex: OE-1)" placeholder="Cod RAR (ex: OE-1)"
autocomplete="off" autocapitalize="characters" autocomplete="off" autocapitalize="characters"
@@ -89,7 +89,7 @@
</button> </button>
</div> </div>
{# Lista slim trimiteri (US-004, PRD 5.15). {# Lista slim trimiteri.
Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill. Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill.
Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile
pe linia meta discreta (linia 3) si in modalul de detaliu. #} pe linia meta discreta (linia 3) si in modalul de detaliu. #}
@@ -160,14 +160,14 @@
{% endif %} {% endif %}
</div> </div>
{# Zona dreapta: pill stare + badge mediu RAR (US-010 PRD 5.20) + buton Trimite (US-005) #} {# Zona dreapta: pill stare + badge mediu RAR + buton Trimite #}
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;"> <div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;">
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span> <span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
{% if r.rar_env %} {% if r.rar_env %}
{% set _eb = eticheta_env(r.rar_env) %} {% set _eb = eticheta_env(r.rar_env) %}
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span> <span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
{% endif %} {% endif %}
{# US-005 (PRD 5.19 / D6): buton "Trimite" DOAR pe randurile tinute (queued AND held=1). {# Buton "Trimite" DOAR pe randurile tinute (queued AND held=1).
Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul
de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #} de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #}
{% if r.tinut %} {% if r.tinut %}

View File

@@ -7,7 +7,7 @@
{% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %} {% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
<div class="card" id="detaliu-card-{{ id }}" style="border:none; padding:0; margin:0;"> <div class="card" id="detaliu-card-{{ id }}" style="border:none; padding:0; margin:0;">
{# === Header — #id + pill stare + badge mediu RAR (US-010 PRD 5.20) + motiv === #} {# === Header — #id + pill stare + badge mediu RAR + motiv === #}
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px;"> <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px;">
<h2 id="detaliu-modal-titlu" style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2> <h2 id="detaliu-modal-titlu" style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2>
<span class="pill {{ stare_css }}">{{ stare_text }}</span> <span class="pill {{ stare_css }}">{{ stare_text }}</span>
@@ -110,14 +110,13 @@
hx-disabled-elt="find button"> hx-disabled-elt="find button">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# Cleanup B (US-009 PRD 5.15): vechiul <select name="cod_prestatie"> eliminat. {# Vechiul <select name="cod_prestatie"> eliminat. Chips din _form_editare.html
Chips din _form_editare.html (via _chips_prestatii.html) il inlocuiesc complet: (via _chips_prestatii.html) il inlocuiesc complet: emit hidden inputs
emit hidden inputs name="cod_prestatie" + picker per-operatie (E4, US-007). name="cod_prestatie" + picker per-operatie.
post_corectie_trimitere foloseste form.getlist("cod_prestatie") → compatibil. #} post_corectie_trimitere foloseste form.getlist("cod_prestatie") → compatibil. #}
{# Operatie service (cod intern + denumire venita prin API/import), distinct de {# Operatie service (cod intern + denumire venita prin API/import), distinct de
operatia RAR mapata. op_service_cod="" cand lipseste → randul absent. operatia RAR mapata. op_service_cod="" cand lipseste → randul absent. #}
RAMANE in _trimitere_detaliu.html (D#5). #}
{% if prez.op_service_cod %} {% if prez.op_service_cod %}
<div style="margin:0 0 12px;"> <div style="margin:0 0 12px;">
<div class="muted" style="font-size:12px;">Operatie service</div> <div class="muted" style="font-size:12px;">Operatie service</div>
@@ -125,7 +124,7 @@
</div> </div>
{% endif %} {% endif %}
{# === Campurile vehicul/data/odo + erori/fix + buton — partial DRY (US-005). === #} {# === Campurile vehicul/data/odo + erori/fix + buton — partial DRY. === #}
{% include "_form_editare.html" %} {% include "_form_editare.html" %}
</form> </form>
{% else %} {% else %}
@@ -150,7 +149,7 @@
</div> </div>
{% endif %} {% endif %}
{# === Rand tinut manual (queued AND held=1) — afordanta de eliberare (US-005/US-010, 5.19). {# === Rand tinut manual (queued AND held=1) — afordanta de eliberare.
Buton dedicat „Trimite acum": elibereaza randul (held 1->0); worker-ul il preia la Buton dedicat „Trimite acum": elibereaza randul (held 1->0); worker-ul il preia la
urmatorul poll. Reincarca lista + inchide modalul. Confirmare 1-linie (ireversibil). === #} urmatorul poll. Reincarca lista + inchide modalul. Confirmare 1-linie (ireversibil). === #}
{% if tinut %} {% if tinut %}

View File

@@ -7,7 +7,7 @@
<meta name="description" content="Încarci fișierul tău cu operațiile service-ului, completezi o dată codurile RAR și le salvezi. ROMFAST trimite prestațiile la RAR AUTOPASS în locul tău, fără tastat manual. Conform Legii 142/2023."> <meta name="description" content="Încarci fișierul tău cu operațiile service-ului, completezi o dată codurile RAR și le salvezi. ROMFAST trimite prestațiile la RAR AUTOPASS în locul tău, fără tastat manual. Conform Legii 142/2023.">
<style> <style>
/* US-001/US-008 (PRD 5.16): IBM Plex eliminat complet — stive font sistem standard web. /* IBM Plex eliminat complet — stive font sistem standard web.
Tokenurile --font-ui / --font-mono definite in :root (sursa unica de adevar). */ Tokenurile --font-ui / --font-mono definite in :root (sursa unica de adevar). */
:root{--font-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--font-mono:ui-monospace,"SF Mono","Cascadia Code","Segoe UI Mono","Roboto Mono",Menlo,Consolas,monospace;} :root{--font-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--font-mono:ui-monospace,"SF Mono","Cascadia Code","Segoe UI Mono","Roboto Mono",Menlo,Consolas,monospace;}
*{box-sizing:border-box;} *{box-sizing:border-box;}

View File

@@ -2,7 +2,7 @@
{% block title %}Autentificare — ROA AUTOPASS{% endblock %} {% block title %}Autentificare — ROA AUTOPASS{% endblock %}
{% block content %} {% block content %}
{# /login — card de autentificare branduit, o singura coloana (simplificat din layout-ul {# /login — card de autentificare branduit, o singura coloana (simplificat din layout-ul
2-coloane US-010 PRD 5.16). Formular: CSRF, POST /login, link signup. #} 2-coloane). Formular: CSRF, POST /login, link signup. #}
<div class="login-2col" style="max-width:860px; margin:32px auto;"> <div class="login-2col" style="max-width:860px; margin:32px auto;">
{# Antet minimal deja randat in base.html (fara RAR dot, fara burger, fara account_name) #} {# Antet minimal deja randat in base.html (fara RAR dot, fara burger, fara account_name) #}
<div class="login-shell"> <div class="login-shell">

View File

@@ -16,7 +16,7 @@ Worker-ul face login per CONT cu acele creds, cache-uieste JWT (30h) in memorie
STERGE creds-urile efemere dupa primul login reusit. Token-ul in memorie acopera STERGE creds-urile efemere dupa primul login reusit. Token-ul in memorie acopera
restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul
submission care aduce creds proaspete (degradare acceptata). Fallback durabil: slotul submission care aduce creds proaspete (degradare acceptata). Fallback durabil: slotul
per-env al contului (accounts.rar_creds_{env}_enc, US-013; coloana legacy dropata). per-env al contului (accounts.rar_creds_{env}_enc; coloana legacy dropata).
Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc. Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc.
Pornire: python -m app.worker Pornire: python -m app.worker
@@ -145,7 +145,7 @@ def purge_expired(conn) -> dict[str, int]:
def expire_held(conn, settings: Settings) -> int: def expire_held(conn, settings: Settings) -> int:
"""Expira randurile tinute manual (`queued AND held=1`) mai vechi de `held_retention_days`. """Expira randurile tinute manual (`queued AND held=1`) mai vechi de `held_retention_days`.
PRD 5.19 US-008 (inchide gaura GDPR/L.142): un `queued held=1` nu e nici `sent` nici Inchide gaura GDPR/L.142: un `queued held=1` nu e nici `sent` nici
blocat -> altfel NU primeste `purge_after` -> PII criptat ar sta la nesfarsit. blocat -> altfel NU primeste `purge_after` -> PII criptat ar sta la nesfarsit.
Le trece la `error`/`TINUT_EXPIRAT` (terminal) si seteaza `purge_after` DIRECT la Le trece la `error`/`TINUT_EXPIRAT` (terminal) si seteaza `purge_after` DIRECT la
momentul expirarii (NU lasa retentia de 30z a randurilor blocate sa se adauge; altfel momentul expirarii (NU lasa retentia de 30z a randurilor blocate sa se adauge; altfel
@@ -154,7 +154,7 @@ def expire_held(conn, settings: Settings) -> int:
(`purge_after < datetime('now')`), iar `datetime('now')` are rezolutie de o secunda — (`purge_after < datetime('now')`), iar `datetime('now')` are rezolutie de o secunda —
cu `purge_after=now` cele doua ar fi egale in acelasi ciclu si randul NU s-ar purja cu `purge_after=now` cele doua ar fi egale in acelasi ciclu si randul NU s-ar purja
decat la ciclul urmator (~1h de PII in plus). Cu `-1 second` randul e purjabil IMEDIAT decat la ciclul urmator (~1h de PII in plus). Cu `-1 second` randul e purjabil IMEDIAT
in acelasi ciclu de purjare (US-008 inchide gaura strict). in acelasi ciclu de purjare.
Intoarce numarul de randuri expirate. Intoarce numarul de randuri expirate.
""" """
@@ -199,7 +199,7 @@ def claim_one(conn) -> dict | None:
"SELECT s.id, s.account_id, s.payload_json, s.rar_creds_enc, s.rar_env " "SELECT s.id, s.account_id, s.payload_json, s.rar_creds_enc, s.rar_env "
"FROM submissions s LEFT JOIN accounts a ON a.id = s.account_id " "FROM submissions s LEFT JOIN accounts a ON a.id = s.account_id "
"WHERE s.status='queued' " "WHERE s.status='queued' "
# PRD 5.19 US-003: randurile tinute manual (held=1) NU se revendica — # Randurile tinute manual (held=1) NU se revendica —
# asteapta eliberarea umana (manual/auto). Se aplica DOAR la claim din # asteapta eliberarea umana (manual/auto). Se aplica DOAR la claim din
# 'queued'; randurile deja 'sending' (orfani/reconciliere) sunt neafectate. # 'queued'; randurile deja 'sending' (orfani/reconciliere) sunt neafectate.
"AND s.held = 0 " "AND s.held = 0 "
@@ -464,7 +464,7 @@ def _creds_for(claimed: dict, settings: Settings) -> dict | None:
def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None: def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None:
"""Creds RAR durabile per-cont din slotul per-env (US-013 — coloana legacy dropata). """Creds RAR durabile per-cont din slotul per-env (coloana legacy dropata).
Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env, singurul slot valid). Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env, singurul slot valid).
""" """
@@ -482,7 +482,7 @@ def _keepalive_target(conn, settings: Settings) -> tuple[int | None, dict | None
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev). """Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala). Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala).
US-013: coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV Coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV
slotul per-env. Sare conturile ale caror creds NU se decripteaza sub cheia curenta slotul per-env. Sare conturile ale caror creds NU se decripteaza sub cheia curenta
(in dev `start.sh both` genereaza o cheie efemera noua la fiecare pornire). (in dev `start.sh both` genereaza o cheie efemera noua la fiecare pornire).
""" """
@@ -565,7 +565,7 @@ def run() -> int:
# Purjare periodica (odata pe ora) — NU mai frecvent. # Purjare periodica (odata pe ora) — NU mai frecvent.
now_ts = time.time() now_ts = time.time()
if now_ts - _last_purge_time >= _PURGE_INTERVAL_S: if now_ts - _last_purge_time >= _PURGE_INTERVAL_S:
# Expira intai randurile tinute imbatranite (US-008), apoi purjeaza: # Expira intai randurile tinute imbatranite, apoi purjeaza:
# expire_held seteaza purge_after=now-1s -> randul e purjabil IMEDIAT in # expire_held seteaza purge_after=now-1s -> randul e purjabil IMEDIAT in
# acelasi ciclu (purge_expired foloseste comparatie stricta purge_after<now). # acelasi ciclu (purge_expired foloseste comparatie stricta purge_after<now).
expired = expire_held(conn, settings) expired = expire_held(conn, settings)

View File

@@ -1,4 +1,4 @@
"""Liveness probe pentru worker (T6) — folosit de healthcheck-ul Docker. """Liveness probe pentru worker — folosit de healthcheck-ul Docker.
Worker-ul nu e server HTTP, deci `restart: always` prinde doar procesul MORT, Worker-ul nu e server HTTP, deci `restart: always` prinde doar procesul MORT,
nu si worker-ul AGATAT (proces viu care nu mai bate heartbeat). Acest probe nu si worker-ul AGATAT (proces viu care nu mai bate heartbeat). Acest probe

View File

@@ -13,7 +13,7 @@ services:
- autopass-data:/data - autopass-data:/data
environment: environment:
AUTOPASS_DB_PATH: /data/autopass.db AUTOPASS_DB_PATH: /data/autopass.db
# Ancora globala, NU tinta trimiterilor: dupa PRD 5.20 mediul RAR e per # Ancora globala, NU tinta trimiterilor: mediul RAR e per
# submission/cont (worker-ul trimite dupa submissions.rar_env). Variabila # submission/cont (worker-ul trimite dupa submissions.rar_env). Variabila
# asta serveste doar ca fallback API pentru conturi fara medii disponibile, # asta serveste doar ca fallback API pentru conturi fara medii disponibile,
# backfill la migrare (db.py) si afisaj dashboard (routes.py). Obligatorie # backfill la migrare (db.py) si afisaj dashboard (routes.py). Obligatorie

View File

@@ -22,9 +22,9 @@ os.environ.setdefault("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
# lazy-load-eze modelul de ~230MB la fiecare test care atinge editorul de mapari # lazy-load-eze modelul de ~230MB la fiecare test care atinge editorul de mapari
# (suita rapida, fara download in CI). Testele de embeddings il pornesc punctual. # (suita rapida, fara download in CI). Testele de embeddings il pornesc punctual.
os.environ.setdefault("AUTOPASS_EMBEDDINGS_ENABLED", "false") os.environ.setdefault("AUTOPASS_EMBEDDINGS_ENABLED", "false")
# Seed-ul de operatii etichetate (SILVER, PRD 5.18) e ON in app, dar OFF in teste: # Seed-ul de operatii etichetate e ON in app, dar OFF in teste: multe teste presupun
# multe teste presupun mapping_suggestions GOL la init_db. Testele US-004/005/006 il # mapping_suggestions GOL la init_db. Testele care au nevoie de el il pornesc punctual
# pornesc punctual (object.__setattr__ pe settings sau apel direct la seeder). # (object.__setattr__ pe settings sau apel direct la seeder).
os.environ.setdefault("AUTOPASS_SEED_OPERATII_ENABLED", "false") os.environ.setdefault("AUTOPASS_SEED_OPERATII_ENABLED", "false")
@@ -35,15 +35,15 @@ _REAL_INIT_DB = None
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _auto_send_on_default_account(request, monkeypatch): def _auto_send_on_default_account(request, monkeypatch):
"""PRD 5.19 US-009: contul default id=1 (seed schema.sql, INSERT OR IGNORE) porneste pe """Contul default id=1 (seed schema.sql, INSERT OR IGNORE) porneste pe
`auto_send_enabled=0` (Auto OFF) — corect in productie (nimic nu pleaca fara confirmare). `auto_send_enabled=0` (Auto OFF) — corect in productie (nimic nu pleaca fara confirmare).
Dar suita exercita lantul `POST -> claim_one -> sent` pe contul id=1 Dar suita exercita lantul `POST -> claim_one -> sent` pe contul id=1
(test_import_e2e, test_creds_delivery, test_live_rar): cu Auto OFF, ingestia ar marca (test_import_e2e, test_creds_delivery, test_live_rar): cu Auto OFF, ingestia ar marca
`held=1` (US-002) si worker-ul (`claim_one ... AND held=0`, US-003) nu ar mai lua randul `held=1` si worker-ul (`claim_one ... AND held=0`) nu ar mai lua randul
-> testele ar stagna tacit. E un fix de STARE DB, nu env var (coloana e per-rand in -> testele ar stagna tacit. E un fix de STARE DB, nu env var (coloana e per-rand in
`accounts`), deci il aplicam wraparind `init_db` (contul id=1 NU e creat de `create_account` `accounts`), deci il aplicam wraparind `init_db` (contul id=1 NU e creat de `create_account`
-> un patch pe factory NU l-ar acoperi — Eng HIGH). -> un patch pe factory NU l-ar acoperi).
Wrapam `init_db` astfel incat orice DB de test creat sa aiba id=1 pe Auto ON (held=0 la Wrapam `init_db` astfel incat orice DB de test creat sa aiba id=1 pe Auto ON (held=0 la
ingestie), pastrand suita verde ca inainte de 5.19. Conturile create explicit de teste ingestie), pastrand suita verde ca inainte de 5.19. Conturile create explicit de teste
@@ -114,7 +114,7 @@ def _reset_embeddings_singleton():
def make_test_cui(seed: str = "") -> str: def make_test_cui(seed: str = "") -> str:
"""Factory centralizat (D#14, PRD 5.12 US-001): genereaza un CUI de test unic din seed. """Factory centralizat: genereaza un CUI de test unic din seed.
Folosit de fixture-urile de test care creeaza conturi via /signup sau create_account Folosit de fixture-urile de test care creeaza conturi via /signup sau create_account
si au nevoie de un CUI unic per test (altfel unicitatea CUI-ului bloca al doilea signup si au nevoie de un CUI unic per test (altfel unicitatea CUI-ului bloca al doilea signup

View File

@@ -1,8 +1,8 @@
"""Teste US-003 (PRD 3.6): Acasa unificata — Trimiteri ca sectiune permanenta. """Teste Acasa unificata — Trimiteri ca sectiune permanenta.
Tab-ul "Trimiteri" (coada) eliminat din tab-bar; Trimiterile devin o sectiune sub Tab-ul "Trimiteri" (coada) eliminat din tab-bar; Trimiterile devin o sectiune sub
upload pe Acasa, cu heading "Trimiterile tale". ?tab=coada si /_fragments/coada upload pe Acasa, cu heading "Trimiterile tale". ?tab=coada si /_fragments/coada
servesc continutul Acasa (nu 404, nu fragment orfan). Poll aliniat la 15s (M5). servesc continutul Acasa (nu 404, nu fragment orfan).
""" """
from __future__ import annotations from __future__ import annotations
@@ -50,14 +50,14 @@ def _seed_submission(status: str = "sent", n: int = 1) -> None:
def test_tab_bar_fara_trimiteri(client): def test_tab_bar_fara_trimiteri(client):
"""US-009: tab-bar eliminat; 'Coada' nu exista; Mapari/Cont/Nomenclator raman in meniu.""" """Tab-bar eliminat; 'Coada' nu exista; Mapari/Cont/Nomenclator raman in meniu."""
r = client.get("/") r = client.get("/")
assert r.status_code == 200 assert r.status_code == 200
html = r.text html = r.text
# "Coada" nu trebuie sa existe nici ca tab, nici ca link in meniu # "Coada" nu trebuie sa existe nici ca tab, nici ca link in meniu
assert 'id="tab-coada"' not in html assert 'id="tab-coada"' not in html
assert 'href="/?tab=coada"' not in html assert 'href="/?tab=coada"' not in html
# US-009: tab-bar eliminat; Mapari/Cont/Nomenclator sunt in meniul hamburger # Mapari/Cont/Nomenclator sunt in meniul hamburger
for label in ("Mapari", "Cont", "Nomenclator"): for label in ("Mapari", "Cont", "Nomenclator"):
assert f">{label}" in html or f"{label}<" in html, f"lipseste intrarea {label} in meniu" assert f">{label}" in html or f"{label}<" in html, f"lipseste intrarea {label} in meniu"
@@ -70,8 +70,7 @@ def test_acasa_contine_sectiunea_trimiteri(client):
html = r.text html = r.text
assert 'id="filtre-trimiteri"' in html assert 'id="filtre-trimiteri"' in html
assert "/_fragments/submissions" in html assert "/_fragments/submissions" in html
# PRD 5.9 US-003: detaliul s-a mutat in modalul global (#modal-detaliu); vechiul # Detaliul e in modalul global (#modal-detaliu); panoul inert vechi eliminat.
# panou inert #trimitere-detaliu a fost eliminat.
assert 'id="modal-detaliu"' in html assert 'id="modal-detaliu"' in html
assert 'id="trimitere-detaliu"' not in html assert 'id="trimitere-detaliu"' not in html
@@ -108,7 +107,7 @@ def test_acasa_fara_linkuri_ajutor(client):
def test_acasa_fara_wayfinding_ajutor(client): def test_acasa_fara_wayfinding_ajutor(client):
"""US-001 (5.5): randul 'Ajutor' (wayfinding Mapari/Coduri RAR) eliminat din Acasa — """Randul 'Ajutor' (wayfinding Mapari/Coduri RAR) eliminat din Acasa —
navigarea traieste in tab-bar si in meniul de cont.""" navigarea traieste in tab-bar si in meniul de cont."""
r = client.get("/?tab=acasa") r = client.get("/?tab=acasa")
html = r.text html = r.text
@@ -132,7 +131,7 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
"""Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se """Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se
incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri. incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri.
Reimprospatarea automata la date noi externe se face prin pollerul de versiune Reimprospatarea automata la date noi externe se face prin pollerul de versiune
care cheama reincarcaTrimiteri() — fara nudge manual (US-008).""" care cheama reincarcaTrimiteri()."""
_seed_submission("sent") _seed_submission("sent")
r = client.get("/?tab=acasa") r = client.get("/?tab=acasa")
html = r.text html = r.text
@@ -142,6 +141,6 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}" assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
assert "reincarcaTrimiteri" in wrap assert "reincarcaTrimiteri" in wrap
assert "trimiteriChanged" in wrap assert "trimiteriChanged" in wrap
# Pollerul de versiune exista (auto-refresh la date noi externe). Nudge eliminat (US-008). # Pollerul de versiune exista (auto-refresh la date noi externe); fara nudge manual.
assert "/_fragments/trimiteri-versiune" in html assert "/_fragments/trimiteri-versiune" in html
assert 'id="nudge-trimiteri"' not in html assert 'id="nudge-trimiteri"' not in html

View File

@@ -1,4 +1,4 @@
"""Teste US-004 (PRD 5.5): model de stare a contului `accounts.status` + helperi. """Teste model de stare a contului `accounts.status` + helperi.
Acopera: derivarea status din active la migrare, invariantul active=1 <=> status='active', Acopera: derivarea status din active la migrare, invariantul active=1 <=> status='active',
helperii set_status/delete_account, protectia contului de sistem id=1, excluderea 'deleted' helperii set_status/delete_account, protectia contului de sistem id=1, excluderea 'deleted'
@@ -94,7 +94,7 @@ def test_delete_purjeaza_pii_si_elibereaza_cui(conn):
"""Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil).""" """Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil)."""
from app.accounts import create_account, delete_account, list_accounts from app.accounts import create_account, delete_account, list_accounts
acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True) acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True)
# US-013: creds in sloturi per-env (rar_creds_enc legacy dropata) # creds in sloturi per-env
conn.execute( conn.execute(
"UPDATE accounts SET rar_creds_test_enc='secret_enc_test', rar_creds_prod_enc='secret_enc_prod' WHERE id=?", "UPDATE accounts SET rar_creds_test_enc='secret_enc_test', rar_creds_prod_enc='secret_enc_prod' WHERE id=?",
(acct_id,), (acct_id,),
@@ -138,7 +138,6 @@ def test_migrare_deriva_status_din_active(conn):
from app.db import _migrate from app.db import _migrate
# Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi). # Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi).
# US-013: rar_creds_enc nu mai exista in accounts (dropata); nu e in legacy DDL.
conn.executescript( conn.executescript(
""" """
PRAGMA foreign_keys=OFF; PRAGMA foreign_keys=OFF;

View File

@@ -1,4 +1,4 @@
"""Teste US-001 (PRD 3.1): coloana accounts.active + helper-e cont in app/accounts.py.""" """Teste coloana accounts.active + helper-e cont in app/accounts.py."""
from __future__ import annotations from __future__ import annotations
@@ -119,7 +119,7 @@ def test_list_accounts_ordonat_fara_creds(conn):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# US-001 (PRD 5.12): accounts.email + validari companie/email/CUI # accounts.email + validari companie/email/CUI
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_create_account_fara_email_ridica(conn): def test_create_account_fara_email_ridica(conn):
@@ -176,7 +176,7 @@ def test_account_is_complete_false_pe_legacy_incomplet(conn):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 5.17 US-001/US-008: schema tier + trial_until + set_tier + CLI set-tier # schema tier + trial_until + set_tier + CLI set-tier
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_migrare_tier_trial_defensiva(conn): def test_migrare_tier_trial_defensiva(conn):

View File

@@ -1,4 +1,4 @@
"""Teste US-008 (PRD 5.5): rute admin pentru ciclul de viata al conturilor — """Rute admin pentru ciclul de viata al conturilor —
block/archive/delete + bulk pe lista account_id, require_admin + CSRF + PRG, dev id=1 protejat. block/archive/delete + bulk pe lista account_id, require_admin + CSRF + PRG, dev id=1 protejat.
""" """

View File

@@ -1,7 +1,4 @@
"""Teste US-011 (PRD 3.3b): panou admin web /admin — conturi in asteptare + activare. """Teste panou admin web /admin — conturi in asteptare + activare.
TDD strict: testele se scriu INAINTE de implementare; la inceput pica (RED),
dupa implementare trec (GREEN).
Fisiere testate: app/web/admin_routes.py, app/web/templates/admin.html. Fisiere testate: app/web/admin_routes.py, app/web/templates/admin.html.
""" """
@@ -175,7 +172,7 @@ def test_non_admin_403(client):
Creeaza intai un cont admin (bootstrap: primul user devine admin), Creeaza intai un cont admin (bootstrap: primul user devine admin),
apoi un al doilea cont (non-admin) si verifica ca al doilea primeste 403. apoi un al doilea cont (non-admin) si verifica ca al doilea primeste 403.
""" """
# Primul signup devine automat admin (bootstrap US-010) # Primul signup devine automat admin (bootstrap)
_signup(client, "Admin Bootstrap SA", "bootstrap@test.ro") _signup(client, "Admin Bootstrap SA", "bootstrap@test.ro")
# Al doilea user NU e admin # Al doilea user NU e admin
@@ -286,7 +283,7 @@ def test_set_tier_din_admin_incheie_trial(client):
"""POST /admin/set-tier -> tier actualizat, trial_until=NULL (trial incheiat), 303. """POST /admin/set-tier -> tier actualizat, trial_until=NULL (trial incheiat), 303.
Contul nou are trial Pro 30z; alocarea manuala trebuie sa-l incheie ca alegerea Contul nou are trial Pro 30z; alocarea manuala trebuie sa-l incheie ca alegerea
sa aiba efect imediat (decizie user 2026-06-29).""" sa aiba efect imediat."""
target_id = _signup(client, "Firma Upgrade SRL", "upgrade@test.ro") target_id = _signup(client, "Firma Upgrade SRL", "upgrade@test.ro")
tier0, trial0 = _get_tier_trial(target_id) tier0, trial0 = _get_tier_trial(target_id)
assert tier0 == "free", "cont nou trebuie sa porneasca pe free" assert tier0 == "free", "cont nou trebuie sa porneasca pe free"

View File

@@ -1,4 +1,4 @@
"""Teste US-010 (PRD 3.3b): rol admin + bootstrap + guard require_admin. """Teste rol admin + bootstrap + guard require_admin.
Fisiere testate: app/users.py (helper-e admin), app/web/session.py (require_admin), Fisiere testate: app/users.py (helper-e admin), app/web/session.py (require_admin),
tools/account.py (subcomanda set-admin). tools/account.py (subcomanda set-admin).

View File

@@ -1,4 +1,4 @@
"""Teste API /v1/prezentari — rutare validare (T3) + idempotenta.""" """Teste API /v1/prezentari — rutare validare + idempotenta."""
from __future__ import annotations from __future__ import annotations
@@ -63,7 +63,7 @@ def test_idempotenta_dedup(client):
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# PRD 5.7 — raspuns onest: erori/nemapate/motiv pe randuri blocate # # Raspuns onest: erori/nemapate/motiv pe randuri blocate #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def test_needs_data_intoarce_erori(client): def test_needs_data_intoarce_erori(client):

View File

@@ -1,4 +1,4 @@
"""Teste US-010 (PRD 5.6): API DELETE + re-pune in coada, scoped + oracol scope/stare.""" """Teste API DELETE + re-pune in coada, scoped + oracol scope/stare."""
from __future__ import annotations from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Teste US-005 — camp rar_env pe POST /v1/prezentari si /valideaza. """Teste camp rar_env pe POST /v1/prezentari si /valideaza.
Acopera: default din cont, tinta explicita, respingere tinta indisponibila, Acopera: default din cont, tinta explicita, respingere tinta indisponibila,
echo GET, valoare invalida (422 Pydantic), echo dry-run valideaza. echo GET, valoare invalida (422 Pydantic), echo dry-run valideaza.

View File

@@ -1,4 +1,4 @@
"""US-011 (PRD 5.15): account-scope pe GET-urile de listare API (securitate). """Teste account-scope pe GET-urile de listare API (securitate).
Verifica: Verifica:
- GET /v1/prezentari: un cont nu vede submissions ale altui cont - GET /v1/prezentari: un cont nu vede submissions ale altui cont
@@ -6,9 +6,8 @@ Verifica:
- Unauthenticated access cu require_api_key=True -> 401 - Unauthenticated access cu require_api_key=True -> 401
- Filtre si paginare nu sparg scope-ul - Filtre si paginare nu sparg scope-ul
Legatura cu implementare: resolve_account_id + account_scope_clause (mecanisme Foloseste resolve_account_id + account_scope_clause; verifica alinierea
existente, reutilizate). Testele LOCK DOWN comportamentul deja implementat cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
si verifica alinierea cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
""" """
from __future__ import annotations from __future__ import annotations
@@ -227,7 +226,7 @@ def test_get_listare_filtru_status_nu_sparge_scope(client):
# ============================================================================ # ============================================================================
# PRD 5.17 — enforcement planuri: gate API (T4) + volum (T3) + kill-switch (T5) # Enforcement planuri: gate API + volum + kill-switch
# ============================================================================ # ============================================================================
_PREZ_PLAN = { _PREZ_PLAN = {
@@ -279,7 +278,7 @@ def _insert_n_submissions(account_id: int, n: int) -> None:
def test_free_fara_api_403(client): def test_free_fara_api_403(client):
"""Cont free (non-default, cu cheie API) → 403 PLAN_FARA_API pe POST /v1/prezentari. """Cont free (non-default, cu cheie API) → 403 PLAN_FARA_API pe POST /v1/prezentari.
T4 PRD 5.17: gate API refuza conturi cu api_access=False in PLANS. Gate-ul API refuza conturi cu api_access=False in PLANS.
""" """
acct_id, key = _create_account_with_key("FreePlanGate") acct_id, key = _create_account_with_key("FreePlanGate")
_set_tier_acct(acct_id, "free", trial_until=None) _set_tier_acct(acct_id, "free", trial_until=None)
@@ -349,8 +348,8 @@ def test_trial_pro_api_ok(client):
def test_dry_run_valideaza_ramane_permis(client): def test_dry_run_valideaza_ramane_permis(client):
"""POST /v1/prezentari/valideaza (dry-run) e permis pe orice plan, inclusiv free. """POST /v1/prezentari/valideaza (dry-run) e permis pe orice plan, inclusiv free.
Decizie design (PRD 5.17): valideaza nu face enqueue si nu consuma cota, Valideaza nu face enqueue si nu consuma cota, deci NU e protejat de
deci NU e protejat de gate-ul API — integratorii pot testa fara upgrade. gate-ul API — integratorii pot testa fara upgrade.
""" """
acct_id, key = _create_account_with_key("FreeValideazaTest") acct_id, key = _create_account_with_key("FreeValideazaTest")
_set_tier_acct(acct_id, "free", trial_until=None) _set_tier_acct(acct_id, "free", trial_until=None)
@@ -374,8 +373,8 @@ def test_dry_run_valideaza_ramane_permis(client):
def test_free_peste_60_respins_api(client): def test_free_peste_60_respins_api(client):
"""Dev account (id=1) la 60/60 pe free → a 61-a prezentare respinsa 422 PLAN_LIMITA_LUNARA. """Dev account (id=1) la 60/60 pe free → a 61-a prezentare respinsa 422 PLAN_LIMITA_LUNARA.
Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API (T4), Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API,
dar NU la verificarea de volum (T3). La 60/60, urmatoarea cerere trebuie respinsa 422. dar NU la verificarea de volum. La 60/60, urmatoarea cerere trebuie respinsa 422.
""" """
_set_tier_acct(1, "free", trial_until=None) _set_tier_acct(1, "free", trial_until=None)
_insert_n_submissions(1, 60) _insert_n_submissions(1, 60)
@@ -392,7 +391,7 @@ def test_free_peste_60_respins_api(client):
def test_eroare_3_niveluri_plan_limita(client): def test_eroare_3_niveluri_plan_limita(client):
"""Eroarea PLAN_LIMITA_LUNARA contine toate 3 nivelurile: cod, problema, cauza, fix. """Eroarea PLAN_LIMITA_LUNARA contine toate 3 nivelurile: cod, problema, cauza, fix.
Pattern standard (PRD 5.17): eroare structurata pe 3 niveluri — nu 500, nu catch-all. Pattern standard de eroare structurata pe 3 niveluri — nu 500, nu catch-all.
""" """
_set_tier_acct(1, "free", trial_until=None) _set_tier_acct(1, "free", trial_until=None)
_insert_n_submissions(1, 60) _insert_n_submissions(1, 60)
@@ -512,7 +511,7 @@ def test_dev_id1_neblocat(client):
def test_flag_enforce_plans_false_sare_enforcement(client, monkeypatch): def test_flag_enforce_plans_false_sare_enforcement(client, monkeypatch):
"""Kill-switch AUTOPASS_ENFORCE_PLANS=false sare toate gate-urile de plan. """Kill-switch AUTOPASS_ENFORCE_PLANS=false sare toate gate-urile de plan.
T5 PRD 5.17: flag de operare pentru debugging sau rollback rapid fara revert de cod. Flag de operare pentru debugging sau rollback rapid fara revert de cod.
Chiar si cu free la 60/60, nu trebuie 422 cand flag-ul e oprit. Chiar si cu free la 60/60, nu trebuie 422 cand flag-ul e oprit.
""" """
from app.config import get_settings from app.config import get_settings

View File

@@ -1,4 +1,4 @@
"""Teste US-004 (PRD 5.6): audit cerere API per cont in jurnal.""" """Teste audit cerere API per cont in jurnal."""
from __future__ import annotations from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""US-001 (PRD 5.19): schema comutator Auto per cont + flag `held` pe submission. """Schema comutator Auto per cont + flag `held` pe submission.
Acopera: migrarea aditiva idempotenta (coloane + index partial pe DB vechi), helperii Acopera: migrarea aditiva idempotenta (coloane + index partial pe DB vechi), helperii
`get_auto_send`/`set_auto_send`/`held_for_account` din app/accounts.py si default-ul 0. `get_auto_send`/`set_auto_send`/`held_for_account` din app/accounts.py si default-ul 0.

View File

@@ -1,6 +1,5 @@
"""Teste US-007 (PRD 3.6) actualizate dupa US-002 (PRD 5.11). """Teste comutator auto_send: macro autosend_toggle neutralizat (intoarce string gol).
US-002: macro autosend_toggle neutralizat (intoarce string gol).
Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman. Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman.
Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman. Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman.
""" """
@@ -97,24 +96,24 @@ def _macro_html(checked: bool = True, form_id: str = "") -> str:
return str(mod.autosend_toggle(form_id=form_id, checked=checked)) return str(mod.autosend_toggle(form_id=form_id, checked=checked))
# --- markup: macro neutralizat dupa US-002 --- # --- markup: macro neutralizat ---
def test_comutator_coada_prezent(): def test_comutator_coada_prezent():
"""US-002: macro autosend_toggle neutralizat -> output gol (fara checkbox).""" """macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
html = _macro_html() html = _macro_html()
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI" assert 'name="auto_send"' not in html, "checkbox auto_send scos din UI"
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}" assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
def test_eticheta_scoped_pe_operatie(): def test_eticheta_scoped_pe_operatie():
"""US-002: macro neutralizat -> nicio eticheta scoped.""" """macro neutralizat -> nicio eticheta scoped."""
html = _macro_html() html = _macro_html()
assert "aceasta operatie" not in html assert "aceasta operatie" not in html
assert html.strip() == "" assert html.strip() == ""
def test_default_pune_automat(): def test_default_pune_automat():
"""US-002: macro neutralizat intoarce gol indiferent de parametrul checked.""" """macro neutralizat intoarce gol indiferent de parametrul checked."""
html_default = _macro_html(checked=True) html_default = _macro_html(checked=True)
assert html_default.strip() == "" assert html_default.strip() == ""
html_off = _macro_html(checked=False) html_off = _macro_html(checked=False)
@@ -154,7 +153,7 @@ def test_tine_pentru_verificare_mapeaza_auto_send_false(client):
# --- prezent in AMBELE locuri (mapari tab + panou preview) --- # --- prezent in AMBELE locuri (mapari tab + panou preview) ---
def test_comutator_in_tab_mapari(client): def test_comutator_in_tab_mapari(client):
"""US-002: tabul Mapari nu mai contine checkbox auto_send (macro neutralizat).""" """Tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
from app.db import get_connection from app.db import get_connection
import json import json
acct = _create_account_user("tm@test.com") acct = _create_account_user("tm@test.com")
@@ -172,12 +171,12 @@ def test_comutator_in_tab_mapari(client):
_login(client, "tm@test.com") _login(client, "tm@test.com")
resp = client.get("/?tab=mapari") resp = client.get("/?tab=mapari")
assert resp.status_code == 200 assert resp.status_code == 200
assert 'name="auto_send"' not in resp.text, "US-002: checkbox auto_send scos din UI" assert 'name="auto_send"' not in resp.text, "checkbox auto_send scos din UI"
assert "In coada" not in resp.text, "US-002: coloana 'In coada' scoasa" assert "In coada" not in resp.text, "coloana 'In coada' scoasa"
def test_comutator_in_panou_preview(client): def test_comutator_in_panou_preview(client):
"""US-002: panoul de mapare din preview nu mai contine checkbox auto_send.""" """Panoul de mapare din preview nu mai contine checkbox auto_send."""
_create_account_user("pp@test.com") _create_account_user("pp@test.com")
_seed_nomenclator("R-FRANE") _seed_nomenclator("R-FRANE")
_login(client, "pp@test.com") _login(client, "pp@test.com")
@@ -203,5 +202,5 @@ def test_comutator_in_panou_preview(client):
}) })
assert r.status_code == 200 assert r.status_code == 200
assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare" assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare"
assert 'name="auto_send"' not in r.text, "US-002: checkbox auto_send scos din preview" assert 'name="auto_send"' not in r.text, "checkbox auto_send scos din preview"
assert "In coada automat" not in r.text, "US-002: eticheta 'In coada automat' scoasa" assert "In coada automat" not in r.text, "eticheta 'In coada automat' scoasa"

View File

@@ -1,4 +1,4 @@
"""Teste US-010 (PRD 5.20): badge mediu RAR in liste/detaliu/audit/ecou API. """Teste badge mediu RAR in liste/detaliu/audit/ecou API.
Teste: Teste:
test_badge_in_lista -- /_fragments/submissions afiseaza PRODUCȚIE / Testare per rand test_badge_in_lista -- /_fragments/submissions afiseaza PRODUCȚIE / Testare per rand

View File

@@ -137,7 +137,7 @@ def test_legacy_dual_lookup_strategy():
assert found, "dual-lookup trebuie sa gaseasca randul vechi" assert found, "dual-lookup trebuie sa gaseasca randul vechi"
# --- Integrare: API route foloseste build_key (OV-2) --- # --- Integrare: API route foloseste build_key ---
@pytest.fixture() @pytest.fixture()
def client(monkeypatch): def client(monkeypatch):

View File

@@ -1,4 +1,4 @@
"""Teste PRD 5.19 R4 — tools/carantina_held.py (atenuare hazard de rollback). """Teste tools/carantina_held.py (atenuare hazard de rollback).
Carantineaza randurile `queued AND held=1` (-> `error`/`ROLLBACK_QUARANTINE`) Carantineaza randurile `queued AND held=1` (-> `error`/`ROLLBACK_QUARANTINE`)
ca sa nu fie trimise de un worker fara filtrul `AND held=0` dupa un revert. ca sa nu fie trimise de un worker fara filtrul `AND held=0` dupa un revert.
@@ -63,7 +63,7 @@ def test_carantineaza_rand_tinut(env, capsys):
r = _row(conn, sid) r = _row(conn, sid)
assert r["status"] == "error" assert r["status"] == "error"
assert r["rar_error"] == "ROLLBACK_QUARANTINE" assert r["rar_error"] == "ROLLBACK_QUARANTINE"
# /code-review #4: randul carantinat primeste purge_after (altfel PII ar sta la nesfarsit, # randul carantinat primeste purge_after (altfel PII ar sta la nesfarsit,
# purge_expired sterge doar randuri cu purge_after setat). # purge_expired sterge doar randuri cu purge_after setat).
assert r["purge_after"] is not None assert r["purge_after"] is not None
assert conn.execute( assert conn.execute(

View File

@@ -1,4 +1,4 @@
"""Teste US-008 (PRD 5.20): configurare medii RAR per cont — Testare + Productie. """Teste configurare medii RAR per cont — Testare + Productie.
Ruta testata: POST /cont/rar-medii Ruta testata: POST /cont/rar-medii

View File

@@ -47,12 +47,12 @@ def _body(**over):
def test_dashboard_renders_with_rar_state(client): def test_dashboard_renders_with_rar_state(client):
r = client.get("/") r = client.get("/")
assert r.status_code == 200 assert r.status_code == 200
# Dupa US-003 bara de status e incarcata via HTMX (hx-trigger=load, every 15s) # Bara de status e incarcata via HTMX (hx-trigger=load, every 15s)
assert "/_fragments/status" in r.text, "Dashboard-ul trebuie sa referenceze fragmentul de status" assert "/_fragments/status" in r.text, "Dashboard-ul trebuie sa referenceze fragmentul de status"
# Fragmentul de status contine starea de sanatate (text uman, nu brut tehnic) # Fragmentul de status contine starea de sanatate (text uman, nu brut tehnic)
rs = client.get("/_fragments/status") rs = client.get("/_fragments/status")
assert rs.status_code == 200 assert rs.status_code == 200
# US-003 D6: strip sanatate unificat — "declaratiile" apare in orice stare (curg/blocat) # strip sanatate unificat — "declaratiile" apare in orice stare (curg/blocat)
assert "declaratiile" in rs.text.lower(), ( assert "declaratiile" in rs.text.lower(), (
f"Strip sanatate lipseste din fragment. HTML: {rs.text[:500]}" f"Strip sanatate lipseste din fragment. HTML: {rs.text[:500]}"
) )
@@ -73,7 +73,7 @@ def test_nomenclator_fragment_lists_seed(client):
def test_submissions_fragment_empty_state(client): def test_submissions_fragment_empty_state(client):
r = client.get("/_fragments/submissions") r = client.get("/_fragments/submissions")
assert r.status_code == 200 assert r.status_code == 200
# US-005: empty state prietenos cu indemn la Import (nu mesajul tehnic vechi) # empty state prietenos cu indemn la Import (nu mesajul tehnic vechi)
assert "Nicio trimitere" in r.text or "incepe cu Import" in r.text or "?tab=import" in r.text assert "Nicio trimitere" in r.text or "incepe cu Import" in r.text or "?tab=import" in r.text

View File

@@ -1,7 +1,6 @@
"""Test US-011 (discoverability): linkul 'Panou admin' apare pe dashboard doar pentru admini. """Linkul 'Panou admin' apare pe dashboard doar pentru admini.
Completeaza intentia US-011 — adminul trebuie sa poata descoperi /admin din UI, nu doar Adminul trebuie sa poata descoperi /admin din UI, nu doar prin URL direct.
prin URL direct.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -1,6 +1,6 @@
"""Teste US-005 (PRD 3.3): scoping dashboard pe sesiune (2 conturi, citiri). """Teste scoping dashboard pe sesiune (2 conturi, citiri).
Comportamental (C6): nu grep, ci verificare reala cu 2 conturi + date distincte. Verificare comportamentala reala cu 2 conturi + date distincte, nu grep.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Teste US-012 (PRD 5.6): un rand `error` nu mai blocheaza retrimiterea (dedup). """Un rand `error` nu mai blocheaza retrimiterea (dedup).
La resubmit cu aceeasi cheie de continut peste un rand `error`: se RE-ACTIVEAZA La resubmit cu aceeasi cheie de continut peste un rand `error`: se RE-ACTIVEAZA
(reactivated:true + stare noua), creds-urile se actualizeaza. Peste sent/queued/ (reactivated:true + stare noua), creds-urile se actualizeaza. Peste sent/queued/
@@ -82,7 +82,7 @@ def test_resubmit_actualizeaza_creds_pe_reactivare(client):
enc_dupa = _creds_enc(sid) enc_dupa = _creds_enc(sid)
assert enc_dupa is not None assert enc_dupa is not None
assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare" assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare"
# US-013: creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test) # creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test)
from app.db import get_connection from app.db import get_connection
from app.crypto import decrypt_creds from app.crypto import decrypt_creds
conn = get_connection() conn = get_connection()
@@ -118,7 +118,7 @@ def test_resubmit_peste_queued_ramane_deduped(client):
def test_reactivare_pe_needs_data_expune_erori(client): def test_reactivare_pe_needs_data_expune_erori(client):
"""PRD 5.7: reactivarea unui rand error care re-clasifica needs_data trebuie sa """Reactivarea unui rand error care re-clasifica needs_data trebuie sa
expuna erori/motiv (raspuns onest), nu doar status + reactivated.""" expuna erori/motiv (raspuns onest), nu doar status + reactivated."""
# rand initial valid -> queued; il fortam error # rand initial valid -> queued; il fortam error
r1 = client.post("/v1/prezentari", json=_body()) r1 = client.post("/v1/prezentari", json=_body())

View File

@@ -1,6 +1,4 @@
"""Teste US-012 (PRD 5.15): Analytics device-mix — validare premisa mobil, fara PII. """Teste analytics device-mix — validare premisa mobil, fara PII.
TDD: RED inainte de implementare.
Semnal: la acces dashboard -> eveniment 'device_mix' in app_events cu cod 'desktop'/'mobil'. Semnal: la acces dashboard -> eveniment 'device_mix' in app_events cu cod 'desktop'/'mobil'.
Zero PII: nu se stocheaza UA brut, IP sau VIN. Zero PII: nu se stocheaza UA brut, IP sau VIN.

View File

@@ -1,6 +1,4 @@
"""Teste US-012 (PRD 5.20): audit evenimente medii RAR + e2e lant submission. """Teste audit evenimente medii RAR + e2e lant submission.
Fisier nou; nu modifica teste existente.
Teste: Teste:
test_lant_import_pana_la_queued -- submission ajunge la status=queued cu rar_env corect test_lant_import_pana_la_queued -- submission ajunge la status=queued cu rar_env corect

View File

@@ -1,7 +1,7 @@
"""US-005 (PRD 5.18) — embeddings indexeaza corpusul etichetat (NU nomenclatorul). """Embeddings indexeaza corpusul etichetat (NU nomenclatorul).
k-NN peste exemple reale etichetate (denumire_normalizata -> cod, is_nul) e net mai k-NN peste exemple reale etichetate (denumire_normalizata -> cod, is_nul) e net mai
precis decat peste cele 18 categorii generice. Acopera si simetria corpus/query (F1): precis decat peste cele 18 categorii generice. Acopera si simetria corpus/query:
corpusul e text NORMALIZAT, deci query-ul trebuie normalizat la fel inainte de embedding. corpusul e text NORMALIZAT, deci query-ul trebuie normalizat la fel inainte de embedding.
""" """
@@ -34,7 +34,7 @@ def env(monkeypatch):
tmp = tempfile.mkdtemp() tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us005.db")) monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us005.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false") monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # US-005 are nevoie de embeddings ON monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # necesar pentru testele de embeddings
from app.config import get_settings from app.config import get_settings
get_settings.cache_clear() get_settings.cache_clear()
from app.db import init_db from app.db import init_db
@@ -115,7 +115,7 @@ def test_semnatura_corpus_pe_seed(conn):
def test_query_normalizat_ca_si_corpusul(conn, monkeypatch): def test_query_normalizat_ca_si_corpusul(conn, monkeypatch):
"""F1 (HIGH): enrich_suggestions interogheaza suggest_nearest cu textul NORMALIZAT.""" """enrich_suggestions interogheaza suggest_nearest cu textul NORMALIZAT."""
import app.embeddings as emb import app.embeddings as emb
captura = {} captura = {}
monkeypatch.setattr(emb, "has_corpus", lambda: True) monkeypatch.setattr(emb, "has_corpus", lambda: True)

View File

@@ -1,8 +1,8 @@
"""US-006 (PRD 5.18) — enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat. """enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat.
Ordinea de precedenta: pre-filtru NUL -> (daca NUL: fara cod) altfel GOLD partajat > Ordinea de precedenta: pre-filtru NUL -> (daca NUL: fara cod) altfel GOLD partajat >
exact (SILVER) > k-NN embeddings. k-NN sub prag -> abtinere. Vecin k-NN NUL -> supresie. exact (SILVER) > k-NN embeddings. k-NN sub prag -> abtinere. Vecin k-NN NUL -> supresie.
Invariant #13: nimic din asta nu intra in resolve_prestatii/load_mapping. Nimic din enrich_suggestions nu intra in resolve_prestatii/load_mapping.
""" """
from __future__ import annotations from __future__ import annotations
@@ -123,7 +123,7 @@ def test_vecin_knn_nul_supreseaza(conn, monkeypatch):
def test_invariant_13_resolve_neatins(conn): def test_invariant_13_resolve_neatins(conn):
"""Regresie #13: SILVER populat NU produce auto-rezolvare in resolve_prestatii.""" """SILVER populat NU produce auto-rezolvare in resolve_prestatii."""
from app.mapping import resolve_prestatii, normalize_for_match from app.mapping import resolve_prestatii, normalize_for_match
_silver(conn, normalize_for_match("OPERATIE X"), "OE-1") _silver(conn, normalize_for_match("OPERATIE X"), "OE-1")
resolved, unmapped = resolve_prestatii( resolved, unmapped = resolve_prestatii(

View File

@@ -1,4 +1,4 @@
"""Teste US-001 (PRD 5.6): handler global de excepții -> 500 structurat (3 niveluri) + log.""" """Teste handler global de exceptii -> 500 structurat (3 niveluri) + log."""
from __future__ import annotations from __future__ import annotations
@@ -39,7 +39,7 @@ def test_exceptie_neasteptata_da_500_structurat(client, monkeypatch):
assert r.status_code == 500 assert r.status_code == 500
body = r.json() body = r.json()
assert body["cod"] == "EROARE_INTERNA" assert body["cod"] == "EROARE_INTERNA"
# 3 niveluri (PRD 5.4): problema + fix # 3 niveluri: cod + problema + fix
assert body["problema"] assert body["problema"]
assert body["fix"] assert body["fix"]
assert "request_id" in body assert "request_id" in body

View File

@@ -1,7 +1,4 @@
"""Teste pentru app/errors.py — catalog central de erori (US-001 / PRD 5.4). """Teste pentru app/errors.py — catalog central de erori."""
Urmeaza TDD: testele sunt scrise INAINTE de implementare.
"""
from __future__ import annotations from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""US-002 (PRD 5.18) — etichetator offline multi-backend cu prompt procedural. """Etichetator offline multi-backend cu prompt procedural.
Toate testele ruleaza FARA retea reala (transport injectabil / inspectie body). Toate testele ruleaza FARA retea reala (transport injectabil / inspectie body).
Acopera: prompt 3 pasi, envelope json_schema strict + enum, backend selectabil Acopera: prompt 3 pasi, envelope json_schema strict + enum, backend selectabil
@@ -70,7 +70,7 @@ def test_parseaza_raspuns_si_garda_truncare():
def test_backend_selectabil_env(monkeypatch): def test_backend_selectabil_env(monkeypatch):
# Default = lmstudio (backend aprobat v1, D4). # Default = lmstudio.
monkeypatch.delenv("ETICHETARE_BACKEND", raising=False) monkeypatch.delenv("ETICHETARE_BACKEND", raising=False)
assert eticheteaza.get_backend().name == "lmstudio" assert eticheteaza.get_backend().name == "lmstudio"
# Selectie prin env. # Selectie prin env.

View File

@@ -1,4 +1,4 @@
"""Teste US-006 (PRD 3.6): "Formate de coloane salvate" randate ca TABEL. """Teste "Formate de coloane salvate" randate ca TABEL.
Doar marcaj de template — POST-urile /formate-coloane/editeaza si /formate-coloane/sterge Doar marcaj de template — POST-urile /formate-coloane/editeaza si /formate-coloane/sterge
raman identice. .tablewrap pentru consistenta cu celelalte tabele. raman identice. .tablewrap pentru consistenta cu celelalte tabele.

View File

@@ -1,4 +1,4 @@
"""Teste FOUNDATION (Task #1): schema + migrari idempotente + openpyxl disponibil.""" """Teste schema + migrari idempotente + openpyxl disponibil."""
from __future__ import annotations from __future__ import annotations
@@ -35,11 +35,11 @@ def _tables(conn) -> set[str]:
# --- Coloane noi --- # --- Coloane noi ---
def test_accounts_rar_creds_enc_dropata(db_conn): def test_accounts_rar_creds_enc_dropata(db_conn):
"""US-013: accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane.""" """accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
acc_cols = _table_cols(db_conn, "accounts") acc_cols = _table_cols(db_conn, "accounts")
sub_cols = _table_cols(db_conn, "submissions") sub_cols = _table_cols(db_conn, "submissions")
assert "rar_creds_enc" not in acc_cols, \ assert "rar_creds_enc" not in acc_cols, \
"accounts.rar_creds_enc trebuie sa fie ABSENTA dupa US-013 DROP" "accounts.rar_creds_enc trebuie sa fie ABSENTA"
assert "rar_creds_enc" in sub_cols, \ assert "rar_creds_enc" in sub_cols, \
"submissions.rar_creds_enc trebuie sa RAMANA (creds efemere per-cerere)" "submissions.rar_creds_enc trebuie sa RAMANA (creds efemere per-cerere)"
@@ -158,7 +158,7 @@ def test_migrate_on_existing_db(monkeypatch):
assert "batch_id" in sub_cols assert "batch_id" in sub_cols
assert "row_index" in sub_cols assert "row_index" in sub_cols
assert "purge_after" in sub_cols assert "purge_after" in sub_cols
# US-013: accounts.rar_creds_enc a fost dropata. # accounts.rar_creds_enc a fost dropata.
assert "rar_creds_enc" not in acc_cols assert "rar_creds_enc" not in acc_cols
conn.close() conn.close()
get_settings.cache_clear() get_settings.cache_clear()

View File

@@ -1,10 +1,10 @@
"""US-003 (PRD 5.18) — generare seed etichetat in faze pe frecventa. """Generare seed etichetat in faze pe frecventa.
Pipeline dedup OBLIGATORIU inainte de orice apel LLM (D5): Pipeline dedup OBLIGATORIU inainte de orice apel LLM:
brut -> normalize_for_match -> arunca chei vide -> dedup pe cheie (freq=suma NR) brut -> normalize_for_match -> arunca chei vide -> dedup pe cheie (freq=suma NR)
-> reuse etichete existente (labels-groq + seed comis, conflict freq-max) -> de_etichetat. -> reuse etichete existente (labels-groq + seed comis, conflict freq-max) -> de_etichetat.
Idempotenta cross-run (F2/F7): a doua rulare consuma seedul comis ca cache -> 0 apeluri LLM. Idempotenta cross-run: a doua rulare consuma seedul comis ca cache -> 0 apeluri LLM.
Toate testele FARA retea: `clasifica` e injectat (mock care inregistreaza ce primeste). Toate testele FARA retea: `clasifica` e injectat (mock care inregistreaza ce primeste).
""" """
@@ -134,7 +134,7 @@ def test_zero_duplicate_trimis_la_llm(tmp_path):
def test_rerun_zero_apeluri_llm(tmp_path): def test_rerun_zero_apeluri_llm(tmp_path):
"""Criteriul real de idempotenta (F2/F7): a doua rulare = 0 apeluri LLM, seed identic.""" """Criteriul real de idempotenta: a doua rulare = 0 apeluri LLM, seed identic."""
f = tmp_path / "a.csv" f = tmp_path / "a.csv"
_scrie_csv(f, [("OP UNU", 10), ("OP DOI", 5)]) _scrie_csv(f, [("OP UNU", 10), ("OP DOI", 5)])
seed = tmp_path / "seed.json" seed = tmp_path / "seed.json"

View File

@@ -1,4 +1,4 @@
"""Teste scope cont pe GET /v1/audit/export (US-003, PRD 3.2).""" """Teste scope cont pe GET /v1/audit/export."""
from __future__ import annotations from __future__ import annotations
@@ -51,7 +51,7 @@ def test_export_doar_contul_cheii(env):
from app.db import get_connection from app.db import get_connection
conn = get_connection() conn = get_connection()
try: try:
# tier='pro' ca sa treaca gate-ul API (T4 PRD 5.17); testul masoara scoping, nu planuri. # tier='pro' ca sa treaca gate-ul API; testul masoara scoping, nu planuri.
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')") conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
k1 = create_api_key(conn, 1) k1 = create_api_key(conn, 1)
k2 = create_api_key(conn, 2) k2 = create_api_key(conn, 2)

View File

@@ -1,6 +1,6 @@
"""Teste scope cont pe GET /v1/mapari + /pending (US-002, PRD 3.2). """Teste scope cont pe GET /v1/mapari + /pending.
TD-3.2 (decis la poarta): parametrul ?account_id= din query se pastreaza DAR: Parametrul ?account_id= din query se pastreaza DAR:
- daca e prezent SI difera de contul cheii -> 400 explicit - daca e prezent SI difera de contul cheii -> 400 explicit
- daca e prezent si egal -> ok - daca e prezent si egal -> ok
- daca lipseste -> contul cheii - daca lipseste -> contul cheii
@@ -69,7 +69,7 @@ def test_mapari_doar_contul_cheii(env):
def test_mapari_query_account_id_diferit_400(env): def test_mapari_query_account_id_diferit_400(env):
"""Daca ?account_id difera de contul cheii -> 400 explicit (TD-3.2).""" """Daca ?account_id difera de contul cheii -> 400 explicit."""
with _client() as c: with _client() as c:
from app.auth import create_api_key from app.auth import create_api_key
from app.db import get_connection from app.db import get_connection
@@ -85,7 +85,7 @@ def test_mapari_query_account_id_diferit_400(env):
def test_mapari_query_account_id_egal_ok(env): def test_mapari_query_account_id_egal_ok(env):
"""Daca ?account_id egal cu contul cheii -> 200 (TD-3.2).""" """Daca ?account_id egal cu contul cheii -> 200."""
with _client() as c: with _client() as c:
from app.auth import create_api_key from app.auth import create_api_key
from app.db import get_connection from app.db import get_connection
@@ -161,7 +161,7 @@ def test_pending_web_global_neschimbat(env):
def test_pending_filtreaza_in_sql_cu_regula_null(env): def test_pending_filtreaza_in_sql_cu_regula_null(env):
"""B1: pending_unmapped(conn, account_id=1) include si randuri cu account_id=NULL (legacy). """pending_unmapped(conn, account_id=1) include si randuri cu account_id=NULL (legacy).
Filtrarea trebuie sa se faca IN SQL cu: Filtrarea trebuie sa se faca IN SQL cu:
WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1)) WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1))

View File

@@ -1,8 +1,4 @@
"""Teste scope cont pe GET /v1/prezentari + /{id} (US-001, PRD 3.2). """Teste scope cont pe GET /v1/prezentari + /{id}."""
Metoda TDD: testele se scriu inainte de implementare (RED) si trebuie sa ramana
verzi dupa implementare (GREEN).
"""
from __future__ import annotations from __future__ import annotations
@@ -47,7 +43,7 @@ def test_lista_doar_contul_cheii(env):
from app.db import get_connection from app.db import get_connection
conn = get_connection() conn = get_connection()
try: try:
# tier='pro' pe ambele conturi — testul verifica scoping GET, nu planuri (T4 PRD 5.17). # tier='pro' pe ambele conturi — testul verifica scoping GET, nu planuri.
conn.execute("UPDATE accounts SET tier='pro' WHERE id=1") conn.execute("UPDATE accounts SET tier='pro' WHERE id=1")
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')") conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
k1 = create_api_key(conn, 1) k1 = create_api_key(conn, 1)
@@ -77,7 +73,7 @@ def test_lista_doar_contul_cheii(env):
def test_detaliu_cross_account_404(env): def test_detaliu_cross_account_404(env):
"""GET /{id} cu cheia contului B pentru submission-ul contului A -> 404. """GET /{id} cu cheia contului B pentru submission-ul contului A -> 404.
B3: detail-ul 404 cross-account trebuie byte-identic cu cel al unui id inexistent Raspunsul 404 cross-account trebuie byte-identic cu cel al unui id inexistent
(acelasi status + acelasi mesaj) — nu dam indicii ca randul exista. (acelasi status + acelasi mesaj) — nu dam indicii ca randul exista.
""" """
with _client() as c: with _client() as c:
@@ -103,7 +99,7 @@ def test_detaliu_cross_account_404(env):
def test_legacy_null_vizibil_pentru_cont_1(env): def test_legacy_null_vizibil_pentru_cont_1(env):
"""Randuri cu account_id=NULL apartin contului 1 (legacy OV-2); contul 2 nu le vede.""" """Randuri cu account_id=NULL apartin contului 1 (legacy); contul 2 nu le vede."""
with _client() as c: with _client() as c:
from app.auth import create_api_key from app.auth import create_api_key
from app.db import get_connection from app.db import get_connection
@@ -149,11 +145,11 @@ def test_fara_cheie_flag_off_vede_contul_1(env):
def test_detaliu_nu_expune_creds(env): def test_detaliu_nu_expune_creds(env):
"""B4: GET /v1/prezentari/{id} nu expune campuri sensibile (rar_creds_enc, payload_json, """GET /v1/prezentari/{id} nu expune campuri sensibile (rar_creds_enc, payload_json,
idempotency_key). idempotency_key).
NOTA T9 (PRD 5.6): `rar_error` e ACUM expus intentionat (recovery API observabil) — `rar_error` e expus intentionat (recovery API observabil) — contine doar
contine doar coduri/mesaje de validare RAR, niciodata creds. coduri/mesaje de validare RAR, niciodata creds.
""" """
with _client() as c: with _client() as c:
from app.auth import create_api_key from app.auth import create_api_key

View File

@@ -1,4 +1,4 @@
"""Teste PRD 5.19 US-010 — onestitate + observabilitate `held` pe canalul API. """Onestitate + observabilitate `held` pe canalul API.
- `held` in raspunsul enqueue (SubmissionResult) + motiv non-null. - `held` in raspunsul enqueue (SubmissionResult) + motiv non-null.
- `held` in proiectiile GET /v1/prezentari si /v1/prezentari/{id}. - `held` in proiectiile GET /v1/prezentari si /v1/prezentari/{id}.

View File

@@ -1,6 +1,6 @@
"""Teste PRD 5.19 US-002 — snapshot `held` la ingestie din comutatorul de cont. """Teste snapshot `held` la ingestie din comutatorul de cont.
Acopera TOATE situ-rile care scriu `status='queued'` prin ingestie (chokepoint Acopera TOATE situatiile care scriu `status='queued'` prin ingestie (chokepoint
`held_for_account`): enqueue API, reactivare error->queued, dedup echo, commit import, `held_for_account`): enqueue API, reactivare error->queued, dedup echo, commit import,
reresolve `needs_mapping`. `held` NU intra in idempotency_key. reresolve `needs_mapping`. `held` NU intra in idempotency_key.
@@ -107,7 +107,7 @@ def test_auto_on_ingestie_not_held(client):
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Reactivare error->queued (bug de bypass Eng Finding A) # # Reactivare error->queued #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def test_reactivare_respecta_auto_off(client): def test_reactivare_respecta_auto_off(client):

View File

@@ -1,9 +1,9 @@
"""Regresie PRD 5.19 US-002 — re-snapshot `held` pe caile de re-punere in coada. """Regresie — re-snapshot `held` pe caile de re-punere in coada.
Bug (gasit la /code-review high): create_prezentari (reactivare) si reresolve_account Bug: create_prezentari (reactivare) si reresolve_account re-calculau `held` din
re-calculau `held` din comutatorul contului la tranzitia -> queued, dar caile de comutatorul contului la tranzitia -> queued, dar caile de re-punere din
re-punere din dashboard/admin (requeue_submission, corectie, repune-cu-cod, bulk-fix) dashboard/admin (requeue_submission, corectie, repune-cu-cod, bulk-fix) lasau
lasau `held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a `held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a
esuat, apoi contul trecut pe Auto OFF, la re-punere pastra held=0 -> worker-ul il esuat, apoi contul trecut pe Auto OFF, la re-punere pastra held=0 -> worker-ul il
auto-trimitea la RAR (FINALIZATA ireversibil) desi contul e Auto OFF. auto-trimitea la RAR (FINALIZATA ireversibil) desi contul e Auto OFF.

View File

@@ -1,4 +1,4 @@
"""Teste TDD pentru tools/mapare-llm/heldout_eval.py (L14-S5). """Teste pentru tools/mapare-llm/heldout_eval.py.
Fixture sintetic cu predictii+ground_truth cunoscute. Verifica: Fixture sintetic cu predictii+ground_truth cunoscute. Verifica:
- precizie globala - precizie globala
@@ -398,8 +398,7 @@ class TestExportForLabeling:
for row in reader: for row in reader:
# Coloana cod_gold trebuie sa fie vida (nu etichetata de cod!) # Coloana cod_gold trebuie sa fie vida (nu etichetata de cod!)
assert row["cod_gold"] == "", ( assert row["cod_gold"] == "", (
"cod_gold nu trebuie pre-completat: ar fi 'antrenare pe test' " "cod_gold nu trebuie pre-completat: ar fi 'antrenare pe test'"
"(Decision #19 PRD 5.14)"
) )
def test_n_randuri_egal_cu_sample(self, tmp_path): def test_n_randuri_egal_cu_sample(self, tmp_path):
@@ -413,7 +412,7 @@ class TestExportForLabeling:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Sectiunea 8: kill_criterion — pragul de acceptanta (F-E, PRD 5.14) # Sectiunea 8: kill_criterion — pragul de acceptanta
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestKillCriterion: class TestKillCriterion:
@@ -424,8 +423,8 @@ class TestKillCriterion:
Justificare threshold 0.5% (0.005): Justificare threshold 0.5% (0.005):
Un service cu 200 operatii/zi auto-rezolvate = 1 FINALIZATA gresita/zi. Un service cu 200 operatii/zi auto-rezolvate = 1 FINALIZATA gresita/zi.
FINALIZATA e ireversibila (cf. PRD 5.14 Premisa 3 / invariant CLAUDE.md). FINALIZATA e ireversibila.
Pragul poate fi RELAXAT empiric; nu INASPRIT post-hoc (sesizare-in-timp). Pragul poate fi RELAXAT empiric; nu INASPRIT post-hoc.
""" """
def test_trece_cand_sub_prag(self): def test_trece_cand_sub_prag(self):

View File

@@ -1,4 +1,4 @@
"""US-003 (PRD 5.20): build_key incorporeaza rar_env.""" """build_key incorporeaza rar_env."""
from __future__ import annotations from __future__ import annotations

View File

@@ -1,10 +1,10 @@
"""Teste API import Treapta 2 — POST /v1/import, preview, commit, export-failed. """Teste API import — POST /v1/import, preview, commit, export-failed.
Acopera: Acopera:
- #11 U1+T4: upload + staging + mapare coloane semnatura/drift/fuzzy - upload + staging + mapare coloane semnatura/drift/fuzzy
- #12 T2+T11: preview 6 stari + already_sent batch lookup + intra-batch collision - preview 6 stari + already_sent batch lookup + intra-batch collision
- #13 T5+T12: gate HARD confirmare + atestare valori + commit ON CONFLICT (TOCTOU) - gate HARD confirmare + atestare valori + commit ON CONFLICT (TOCTOU)
- #14 T8: export randuri esuate CSV - export randuri esuate CSV
""" """
from __future__ import annotations from __future__ import annotations
@@ -91,15 +91,11 @@ def _default_column_mapping() -> dict:
def _setup_nomenclator(client: TestClient) -> None: def _setup_nomenclator(client: TestClient) -> None:
"""Seed nomenclator cu un cod de prestatie pentru teste.""" """Seed nomenclator cu un cod de prestatie pentru teste."""
# Folosim POST /v1/prezentari pentru a forta seed-ul nomenclatorului
# care are loc in init_db -> seed_nomenclator_if_empty
pass # seed-ul se face automat in init_db pass # seed-ul se face automat in init_db
def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None: def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None:
"""Salveaza o mapare de operatii pentru teste.""" """Salveaza o mapare de operatii pentru teste."""
# Adauga mai intai in nomenclator daca nu exista (prin POST prezentare care creeaza cod)
# De fapt, cod OE-1 e in nomenclatorul seed
client.post("/v1/mapari", json={ client.post("/v1/mapari", json={
"cod_op_service": cod_op, "cod_op_service": cod_op,
"cod_prestatie": cod_prest, "cod_prestatie": cod_prest,
@@ -108,7 +104,7 @@ def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_pre
# =========================================================================== # # =========================================================================== #
# #11 — Upload + staging (U1+T4) # # Upload + staging #
# =========================================================================== # # =========================================================================== #
class TestUploadStaging: class TestUploadStaging:
@@ -144,7 +140,7 @@ class TestUploadStaging:
assert r.status_code == 422 assert r.status_code == 422
def test_issue5a_raw_json_criptat(self, client): def test_issue5a_raw_json_criptat(self, client):
"""Issue 5a: raw_json din import_rows trebuie sa fie criptat (ciphertext la rest).""" """raw_json din import_rows trebuie sa fie criptat (ciphertext la rest)."""
data = _make_xlsx([_HEADER, _ROW_OK]) data = _make_xlsx([_HEADER, _ROW_OK])
r = _upload_file(client, data, "test.xlsx") r = _upload_file(client, data, "test.xlsx")
assert r.status_code == 200 assert r.status_code == 200
@@ -173,7 +169,7 @@ class TestUploadStaging:
conn.close() conn.close()
def test_issue5b_fuzzy_coloane_refoloseste_normalize_for_match(self, client): def test_issue5b_fuzzy_coloane_refoloseste_normalize_for_match(self, client):
"""Issue 5b: fuzzy_suggestions din raspuns foloseste normalize_for_match (fara duplicat).""" """fuzzy_suggestions din raspuns foloseste normalize_for_match (fara duplicat)."""
data = _make_xlsx([_HEADER, _ROW_OK]) data = _make_xlsx([_HEADER, _ROW_OK])
r = _upload_file(client, data, "test.xlsx") r = _upload_file(client, data, "test.xlsx")
assert r.status_code == 200 assert r.status_code == 200
@@ -190,7 +186,7 @@ class TestUploadStaging:
assert "odometru_final" in camps, f"'odometru_final' trebuie in sugestii" assert "odometru_final" in camps, f"'odometru_final' trebuie in sugestii"
def test_drift_semnatura_coloane(self, client): def test_drift_semnatura_coloane(self, client):
"""T4/D3: upload 2 cu coloane mutate -> mapping_status='new' (nu aplica orb).""" """Upload 2 cu coloane mutate -> mapping_status='new' (nu aplica orb)."""
# Upload 1 cu header standard # Upload 1 cu header standard
data1 = _make_xlsx([_HEADER, _ROW_OK]) data1 = _make_xlsx([_HEADER, _ROW_OK])
r1 = _upload_file(client, data1, "test.xlsx") r1 = _upload_file(client, data1, "test.xlsx")
@@ -275,7 +271,7 @@ class TestUploadStaging:
assert body["sample_rows"][0]["VIN"] == _ROW_OK2[0] assert body["sample_rows"][0]["VIN"] == _ROW_OK2[0]
def test_purge_after_setat_la_insert(self, client): def test_purge_after_setat_la_insert(self, client):
"""T16: purge_after trebuie setat la insert import_batches.""" """purge_after trebuie setat la insert import_batches."""
data = _make_xlsx([_HEADER, _ROW_OK]) data = _make_xlsx([_HEADER, _ROW_OK])
r = _upload_file(client, data, "test.xlsx") r = _upload_file(client, data, "test.xlsx")
import_id = r.json()["import_id"] import_id = r.json()["import_id"]
@@ -294,7 +290,7 @@ class TestUploadStaging:
# =========================================================================== # # =========================================================================== #
# #11 — Mapare coloane (T4) # # Mapare coloane #
# =========================================================================== # # =========================================================================== #
class TestColumnMapping: class TestColumnMapping:
@@ -349,7 +345,7 @@ class TestColumnMapping:
# =========================================================================== # # =========================================================================== #
# #12 — Preview 6 stari (T2 + T11) # # Preview 6 stari #
# =========================================================================== # # =========================================================================== #
class TestPreview: class TestPreview:
@@ -405,7 +401,7 @@ class TestPreview:
assert any(r["resolved_status"] in ("needs_data",) for r in body["rows"]) assert any(r["resolved_status"] in ("needs_data",) for r in body["rows"])
def test_preview_already_sent_dupa_submit(self, client): def test_preview_already_sent_dupa_submit(self, client):
"""Rand deja trimis prin API -> stare already_sent la preview (T2/D5).""" """Rand deja trimis prin API -> stare already_sent la preview."""
# Trimite prin API canalul standard # Trimite prin API canalul standard
client.post("/v1/prezentari", json={ client.post("/v1/prezentari", json={
"rar_credentials": {"email": "x@y.ro", "password": "s"}, "rar_credentials": {"email": "x@y.ro", "password": "s"},
@@ -430,7 +426,7 @@ class TestPreview:
assert "already_sent" in statuses, f"Asteptat 'already_sent', primit: {statuses}" assert "already_sent" in statuses, f"Asteptat 'already_sent', primit: {statuses}"
def test_preview_duplicate_in_file(self, client): def test_preview_duplicate_in_file(self, client):
"""T11/OV-3: 2 randuri identice in ACELASI fisier -> duplicate_in_file.""" """2 randuri identice in ACELASI fisier -> duplicate_in_file."""
# Acelasi rand de doua ori # Acelasi rand de doua ori
rows = [_HEADER, _ROW_OK, _ROW_OK] # duplicat exact rows = [_HEADER, _ROW_OK, _ROW_OK] # duplicat exact
import_id = self._upload_and_map(client, rows=rows) import_id = self._upload_and_map(client, rows=rows)
@@ -444,7 +440,7 @@ class TestPreview:
f"Asteptat 'duplicate_in_file', primit: {statuses}" f"Asteptat 'duplicate_in_file', primit: {statuses}"
def test_preview_already_sent_batch_lookup_nu_n_plus_1(self, client): def test_preview_already_sent_batch_lookup_nu_n_plus_1(self, client):
"""Eng#5: already_sent lookup BATCH (nu N+1) — ≤7 interogari pentru 5 randuri.""" """already_sent lookup BATCH (nu N+1) — ≤7 interogari pentru 5 randuri."""
# Cream 5 randuri distincte # Cream 5 randuri distincte
rows_data = [_HEADER] rows_data = [_HEADER]
for i in range(5): for i in range(5):
@@ -492,7 +488,7 @@ class TestPreview:
# =========================================================================== # # =========================================================================== #
# #13 — Commit gate HARD + atestare + TOCTOU (T5 + T12) # # Commit gate HARD + atestare + TOCTOU #
# =========================================================================== # # =========================================================================== #
class TestCommit: class TestCommit:
@@ -528,7 +524,7 @@ class TestCommit:
assert body["enqueued"] == n_ok assert body["enqueued"] == n_ok
def test_commit_cu_n_gresit_reject(self, client): def test_commit_cu_n_gresit_reject(self, client):
"""T5/D3: commit cu N gresit -> 422, nu enqueue.""" """Commit cu N gresit -> 422, nu enqueue."""
import_id, preview = self._upload_preview_ok(client) import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0) n_ok = preview["summary"].get("ok", 0)
@@ -543,7 +539,7 @@ class TestCommit:
f"Eroare neasteptata: {detail}" f"Eroare neasteptata: {detail}"
def test_commit_log_atestare(self, client): def test_commit_log_atestare(self, client):
"""T12/Voce#9: commit scrie import_attestations cu rows_hash + n_confirmed.""" """Commit scrie import_attestations cu rows_hash + n_confirmed."""
import_id, preview = self._upload_preview_ok(client) import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0) n_ok = preview["summary"].get("ok", 0)
@@ -574,7 +570,7 @@ class TestCommit:
conn.close() conn.close()
def test_commit_batch_id_setat_pe_submission(self, client): def test_commit_batch_id_setat_pe_submission(self, client):
"""T7: submission creata la commit trebuie sa aiba batch_id + row_index setate.""" """Submission creata la commit trebuie sa aiba batch_id + row_index setate."""
import_id, preview = self._upload_preview_ok(client) import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0) n_ok = preview["summary"].get("ok", 0)
@@ -602,7 +598,7 @@ class TestCommit:
conn.close() conn.close()
def test_commit_toctou_cheie_inserata_concurent(self, client): def test_commit_toctou_cheie_inserata_concurent(self, client):
"""Issue 1 (TOCTOU): cheie inserata de canal concurent -> reclasificata already_sent. """Cheie inserata de canal concurent (TOCTOU) -> reclasificata already_sent.
Simulam TOCTOU inserand cheia direct in submissions inainte de commit. Simulam TOCTOU inserand cheia direct in submissions inainte de commit.
""" """
@@ -635,7 +631,6 @@ class TestCommit:
"reviewed_rows": [], "reviewed_rows": [],
}) })
# Poate fi 200 cu toctou_collisions sau 422 cu informatii clare # Poate fi 200 cu toctou_collisions sau 422 cu informatii clare
# Conform planului Issue 1: reclasificat already_sent, nu rollback
# Dar n_enqueued va fi 0 daca toate colideaza # Dar n_enqueued va fi 0 daca toate colideaza
if rc.status_code == 200: if rc.status_code == 200:
body = rc.json() body = rc.json()
@@ -644,7 +639,7 @@ class TestCommit:
# (dupa reclasificare, n_total_ok scade) # (dupa reclasificare, n_total_ok scade)
def test_commit_needs_review_nebifat_exclus_din_n(self, client): def test_commit_needs_review_nebifat_exclus_din_n(self, client):
"""Voce#1: rand needs_review nebifat explicit -> NU intra in N, NU se enqueued.""" """Rand needs_review nebifat explicit -> NU intra in N, NU se enqueued."""
# Rand cu VIN numeric (coercion -> needs_review) # Rand cu VIN numeric (coercion -> needs_review)
import datetime as dt import datetime as dt
wb = openpyxl.Workbook() wb = openpyxl.Workbook()
@@ -704,7 +699,7 @@ class TestCommit:
assert r2.status_code == 409 assert r2.status_code == 409
def test_atestare_purge_after_setat_pe_submission(self, client): def test_atestare_purge_after_setat_pe_submission(self, client):
"""T16: submissions create la commit trebuie sa aiba purge_after setat.""" """Submissions create la commit trebuie sa aiba purge_after setat."""
import_id, preview = self._upload_preview_ok(client) import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0) n_ok = preview["summary"].get("ok", 0)
@@ -732,7 +727,7 @@ class TestCommit:
# =========================================================================== # # =========================================================================== #
# #14 — Export randuri esuate CSV (T8) # # Export randuri esuate CSV #
# =========================================================================== # # =========================================================================== #
class TestExportFailed: class TestExportFailed:
@@ -806,12 +801,12 @@ class TestExportFailed:
# =========================================================================== # # =========================================================================== #
# Regresie: reconcile.py ramane op-blind (OV-3) # # Regresie: reconcile.py ramane op-blind #
# =========================================================================== # # =========================================================================== #
class TestReconcileRegresie: class TestReconcileRegresie:
def test_match_finalizata_ramane_op_blind(self): def test_match_finalizata_ramane_op_blind(self):
"""OV-3: reconcile.py trebuie sa ramana op-blind (nu editat de import). """reconcile.py trebuie sa ramana op-blind (nu editat de import).
Importam reconcile si verificam ca nu s-au adaugat parametri de operatie. Importam reconcile si verificam ca nu s-au adaugat parametri de operatie.
""" """

View File

@@ -1,6 +1,5 @@
"""Teste US-007 (PRD 5.11) — Post-commit: lista Trimiteri apare + refresh auto. """Teste post-commit: lista Trimiteri apare + refresh auto.
TDD RED: testele sunt scrise inainte de implementare.
Verifica: Verifica:
1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged. 1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged.
2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap. 2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap.
@@ -130,7 +129,7 @@ _ROWS_OK = [
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Teste RED → GREEN # # Teste #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def test_confirma_emite_hx_trigger(client): def test_confirma_emite_hx_trigger(client):

View File

@@ -1,16 +1,16 @@
"""Teste E2E T15 — integrare import->commit->worker (RAR mock) + masina de stari + failure registry. """Teste E2E: integrare import->commit->worker (RAR mock) + masina de stari + failure registry.
Acopera (plan T15, sect.12): Acopera:
- Scenariul 1: upload xlsx/csv -> column-mapping -> preview -> commit N - Scenariul 1: upload xlsx/csv -> column-mapping -> preview -> commit N
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare. -> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent - Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
(NU al doilea FINALIZATA dupa commit). (NU al doilea FINALIZATA dupa commit).
- Scenariul 3: coada MIXTA API(creds efemere)+web(creds durabile per-env, US-013): - Scenariul 3: coada mixta API(creds efemere)+web(creds durabile per-env):
dupa login + purjare creds efemere, submission-urile web tot se trimit dupa login + purjare creds efemere, submission-urile web tot se trimit
prin fallback accounts.rar_creds_{env}_enc (T1/Voce#5). prin fallback accounts.rar_creds_{env}_enc.
- Masina de stari (sect. 6): tranzitii queued->sending->sent/requeued/error. - Masina de stari: tranzitii queued->sending->sent/requeued/error.
- Failure registry (sect. 8): 400/403/503+reconciliere. - Failure registry: 400/403/503+reconciliere.
- T16: purge_expired + purge_after setat la commit. - Purjare: purge_expired + purge_after setat la commit.
""" """
from __future__ import annotations from __future__ import annotations
@@ -451,10 +451,10 @@ class TestE2EAlreadySent:
class TestE2EMixedQueue: class TestE2EMixedQueue:
def test_coada_mixta_ambele_trimise(self, env, monkeypatch): def test_coada_mixta_ambele_trimise(self, env, monkeypatch):
"""Scenariul 3 (T1/Voce#5): API (creds efemere) + web (canal import, fara creds pe submission). """Scenariul 3: API (creds efemere) + web (canal import, fara creds pe submission).
Dupa purjarea creds efemere ale canalului API (la primul login), submission-urile Dupa purjarea creds efemere ale canalului API (la primul login), submission-urile
import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc (US-013, per-env). import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc, per-env.
Verificam ca ambele submission-uri ajung la status='sent'. Verificam ca ambele submission-uri ajung la status='sent'.
""" """
import app.worker.__main__ as w import app.worker.__main__ as w
@@ -486,11 +486,11 @@ class TestE2EMixedQueue:
assert r_api.status_code == 200, r_api.text assert r_api.status_code == 200, r_api.text
sub_api_id = r_api.json()["results"][0]["submission_id"] sub_api_id = r_api.json()["results"][0]["submission_id"]
# 3. Seteaza creds durabile in accounts per-env (US-013, canal web fallback) # 3. Seteaza creds durabile in accounts per-env (canal web fallback)
conn = get_connection() conn = get_connection()
try: try:
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"}) web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
# US-013: scrie in slotul per-env test (rar_env default = 'test') # scrie in slotul per-env test (rar_env default = 'test')
conn.execute( conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", "UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1",
(web_creds_enc,), (web_creds_enc,),
@@ -522,7 +522,7 @@ class TestE2EMixedQueue:
try: try:
processed = 0 processed = 0
# Drena coada simuland bucla worker completa (T1/D4: creds efemere + fallback per-env) # Drena coada simuland bucla worker completa: creds efemere + fallback per-env
for _ in range(10): # limita de siguranta for _ in range(10): # limita de siguranta
claimed = w.claim_one(conn) claimed = w.claim_one(conn)
if claimed is None: if claimed is None:
@@ -532,7 +532,7 @@ class TestE2EMixedQueue:
account_id = claimed["account_id"] account_id = claimed["account_id"]
rar_env = claimed.get("rar_env", "test") rar_env = claimed.get("rar_env", "test")
# T1/D4: creds din submission (API efemer) OR fallback per-env (US-013) # creds din submission (API efemer) OR fallback per-env
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env) creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env)
assert creds is not None, \ assert creds is not None, \
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe" f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
@@ -566,9 +566,9 @@ class TestE2EMixedQueue:
assert mock_rar.post_calls == 2 assert mock_rar.post_calls == 2
def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch): def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch):
"""T1/Gate purjare (OV-5): dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT. """Dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT.
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile, US-013). Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile).
""" """
import app.worker.__main__ as w import app.worker.__main__ as w
from app.crypto import encrypt_creds from app.crypto import encrypt_creds
@@ -590,7 +590,7 @@ class TestE2EMixedQueue:
assert r_api.status_code == 200 assert r_api.status_code == 200
sub_id = r_api.json()["results"][0]["submission_id"] sub_id = r_api.json()["results"][0]["submission_id"]
# Seteaza creds durabile pe cont (slotul per-env, US-013) # Seteaza creds durabile pe cont (slotul per-env)
conn = get_connection() conn = get_connection()
try: try:
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"}) web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
@@ -634,7 +634,7 @@ class TestE2EMixedQueue:
# =========================================================================== # # =========================================================================== #
# Masina de stari (sect. 6 din plan): tranzitii explicite per submission # # Masina de stari: tranzitii explicite per submission #
# =========================================================================== # # =========================================================================== #
class TestStateMachine: class TestStateMachine:
@@ -797,7 +797,7 @@ class TestStateMachine:
# =========================================================================== # # =========================================================================== #
# Registrul de esecuri (sect. 8): tipuri de eroare RAR si recuperare # # Registrul de esecuri: tipuri de eroare RAR si recuperare #
# =========================================================================== # # =========================================================================== #
class TestFailureRegistry: class TestFailureRegistry:
@@ -929,12 +929,12 @@ class TestFailureRegistry:
# =========================================================================== # # =========================================================================== #
# T16 — Purjare (purge_expired + purge_after setat la commit) # # Purjare (purge_expired + purge_after setat la commit) #
# =========================================================================== # # =========================================================================== #
class TestPurjare: class TestPurjare:
def test_purge_expired_sterge_sent_expirate(self, env): def test_purge_expired_sterge_sent_expirate(self, env):
"""T16: purge_expired sterge submissions cu status='sent' si purge_after in trecut.""" """purge_expired sterge submissions cu status='sent' si purge_after in trecut."""
from app.worker.__main__ import purge_expired from app.worker.__main__ import purge_expired
client, get_connection, get_settings = env client, get_connection, get_settings = env
@@ -958,7 +958,7 @@ class TestPurjare:
conn.close() conn.close()
def test_purge_expired_nu_sterge_queued(self, env): def test_purge_expired_nu_sterge_queued(self, env):
"""T16: purge_expired NU sterge non-sent (queued/error) chiar daca purge_after < now.""" """purge_expired NU sterge non-sent (queued/error) chiar daca purge_after < now."""
from app.worker.__main__ import purge_expired from app.worker.__main__ import purge_expired
client, get_connection, get_settings = env client, get_connection, get_settings = env
@@ -981,7 +981,7 @@ class TestPurjare:
conn.close() conn.close()
def test_purge_after_setat_la_commit_import(self, env): def test_purge_after_setat_la_commit_import(self, env):
"""T16: submissions create la commit import au purge_after setat (!=NULL).""" """submissions create la commit import au purge_after setat (!=NULL)."""
client, get_connection, get_settings = env client, get_connection, get_settings = env
import_id = _upload_and_map(client, rows=[_HEADER, _ROW_1]) import_id = _upload_and_map(client, rows=[_HEADER, _ROW_1])
@@ -1005,7 +1005,7 @@ class TestPurjare:
conn.close() conn.close()
def test_purge_after_setat_la_trimitere_worker(self, env): def test_purge_after_setat_la_trimitere_worker(self, env):
"""T16: dupa trimitere (status='sent'), purge_after se (re)seteaza la now+90 zile.""" """dupa trimitere (status='sent'), purge_after se (re)seteaza la now+90 zile."""
import app.worker.__main__ as w import app.worker.__main__ as w
client, get_connection, get_settings = env client, get_connection, get_settings = env

View File

@@ -1,10 +1,8 @@
"""Teste US-001 (PRD 3.6): backend — persista editarea unui rand de preview. """Teste backend — persista editarea unui rand de preview.
Approach B: editarea scrie un patch CANONIC in import_rows.override_json (criptat Editarea scrie un patch CANONIC in import_rows.override_json (criptat
Fernet), aplicat ULTIMUL in _resolve_row_for_preview + commit. raw_json/idempotency Fernet), aplicat ULTIMUL in _resolve_row_for_preview + commit. raw_json/idempotency
raman neatinse. Ruta editare = mutatie PURA (nu re-deriva status, nu atinge submissions). raman neatinse. Ruta editare = mutatie PURA (nu re-deriva status, nu atinge submissions).
TDD: testele se scriu inainte de implementare (RED -> GREEN).
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Teste US-005 (PRD 5.4): erori de import imbracate pe 3 niveluri. """Teste erori de import imbracate pe 3 niveluri.
Verifica ca fiecare HTTPException de import in scop are `detail` superset: Verifica ca fiecare HTTPException de import in scop are `detail` superset:
- cheile vechi: `error`, `message`, context specific (sheets/found/n_ok) - cheile vechi: `error`, `message`, context specific (sheets/found/n_ok)
@@ -327,7 +327,6 @@ class TestOverrideIlizibil3Niveluri:
assert r.status_code == 422, f"Asteptat 422, primit {r.status_code}: {r.text}" assert r.status_code == 422, f"Asteptat 422, primit {r.status_code}: {r.text}"
detail = r.json()["detail"] detail = r.json()["detail"]
# Dupa US-005: detail trebuie sa fie dict, nu string
assert isinstance(detail, dict), f"detail trebuie sa fie dict, primit: {type(detail)}" assert isinstance(detail, dict), f"detail trebuie sa fie dict, primit: {type(detail)}"
assert detail.get("error") == "override_ilizibil" assert detail.get("error") == "override_ilizibil"
assert detail.get("cod") == "IMPORT_OVERRIDE_ILIZIBIL" assert detail.get("cod") == "IMPORT_OVERRIDE_ILIZIBIL"

View File

@@ -143,7 +143,7 @@ def test_preview_arata_panoul_de_mapare(client):
assert r.status_code == 200 assert r.status_code == 200
assert "Operatii de mapat la cod RAR" in r.text assert "Operatii de mapat la cod RAR" in r.text
assert "OP-REV" in r.text assert "OP-REV" in r.text
# US-004: panoul foloseste ruta plurala (un singur form pentru toate operatiile) # Panoul foloseste ruta plurala (un singur form pentru toate operatiile)
assert "/_import/%d/mapare-operatii" % import_id in r.text assert "/_import/%d/mapare-operatii" % import_id in r.text

View File

@@ -1,7 +1,7 @@
"""Teste pentru app/import_parse.py (T14 + T3 + T10 + T13). """Teste pentru app/import_parse.py.
Fixture-urile xlsx sunt generate in-memory cu openpyxl (nu fisiere binare commituite). Fixture-urile xlsx sunt generate in-memory cu openpyxl (nu fisiere binare commituite).
Fiecare sectiune acopera un task distinct. Fiecare sectiune acopera un scenariu distinct.
""" """
from __future__ import annotations from __future__ import annotations
@@ -77,7 +77,7 @@ _ROW2 = ["WVWZZZ1KZAW000124", "CJ001AB", "2026-05-10", "98765", "Reparatie"]
# =========================================================================== # # =========================================================================== #
# T14 — CSV delimiter sniff + encoding + cap # # CSV delimiter sniff + encoding + cap #
# =========================================================================== # # =========================================================================== #
class TestCsvDelimiter: class TestCsvDelimiter:
@@ -134,7 +134,7 @@ class TestCsvDelimiter:
# =========================================================================== # # =========================================================================== #
# T14 — XLSX read_only dim-check + cap # # XLSX read_only dim-check + cap #
# =========================================================================== # # =========================================================================== #
class TestXlsxDimcheck: class TestXlsxDimcheck:
@@ -176,7 +176,7 @@ class TestXlsxDimcheck:
# =========================================================================== # # =========================================================================== #
# T3 — coercion guard + needs_review + mesaj formule-None # # Coercion guard + needs_review + mesaj formule-None #
# =========================================================================== # # =========================================================================== #
class TestCoercionNeedsReview: class TestCoercionNeedsReview:
@@ -278,7 +278,7 @@ class TestCoercionNeedsReview:
# =========================================================================== # # =========================================================================== #
# T10 — dezambiguizare data la nivel de COLOANA (OV-8) # # Dezambiguizare data la nivel de coloana #
# =========================================================================== # # =========================================================================== #
class TestDateColumnDisambiguation: class TestDateColumnDisambiguation:
@@ -344,7 +344,7 @@ class TestDateColumnDisambiguation:
# =========================================================================== # # =========================================================================== #
# T13 — robustete export RO (multi-sheet + merged header + footer trim) # # Robustete export RO (multi-sheet + merged header + footer trim) #
# =========================================================================== # # =========================================================================== #
class TestRobustetExportRO: class TestRobustetExportRO:

View File

@@ -1,4 +1,4 @@
"""Teste US-009 (PRD 5.20) — Import web: selector mediu RAR conditionat de disponibilitate. """Teste import web: selector mediu RAR conditionat de disponibilitate.
Verifica: Verifica:
- La 0 medii: banner avertisment non-blocant (upload functioneaza, commit foloseste ancora globala). - La 0 medii: banner avertisment non-blocant (upload functioneaza, commit foloseste ancora globala).
@@ -326,7 +326,7 @@ def test_commit_cu_doua_medii_respecta_alegerea(client):
def test_badge_mediu_in_preview(client): def test_badge_mediu_in_preview(client):
"""Preview-ul afiseaza badge-ul cu mediul tinta (US-009, F9/F10).""" """Preview-ul afiseaza badge-ul cu mediul tinta."""
_seed_nomenclator_si_mapare(client) _seed_nomenclator_si_mapare(client)
_configureaza_un_mediu(client, env="test") _configureaza_un_mediu(client, env="test")

View File

@@ -1,13 +1,11 @@
"""Teste US-007 (PRD 5.12): gate review in modal — import_rows.reviewed + confirmare. """Gate review in modal — import_rows.reviewed + confirmare.
TDD RED: testele sunt scrise inainte de implementare.
Scenarii: Scenarii:
1. Migrare: import_rows.reviewed INTEGER DEFAULT 0, idempotent. 1. Migrare: import_rows.reviewed INTEGER DEFAULT 0, idempotent.
2. Rand needs_review exclus din "gata de trimis" pana la confirmare explicita. 2. Rand needs_review exclus din "gata de trimis" pana la confirmare explicita.
3. POST confirma-review seteaza reviewed=1 → randul devine ok la recalcul. 3. POST confirma-review seteaza reviewed=1 → randul devine ok la recalcul.
4. `reviewed` NU intra in payload/idempotency (marcaj separat). 4. `reviewed` NU intra in payload/idempotency (marcaj separat).
5. Editarea unei valori pe un rand confirmat reseteaza reviewed=0 (D#9). 5. Editarea unei valori pe un rand confirmat reseteaza reviewed=0.
""" """
from __future__ import annotations from __future__ import annotations
@@ -268,8 +266,8 @@ def test_migrare_adauga_coloana_reviewed_idempotent():
def test_needs_review_exclus_din_gata_pana_la_confirmare(client): def test_needs_review_exclus_din_gata_pana_la_confirmare(client):
"""Un rand needs_review nu intra in 'gata de trimis' (n_confirmat = 0) pana la confirmare. """Un rand needs_review nu intra in 'gata de trimis' (n_confirmat = 0) pana la confirmare.
US-007 Q1: randul cu data ambigua apare cu pill 'Verifica valori' si este Randul cu data ambigua apare cu pill 'Verifica valori' si este
EXCLUS din n_confirmat. Bannerul de discoverability (T1) trebuie sa fie prezent. EXCLUS din n_confirmat. Bannerul de discoverability trebuie sa fie prezent.
""" """
_seed_op1() _seed_op1()
iid = _upload_and_preview_needs_review(client) iid = _upload_and_preview_needs_review(client)
@@ -295,7 +293,7 @@ def test_needs_review_exclus_din_gata_pana_la_confirmare(client):
assert n_val == 0, \ assert n_val == 0, \
f"n_confirmat trebuie sa fie 0 cand randul e needs_review (negasit), got {n_val}" f"n_confirmat trebuie sa fie 0 cand randul e needs_review (negasit), got {n_val}"
# Bannerul de discoverability (T1) — prezent cand summary.needs_review > 0 # Bannerul de discoverability — prezent cand summary.needs_review > 0
# Trebuie sa contina un mesaj despre faptul ca randurile nu pleaca pana la confirmare # Trebuie sa contina un mesaj despre faptul ca randurile nu pleaca pana la confirmare
# (ex. 'nu pleaca la RAR' sau 'confirmi in modal' sau 'Verifica valori') # (ex. 'nu pleaca la RAR' sau 'confirmi in modal' sau 'Verifica valori')
banner_present = ( banner_present = (
@@ -304,13 +302,13 @@ def test_needs_review_exclus_din_gata_pana_la_confirmare(client):
"preview-needs-review-banner" in html "preview-needs-review-banner" in html
) )
assert banner_present, \ assert banner_present, \
"Bannerul de discoverability (T1) trebuie sa fie prezent cand exista randuri needs_review" "Bannerul de discoverability trebuie sa fie prezent cand exista randuri needs_review"
def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client): def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
"""POST /_import/{id}/rand/0/confirma-review seteaza reviewed=1. """POST /_import/{id}/rand/0/confirma-review seteaza reviewed=1.
US-007 T2: operatorul apasa 'Confirma valorile' in modal → Operatorul apasa 'Confirma valorile' in modal →
reviewed=1 in DB → randul devine ok la recalcul preview. reviewed=1 in DB → randul devine ok la recalcul preview.
Verifica: Verifica:
@@ -350,9 +348,8 @@ def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
def test_reviewed_nu_intra_in_payload_sau_idempotency(client): def test_reviewed_nu_intra_in_payload_sau_idempotency(client):
"""reviewed NU intra in payload, override_json sau cheia de idempotenta. """reviewed NU intra in payload, override_json sau cheia de idempotenta.
US-007 marcaj separat (D#8): reviewed e DOAR un flag de confirmare umana, reviewed e DOAR un flag de confirmare umana, nu un camp de continut.
nu un camp de continut. Cheia de idempotenta trebuie sa fie identica inainte Cheia de idempotenta trebuie sa fie identica inainte si dupa confirmare.
si dupa confirmare.
""" """
_seed_op1() _seed_op1()
iid = _upload_and_preview_needs_review(client) iid = _upload_and_preview_needs_review(client)
@@ -393,7 +390,7 @@ def test_reviewed_nu_intra_in_payload_sau_idempotency(client):
def test_editare_valoare_pe_needs_review_reseteaza_reviewed(client): def test_editare_valoare_pe_needs_review_reseteaza_reviewed(client):
"""Editarea unei valori pe un rand confirmat reseteaza reviewed=0 (D#9). """Editarea unei valori pe un rand confirmat reseteaza reviewed=0.
Cand operatorul SCHIMBA o valoare (via POST editeaza) pe un rand deja confirmat Cand operatorul SCHIMBA o valoare (via POST editeaza) pe un rand deja confirmat
(reviewed=1), apply_row_override trebuie sa reseteze reviewed=0. (reviewed=1), apply_row_override trebuie sa reseteze reviewed=0.
@@ -418,7 +415,7 @@ def test_editare_valoare_pe_needs_review_reseteaza_reviewed(client):
# reviewed trebuie sa fie 0 (resetat de apply_row_override) # reviewed trebuie sa fie 0 (resetat de apply_row_override)
assert _get_reviewed(iid, 0) == 0, \ assert _get_reviewed(iid, 0) == 0, \
"reviewed trebuie sa fie resetat la 0 dupa editarea unei valori (D#9 — re-cere confirmare)" "reviewed trebuie sa fie resetat la 0 dupa editarea unei valori (re-cere confirmare)"
def test_confirma_review_guard_committed_409(client): def test_confirma_review_guard_committed_409(client):
@@ -544,11 +541,11 @@ def test_confirma_review_scoped_404_alt_cont():
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Teste markup Bug 1: confirma-review form swap (B1) # # Teste markup: confirma-review form swap #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def test_confirma_review_form_nu_foloseste_hx_swap_none(): def test_confirma_review_form_nu_foloseste_hx_swap_none():
"""Bug B1 (markup): formularul confirma-review NU trebuie sa foloseasca """Formularul confirma-review NU trebuie sa foloseasca
hx-swap='none' — cu none, htmx aplica doar OOB-urile dar NU executa scriptul hx-swap='none' — cu none, htmx aplica doar OOB-urile dar NU executa scriptul
din continutul principal → updateN() nu ruleaza → n_confirmat stale → 422. din continutul principal → updateN() nu ruleaza → n_confirmat stale → 422.
@@ -561,7 +558,7 @@ def test_confirma_review_form_nu_foloseste_hx_swap_none():
) )
html = template.read_text(encoding="utf-8") html = template.read_text(encoding="utf-8")
# Gasim sectiunea formularului confirma-review (dupa marcajul T2) # Gasim sectiunea formularului confirma-review
idx = html.find("confirma-review") idx = html.find("confirma-review")
assert idx >= 0, "Formularul confirma-review nu a fost gasit in template" assert idx >= 0, "Formularul confirma-review nu a fost gasit in template"
form_section = html[idx:] form_section = html[idx:]
@@ -622,7 +619,7 @@ _ROWS_MULTI_NEEDS_REVIEW = [
def test_confirma_toate_review_marcheaza_toate(client): def test_confirma_toate_review_marcheaza_toate(client):
"""B1 bulk: POST /_import/{id}/confirma-toate-review seteaza reviewed=1 pe TOATE """POST /_import/{id}/confirma-toate-review seteaza reviewed=1 pe TOATE
randurile needs_review din batch cu un singur click. randurile needs_review din batch cu un singur click.
Verifica: Verifica:
@@ -776,7 +773,7 @@ def test_confirma_toate_review_scoped_404_alt_cont():
def test_confirm_rapid_buton_prezent_in_tabel(client): def test_confirm_rapid_buton_prezent_in_tabel(client):
"""B1 quick: randul needs_review are butonul de confirm rapid direct in tabel, """Randul needs_review are butonul de confirm rapid direct in tabel,
care POST-eaza pe ruta /confirma-review (fara a deschide modalul).""" care POST-eaza pe ruta /confirma-review (fara a deschide modalul)."""
_seed_op1() _seed_op1()
iid = _upload_and_preview_needs_review(client) iid = _upload_and_preview_needs_review(client)
@@ -791,7 +788,7 @@ def test_confirm_rapid_buton_prezent_in_tabel(client):
def test_confirm_rapid_per_rand_seteaza_reviewed(client): def test_confirm_rapid_per_rand_seteaza_reviewed(client):
"""B1 quick: POST /confirma-review din tabel (fara modal) seteaza reviewed=1 pe un """POST /confirma-review din tabel (fara modal) seteaza reviewed=1 pe un
singur rand si NU atinge celelalte randuri needs_review din batch.""" singur rand si NU atinge celelalte randuri needs_review din batch."""
_seed_op1() _seed_op1()
iid = _upload_and_preview_rows(client, _ROWS_MULTI_NEEDS_REVIEW) iid = _upload_and_preview_rows(client, _ROWS_MULTI_NEEDS_REVIEW)
@@ -806,7 +803,7 @@ def test_confirm_rapid_per_rand_seteaza_reviewed(client):
def test_confirma_review_cere_reincarcarea_preview(client): def test_confirma_review_cere_reincarcarea_preview(client):
"""Contractul nou (dogfood 5.13): confirma-review NU mai depinde de scriptul updateN """Confirma-review NU mai depinde de scriptul updateN
din payload (care, cu OOB pe <tr> rupt, lasa randul stale). Acum cere reincarcaPreview, din payload (care, cu OOB pe <tr> rupt, lasa randul stale). Acum cere reincarcaPreview,
iar preview-ul reincarcat re-randeaza contorul si butonul de confirmare cu n_confirmat iar preview-ul reincarcat re-randeaza contorul si butonul de confirmare cu n_confirmat
corect server-side — deci problema B1 (n_confirmat stale -> 422) dispare structural. corect server-side — deci problema B1 (n_confirmat stale -> 422) dispare structural.

View File

@@ -1,4 +1,4 @@
"""Teste UI web import (U5) — upload → mapare coloane → preview → confirmare. """Teste UI web import — upload → mapare coloane → preview → confirmare.
Verifica: Verifica:
- Dashboard randeaza sectiunea de upload - Dashboard randeaza sectiunea de upload
@@ -104,7 +104,7 @@ def _seed_op_mapping(client, cod_op: str = "Revizie", cod_prest: str = "OE-1") -
def test_dashboard_contine_drop_zone(client): def test_dashboard_contine_drop_zone(client):
"""Tab-ul Import randeaza sectiunea de upload cu drop zone si mesaj warmth. """Tab-ul Import randeaza sectiunea de upload cu drop zone si mesaj warmth.
Dupa US-003 sectiunea de import e in tab-ul Import (?tab=import), nu pe pagina principala. Sectiunea de import e in tab-ul Import (?tab=import), nu pe pagina principala.
""" """
r = client.get("/?tab=import") r = client.get("/?tab=import")
assert r.status_code == 200 assert r.status_code == 200
@@ -389,11 +389,11 @@ def test_upload_multi_sheet_arata_selector(client):
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Elemente a11y (D10/D11/D12) # # Elemente a11y #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def test_preview_contine_banner_declarant(client): def test_preview_contine_banner_declarant(client):
"""Preview contine bannerul declarant (D12) cu text despre ireversibil.""" """Preview contine bannerul declarant cu text despre ireversibil."""
import_id = _setup_preview(client) import_id = _setup_preview(client)
r = client.get(f"/_import/{import_id}/preview") r = client.get(f"/_import/{import_id}/preview")
assert r.status_code == 200 assert r.status_code == 200
@@ -403,7 +403,7 @@ def test_preview_contine_banner_declarant(client):
def test_preview_contine_checkboxuri_needs_review(client): def test_preview_contine_checkboxuri_needs_review(client):
"""Randurile needs_review au checkbox 'verificat' (D11).""" """Randurile needs_review au checkbox 'verificat'."""
# Cream un rand cu VIN numeric → needs_review # Cream un rand cu VIN numeric → needs_review
rows_with_review = [ rows_with_review = [
{ {
@@ -431,7 +431,7 @@ def test_preview_contine_checkboxuri_needs_review(client):
def test_preview_duplicate_in_file_are_text(client): def test_preview_duplicate_in_file_are_text(client):
"""Randurile duplicate_in_file arata text 'dubla cu randul N' (D10 — nu doar culoare).""" """Randurile duplicate_in_file arata text 'dubla cu randul N' (nu doar culoare)."""
_seed_op_mapping(client, "Revizie", "OE-1") _seed_op_mapping(client, "Revizie", "OE-1")
# Doua randuri identice → duplicate_in_file # Doua randuri identice → duplicate_in_file
dup_rows = [ dup_rows = [
@@ -451,6 +451,6 @@ def test_preview_duplicate_in_file_are_text(client):
) )
r = client.get(f"/_import/{import_id}/preview") r = client.get(f"/_import/{import_id}/preview")
assert r.status_code == 200 assert r.status_code == 200
# Text explicit pentru duplicate_in_file (nu doar culoare — cerinta daltonism D10) # Text explicit pentru duplicate_in_file (nu doar culoare — cerinta daltonism)
assert "dubla cu randul" in r.text assert "dubla cu randul" in r.text
assert "duplicate_in_file" in r.text assert "duplicate_in_file" in r.text

Some files were not shown because too many files have changed in this diff Show More