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:
112
CLAUDE.md
112
CLAUDE.md
@@ -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
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
42
app/db.py
42
app/db.py
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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": (
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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=?",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
17
app/plans.py
17
app/plans.py
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
×
|
×
|
||||||
</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 %}
|
||||||
|
|||||||
@@ -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 ===
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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). #}
|
||||||
|
|||||||
@@ -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' %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 /
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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). #}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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;}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}.
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user