chore: curatare agresiva comentarii — scoatere referinte US/PRD din cod si template-uri

Eliminat zgomotul de trasabilitate (US-xxx, PRD x.x, Rn, OV-x, Tn, decizii/naratiune
istorica) din 41 fisiere app/ + template-uri. Pastrate comentariile care documenteaza
invarianti si logica ne-evidenta (idempotenta/hash, reconciliere anti-duplicat, RAR 500
esec definitiv, creds per cont, WAF User-Agent, 422 fara echo de parola, scope NULL->1),
curatate doar de tokeni.

Verificare: pentru cele 27 module .py curatate, structura de cod (tokeni non-comentariu/
non-string) e IDENTICA fata de HEAD -> doar comentarii/docstring-uri schimbate. Singura
schimbare de cod e in tests/test_web_responsive.py (scos 3 assert pe markeri US-006/007/008,
inlocuite de asertiunile structurale alaturate). 0 tokeni US/PRD reziduali in app/.
Regresie: 896 passed, 1 deselected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-25 21:44:24 +00:00
parent f05fe5b221
commit 4a2afc68bf
43 changed files with 547 additions and 649 deletions

View File

@@ -8,14 +8,13 @@ Endpointuri:
POST /v1/import/{id}/commit — gate HARD + enqueue randuri ok + log atestare
GET /v1/import/{id}/export-failed — CSV cu randuri esuate (needs_data/needs_mapping/needs_review)
Reguli cheie (plan §3.1-3.4, §12):
- Issue 6: scrieri bulk in tranzactie explicita BEGIN IMMEDIATE...COMMIT + executemany.
- Eng#5: already_sent lookup BATCH (IN chunk ~900), nu N+1.
- OV-3: duplicate_in_file EXCLUSIV la preview/commit. NU atinge reconcile.py/worker.
- Issue 1 (TOCTOU): commit per-rand cu ON CONFLICT(idempotency_key) DO NOTHING.
- Issue 5a: import_rows.raw_json CRIPTAT Fernet.
- Issue 5b: fuzzy coloane refoloseste mapping.normalize_for_match (DRY).
- T4/D3: drift semnatura coloane -> NU aplica orb, cere re-confirmare.
Reguli cheie:
- Scrieri bulk in tranzactie explicita BEGIN IMMEDIATE...COMMIT + executemany.
- already_sent lookup BATCH (IN chunk ~900), nu N+1.
- duplicate_in_file EXCLUSIV la preview/commit. NU atinge reconcile.py/worker.
- TOCTOU: commit per-rand cu ON CONFLICT(idempotency_key) DO NOTHING.
- import_rows.raw_json CRIPTAT Fernet.
- Drift semnatura coloane -> NU aplica orb, cere re-confirmare.
"""
from __future__ import annotations
@@ -60,7 +59,7 @@ router = APIRouter(prefix="/v1/import", tags=["import"])
# Marimea maxima a unui chunk pentru IN(...) SQLite (limite SQLite ~999)
_IN_CHUNK = 900
# Campuri canonice si sinonimele lor pentru sugestie fuzzy coloane (Issue 5b/Eng#4)
# Campuri canonice si sinonimele lor pentru sugestie fuzzy coloane
_CANONICAL_SYNONYMS: dict[str, list[str]] = {
"vin": ["VIN", "Serie sasiu", "Sasiu", "Serie", "Numar sasiu", "Nr sasiu", "Chassis"],
"nr_inmatriculare": ["Nr inmatriculare", "Numar inmatriculare", "Numar auto", "Nr auto", "Numar", "Nr"],
@@ -93,7 +92,7 @@ def _fuzzy_suggest_column(
) -> list[dict]:
"""Sugereaza campuri canonice pentru o coloana din fisier.
Refoloseste normalize_for_match + rapidfuzz.fuzz.token_sort_ratio (Issue 5b/Eng#4).
Refoloseste normalize_for_match + rapidfuzz.fuzz.token_sort_ratio.
Intoarce [{camp_canonic, score}] sortat descrescator.
"""
from rapidfuzz import fuzz, process
@@ -140,10 +139,10 @@ def _resolve_row_for_preview(
errors: lista erori validare
flags: motive needs_review
`override` (3.6, Approach B): patch CANONIC editat in preview, aplicat ULTIMUL
peste valorile mapate (dupa `json_mapare` si canonicalizare). Permite corectarea
unei valori sau completarea unui camp a carui coloana LIPSESTE din fisier, fara
sa atinga `raw_json`/idempotency.
`override`: patch CANONIC editat in preview, aplicat ULTIMUL peste valorile
mapate (dupa `json_mapare` si canonicalizare). Permite corectarea unei valori
sau completarea unui camp a carui coloana LIPSESTE din fisier, fara sa atinga
`raw_json`/idempotency.
"""
# Aplica maparea de coloane
mapped: dict[str, Any] = {}
@@ -151,7 +150,7 @@ def _resolve_row_for_preview(
if col_fisier in raw_row and camp_canonic:
mapped[camp_canonic] = raw_row[col_fisier]
# Detectie coloane cu formule (Issue 3) — nu blocheaza, dar adauga flag
# Detectie coloane cu formule — nu blocheaza, dar adauga flag
formula_flag: list[str] = []
for col_fisier, camp_canonic in json_mapare.items():
if col_fisier in formula_columns:
@@ -186,7 +185,7 @@ def _resolve_row_for_preview(
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}]
# Canonicalizare (T9): normalizeaza VIN/nr/odometru
# Canonicalizare: normalizeaza VIN/nr/odometru
canon = canonicalize_row(mapped)
mapped.update({
"vin": canon["vin"],
@@ -194,7 +193,7 @@ def _resolve_row_for_preview(
"odometru_final": canon["odometru_final"],
})
# Override editat in preview (3.6) — aplicat ULTIMUL, peste valorile mapate +
# Override editat in preview — aplicat ULTIMUL, peste valorile mapate +
# canonicalizate. Valorile din override sunt deja canonice (vezi _merge_override).
if override:
mapped.update(override)
@@ -230,7 +229,7 @@ def _resolve_row_for_preview(
"flags": all_flags,
}
# auto_send gate (T6/OV-1)
# auto_send gate
if has_no_auto_send(resolved, mapping_meta):
return {
"resolved_status": "needs_mapping",
@@ -261,8 +260,8 @@ def _build_idempotency_key(account_id: int | None, resolved: dict[str, Any]) ->
return build_key(account_id, canon)
# Campuri de continut editabile in preview (3.6). Operatia/codul RAR NU se editeaza
# aici (raman in panoul de mapare) — vezi Non-Goals din PRD 3.6.
# Campuri de continut editabile in preview. Operatia/codul RAR NU se editeaza
# aici (raman in panoul de mapare).
EDIT_FIELDS = ("vin", "nr_inmatriculare", "data_prestatie", "odometru_initial", "odometru_final")
@@ -350,7 +349,7 @@ def apply_row_override(
def _already_sent_lookup(conn, account_id: int, keys: list[str]) -> dict[str, dict]:
"""Cauta cheile de idempotenta in submissions (batch, nu N+1 — Eng#5).
"""Cauta cheile de idempotenta in submissions (batch, nu N+1).
Intoarce {idempotency_key: {id, id_prezentare, created_at}} pentru cheile gasite.
"""
@@ -371,7 +370,7 @@ def _already_sent_lookup(conn, account_id: int, keys: list[str]) -> dict[str, di
"id_prezentare": r["id_prezentare"],
"created_at": r["created_at"],
}
# Dual-lookup pentru chei legacy (OV-2: chei vechi cu account_id=None)
# Dual-lookup pentru chei legacy (chei vechi cu account_id=None)
legacy_keys_needed = [k for k in chunk if k not in found]
if legacy_keys_needed:
lph = ",".join("?" * len(legacy_keys_needed))
@@ -403,8 +402,7 @@ async def upload_import(
"""Upload fisier xlsx/csv -> staging in import_batches/import_rows.
Nu trimite nimic la RAR. Intoarce {import_id, columns, sample_rows, sheets?}.
PII (raw_json) criptat Fernet la rest (Issue 5a).
Scrieri bulk in tranzactie explicita (Issue 6).
PII (raw_json) criptat Fernet la rest. Scrieri bulk in tranzactie explicita.
"""
acct = account_or_default(account_id)
data = await file.read()
@@ -468,7 +466,7 @@ async def upload_import(
try:
sig = _signature(parsed.columns)
# Issue 6: tranzactie explicita BEGIN IMMEDIATE + executemany
# Tranzactie explicita BEGIN IMMEDIATE + executemany
conn.execute("BEGIN IMMEDIATE")
try:
# Insert import_batches
@@ -482,7 +480,7 @@ async def upload_import(
# Insert import_rows bulk (executemany) cu PII criptat
row_params = []
for i, row_dict in enumerate(parsed.rows):
raw_json_enc = encrypt_creds(row_dict) # Criptat Fernet (Issue 5a)
raw_json_enc = encrypt_creds(row_dict) # Criptat Fernet
row_params.append((batch_id, i, raw_json_enc, "pending", None))
conn.executemany(
@@ -506,11 +504,8 @@ async def upload_import(
# Sample rows (primele 3, fara PII)
sample = parsed.rows[:3]
# Persistam metadata parsedata (coercion_flags, date_col_format, formula_columns)
# in import_batches pentru refolosire la preview (stocam ca JSON in 'status' nu e OK,
# ci ca metadate suplimentare — le stocam intr-un rand separat sau returnam direct)
# Solutie: le returnam in raspuns; preview-ul le va recalcula din raw_json deja stocat
# SAU le stocam ca un camp extra. Cel mai simplu: stocam coloanele in batch.
# Metadata parsata (coercion_flags etc.) se intoarce in raspuns; preview-ul
# o recalculeaza din raw_json deja stocat.
conn.execute(
"UPDATE import_batches SET ok=?, needs_review=? WHERE id=?",
(0, len(parsed.coercion_flags), batch_id),
@@ -532,7 +527,7 @@ async def upload_import(
result["column_mapping"] = json.loads(existing_mapping["json_mapare"])
result["format_data"] = existing_mapping["format_data"]
else:
# Sugestii fuzzy per coloana (Issue 5b: refoloseste normalize_for_match)
# Sugestii fuzzy per coloana
suggestions: dict[str, list[dict]] = {}
for col in parsed.columns:
sugg = _fuzzy_suggest_column(col, limit=3)
@@ -676,7 +671,7 @@ def save_column_mapping(
# --------------------------------------------------------------------------- #
# GET /v1/import/{id}/preview — 6 stari per rand (T2 + T11) #
# GET /v1/import/{id}/preview — 6 stari per rand #
# --------------------------------------------------------------------------- #
@router.get("/{import_id}/preview")
@@ -686,8 +681,8 @@ def preview_import(
) -> dict:
"""Preview 6 stari per rand: ok/needs_mapping/needs_data/needs_review/already_sent/duplicate_in_file.
Nu enqueue-aza nimic. Already_sent = lookup batch (Eng#5). Duplicate_in_file = intra-batch
collision (OV-3: EXCLUSIV aici, NU in reconcile.py/worker).
Nu enqueue-aza nimic. Already_sent = lookup batch. Duplicate_in_file = intra-batch
collision (EXCLUSIV aici, NU in reconcile.py/worker).
"""
acct = account_or_default(account_id)
conn = get_connection()
@@ -708,7 +703,7 @@ def preview_import(
if not raw_rows_db:
return {"rows": [], "summary": {}}
# Decripteaza si reconstruieste randurile + override-urile editate (3.6)
# Decripteaza si reconstruieste randurile + override-urile editate
rows: list[dict] = []
overrides: list[dict] = []
for r in raw_rows_db:
@@ -747,22 +742,18 @@ def preview_import(
json_mapare: dict[str, str] = json.loads(mapping_row["json_mapare"])
format_data = mapping_row["format_data"]
# Incarca maparea de operatii o singura data (Eng#5: load_mapping o singura data)
# Incarca maparea de operatii o singura data
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# Validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Reconstruieste parsed info (coercion_flags si date_col_format) din datele stocate
# Nota: import_rows stocheaza raw_json dupa coercion (din parse_file)
# Recalculam flags din valorile stocate (coercion_flags nu e stocat separat)
# Vom folosi o detectie simpla: VIN-uri care par numerice si odometru float
# Recalculam coercion_flags din valorile stocate (nu sunt persistate separat):
# detectie simpla de VIN numeric.
coercion_flags_map: dict[int, list[str]] = {}
# Detectam din valorile stocate
for i, row_dict in enumerate(rows):
flags = []
# Detectam VIN numeric: daca valoarea a fost stocata si arata ca numar
for col_f, camp_c in json_mapare.items():
if camp_c == "vin":
vin_val = row_dict.get(col_f)
@@ -830,11 +821,11 @@ def preview_import(
"idempotency_key": key,
})
# Already_sent: batch lookup (Eng#5 — nu N+1)
# Already_sent: batch lookup (nu N+1)
unique_keys = list(set(keys_for_lookup))
already_sent_map = _already_sent_lookup(conn, account_id, unique_keys)
# Duplicate_in_file (OV-3): detectie coliziuni intra-batch
# Duplicate_in_file: detectie coliziuni intra-batch.
# Grupam pe cheie de idempotenta: >1 rand cu aceeasi cheie = duplicate
key_to_indices: dict[str, list[int]] = {}
for row in preview_rows:
@@ -857,7 +848,7 @@ def preview_import(
row["already_sent_info"] = sent_info
continue
# Duplicate_in_file (OV-3): >1 rand cu aceeasi cheie in ACELASI fisier
# Duplicate_in_file: >1 rand cu aceeasi cheie in ACELASI fisier
indices_with_same_key = key_to_indices.get(k, [])
if len(indices_with_same_key) > 1 and row["resolved_status"] in ("ok", "needs_review", "needs_data"):
others = [idx for idx in indices_with_same_key if idx != row["row_index"]]
@@ -911,7 +902,7 @@ def preview_import(
# --------------------------------------------------------------------------- #
# POST /v1/import/{id}/commit — gate HARD + enqueue + log atestare (T5+T12) #
# POST /v1/import/{id}/commit — gate HARD + enqueue + log atestare #
# --------------------------------------------------------------------------- #
class CommitIn(BaseModel):
@@ -929,9 +920,9 @@ def commit_import(
req: CommitIn,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Gate HARD confirmare + enqueue randuri ok + log atestare (T5+T12).
"""Gate HARD confirmare + enqueue randuri ok + log atestare.
TOCTOU (Issue 1): INSERT per-rand cu ON CONFLICT(idempotency_key) DO NOTHING.
TOCTOU: INSERT per-rand cu ON CONFLICT(idempotency_key) DO NOTHING.
Randuri colidante = reclasificate already_sent in rezultatul commit-ului.
rows_hash + n_confirmed acopera DOAR randurile efectiv puse in coada.
"""
@@ -981,7 +972,7 @@ def commit_import(
elif r["resolved_status"] == "needs_review":
review_indices.add(r["row_index"])
# needs_review bifate explicit (Voce#1 — atestare pe valori)
# needs_review bifate explicit (atestare pe valori)
confirmed_review = [idx for idx in req.reviewed_rows if idx in review_indices]
for idx in confirmed_review:
# Gaseste randul needs_review si il adauga la ok_rows
@@ -1040,7 +1031,7 @@ def commit_import(
# Incarca maparea de operatii
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# Validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
@@ -1049,10 +1040,9 @@ def commit_import(
toctou_collisions: list[int] = []
rows_for_hash: list[str] = []
# Enqueue in tranzactie explicita (Issue 6)
# Enqueue in tranzactie explicita
conn.execute("BEGIN IMMEDIATE")
try:
# purge_after pentru submissions noi (T16)
purge_after_sql = "datetime('now', '+90 days')"
for ok_row in ok_rows:
@@ -1100,7 +1090,7 @@ def commit_import(
"odometru_final": canon["odometru_final"],
})
# Override editat in preview (3.6) — aplicat ULTIMUL, ca in resolver.
# Override editat in preview — aplicat ULTIMUL, ca in resolver.
override = ok_row.get("override") or {}
if override:
mapped.update(override)
@@ -1127,7 +1117,7 @@ def commit_import(
payload_json = json.dumps(mapped, ensure_ascii=False)
# INSERT ON CONFLICT DO NOTHING (TOCTOU — Issue 1)
# INSERT ON CONFLICT DO NOTHING (TOCTOU)
cur = conn.execute(
"INSERT OR IGNORE INTO submissions "
"(idempotency_key, account_id, status, payload_json, batch_id, row_index, purge_after) "
@@ -1140,7 +1130,6 @@ def commit_import(
toctou_collisions.append(row_index)
else:
sub_id = cur.lastrowid
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, int(sub_id), resolved)
enqueued.append({
"submission_id": sub_id,
@@ -1155,7 +1144,7 @@ def commit_import(
n_enqueued = len(enqueued)
# Log atestare (Voce#9): rows_hash + n_confirmed acopera DOAR randurile puse in coada
# Log atestare: rows_hash + n_confirmed acopera DOAR randurile puse in coada
rows_hash = hashlib.sha256(
json.dumps(sorted(rows_for_hash), ensure_ascii=False).encode("utf-8")
).hexdigest() if rows_for_hash else ""
@@ -1185,7 +1174,7 @@ def commit_import(
# --------------------------------------------------------------------------- #
# POST /v1/import/{id}/rand/{row_index}/editeaza — editare celule preview (3.6) #
# POST /v1/import/{id}/rand/{row_index}/editeaza — editare celule preview #
# --------------------------------------------------------------------------- #
class RandEditIn(BaseModel):
@@ -1205,7 +1194,7 @@ def editeaza_rand(
req: RandEditIn,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Persista editarea unui rand de preview (mutatie pura — Approach B, 3.6).
"""Persista editarea unui rand de preview (mutatie pura).
NU recalculeaza statusul si NU atinge `submissions`; preview-ul rederiva statusul
prin `_resolve_row_for_preview` cu override aplicat ultimul.
@@ -1225,7 +1214,7 @@ def editeaza_rand(
# --------------------------------------------------------------------------- #
# GET /v1/import/{id}/export-failed — CSV randuri esuate (T8) #
# GET /v1/import/{id}/export-failed — CSV randuri esuate #
# --------------------------------------------------------------------------- #
_EXPORT_FAILED_COLUMNS = [

View File

@@ -1,4 +1,4 @@
"""Router integrare US-001 — endpoint-uri de integrare externe.
"""Router integrare — endpoint-uri de integrare externe.
Endpointuri:
GET /v1/ping — readiness check per cont (autentificat sau dev fallback)

View File

@@ -1,12 +1,10 @@
"""API v1 — suprafata gateway (schelet).
"""API v1 — suprafata gateway.
Endpointuri din plan.md sect. 4. In schelet:
Endpointuri:
- POST /v1/prezentari: enqueue cu idempotenta (dedup pe idempotency_key UNIQUE).
- GET /v1/prezentari, /v1/prezentari/{id}: monitorizare coada.
- GET /v1/nomenclator: cache local.
- GET /v1/mapari: listare mapari cont.
Validarea completa (T3), maparea op->cod, auth API-key, redactarea creds in
middleware (CORE) si exportul CSV vin ulterior — marcate TODO unde lipsesc.
"""
from __future__ import annotations
@@ -79,7 +77,7 @@ def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, tex
def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
"""Coduri nemapate imbogatite cu 3 niveluri (COD_NEMAPAT), pentru raspuns onest."""
"""Coduri nemapate imbogatite cu 3 niveluri (COD_NEMAPAT)."""
return [
{**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} necunoscut/fara mapare RAR")}
for u in unmapped
@@ -87,7 +85,7 @@ def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
def _motiv_clasificare(cl: dict) -> str | None:
"""Rezumat uman pe o linie pentru un rezultat de clasificare (PRD 5.7).
"""Rezumat uman pe o linie pentru un rezultat de clasificare.
None cand status='queued'. Acopera toate ramurile de blocaj: erori de continut
(needs_data), coduri nemapate (needs_mapping) si auto_send oprit (needs_mapping).
@@ -107,7 +105,7 @@ def _motiv_clasificare(cl: dict) -> str | None:
def _rezultat_enqueue(submission_id: int | None, cl: dict, **extra) -> SubmissionResult:
"""SubmissionResult onest dintr-un rezultat de clasificare (PRD 5.7).
"""SubmissionResult onest dintr-un rezultat de clasificare.
Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman)
pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None.
@@ -141,42 +139,40 @@ def create_prezentari(
) -> PrezentariResponse:
"""Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission.
Validarea de continut (T3, app.validation) ruleaza inainte de enqueue:
esecurile NU resping cererea, ci enqueue-aza cu status `needs_data` + motiv
(plan.md sect. 3). JSON malformat -> 422 din Pydantic (validare de shape).
Validarea de continut (app.validation) ruleaza inainte de enqueue: esecurile NU
resping cererea, ci enqueue-aza cu status `needs_data` + motiv. JSON malformat ->
422 din Pydantic (validare de shape).
account_id vine din cheia API (resolve_account_id): cont real cu cheie,
implicit id=1 in dev fara cheie, 401 fara cheie valida in prod.
Nota: rar_credentials NU se persista (zero-storage) — worker-ul le va primi
pe alt canal (T2); in schelet enqueue-ul doar stocheaza prezentarea.
Cand rar_credentials lipseste, submission-ul intra fara creds efemere: worker-ul
cade pe creds-urile durabile ale contului (`accounts.rar_creds_enc`).
"""
acct = account_or_default(account_id)
# Creds RAR efemere: criptate si lipite de fiecare submission nou pana la
# primul login reusit pentru cont (worker le sterge atunci). Zero-storage at
# rest — niciodata in clar in DB/loguri (plan sect. 5). Optional: cand lipsesc,
# rest — niciodata in clar in DB/loguri. Optional: cand lipsesc,
# creds_enc=NULL si worker-ul foloseste creds-urile durabile ale contului.
creds_enc = encrypt_creds(req.rar_credentials.model_dump()) if req.rar_credentials else None
conn = get_connection()
results: list[SubmissionResult] = []
try:
# T6/OV-1: load_mapping_meta include auto_send per op (gate pentru coduri noi).
# load_mapping_meta include auto_send per op (gate pentru coduri noi).
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
# valid_codes gol (nomenclator nepopulat) -> None (nu validam, ca sa nu blocam tot).
valid_codes = load_nomenclator_codes(conn) or None
# Reguli text incarcate o data per cerere (seam partajat cu dry-run, invariant 5.2).
# Reguli text incarcate o data per cerere (seam partajat cu dry-run).
text_rules = load_text_rules(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
for prez in req.prezentari:
content = prez.model_dump()
# T9/OV-2: canonicalize_row inaintea build_key (odometru strip ".0", VIN upper).
# canonicalize_row inaintea build_key (odometru strip ".0", VIN upper).
# build_key aplica account_or_default(account_id) inainte de hash:
# None si 1 colapseaza la aceeasi cheie (canal API + canal import).
canon = canonicalize_row(content)
key = build_key(account_id, canon)
# Aplica normalizarea si in content (odometru canonicalizat inainte de validare, §3.4bis)
# Aplica normalizarea si in content (odometru canonicalizat inainte de validare)
content.update({
"vin": canon["vin"],
"nr_inmatriculare": canon["nr_inmatriculare"],
@@ -187,7 +183,7 @@ def create_prezentari(
(key,),
).fetchone()
if existing:
# US-012: un rand `error` (ex. creds RAR gresite) NU mai blocheaza tacit
# Un rand `error` (ex. creds RAR gresite) NU mai blocheaza tacit
# retrimiterea aceluiasi continut. Il RE-ACTIVAM (re-clasificam + actualizam
# creds + reset), printr-un UPDATE compare-and-swap pe status='error'.
if existing["status"] == "error":
@@ -205,17 +201,16 @@ def create_prezentari(
cl["rar_error"], creds_enc, existing["id"]),
)
if cur.rowcount == 1:
# Creds noi se propaga si in canalul durabil (accounts.rar_creds_enc,
# decizie #17) — ambele canale converg pe parola corectata.
# Creds noi se propaga si in canalul durabil (accounts.rar_creds_enc)
# — ambele canale converg pe parola corectata.
if req.rar_credentials is not None:
conn.execute(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
(encrypt_creds(req.rar_credentials.model_dump()), acct),
)
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, existing["id"], cl["resolved"])
# Raspuns onest si la reactivare (PRD 5.7): daca re-clasificarea
# cade pe needs_data/needs_mapping, expune motivul (nu doar status).
# Raspuns onest si la reactivare: daca re-clasificarea cade pe
# needs_data/needs_mapping, expune motivul (nu doar status).
results.append(_rezultat_enqueue(existing["id"], cl, reactivated=True))
continue
# Cursa: alt POST/requeue a schimbat starea intre SELECT si UPDATE
@@ -234,7 +229,7 @@ def create_prezentari(
)
continue
# Helper pur partajat cu dry-run (PRD 5.2): reproduce EXACT clasificarea
# Helper pur partajat cu dry-run: reproduce EXACT clasificarea
# (canonicalize + mapare op->cod + validare + auto_send gate).
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
if cl["blocked_error"]:
@@ -247,13 +242,12 @@ def create_prezentari(
(key, acct, cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], creds_enc),
)
sub_id = int(cur.lastrowid)
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, sub_id, cl["resolved"])
# Raspuns onest (PRD 5.7): pe needs_data/needs_mapping expune erori/nemapate/motiv.
# Raspuns onest: pe needs_data/needs_mapping expune erori/nemapate/motiv.
results.append(_rezultat_enqueue(sub_id, cl))
# US-004: audit cerere API per cont. Doar metadate (count + distributie status),
# NICIUN camp de payload PII integral. Reuse conn (T4 — fara contentie WAL).
# Audit cerere API per cont. Doar metadate (count + distributie status),
# NICIUN camp de payload PII integral. Reuse conn (fara contentie WAL).
dist: dict[str, int] = {}
for r in results:
if r.reactivated:
@@ -284,7 +278,7 @@ def valideaza_prezentari(
Intoarce pentru fiecare prezentare: verdictul (status_estimat), erorile de
continut si codurile nemapate — exact ce ar obtine trimiterea reala pe acelasi
payload + aceeasi mapare de cont. rar_credentials ignorat complet (PRD 5.2).
payload + aceeasi mapare de cont. rar_credentials ignorat complet.
"""
acct = account_or_default(account_id)
conn = get_connection()
@@ -301,7 +295,7 @@ def valideaza_prezentari(
res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
if res["blocked_error"]:
res = {**res, "status": "error"}
# US-003: imbogatim fiecare element nemapat cu 3 niveluri COD_NEMAPAT
# Imbogatim fiecare element nemapat cu 3 niveluri COD_NEMAPAT
nemapate = [
{**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} fara mapare RAR")}
for u in res["unmapped"]
@@ -329,7 +323,7 @@ def list_prezentari(
try:
scope_sql, scope_params = account_scope_clause(account_id)
# payload_json e plaintext (vezi submissions.payload_json); il citim doar ca
# sa derivam campurile afisabile prin helper-ul partajat (US-003, DRY), nu il expunem.
# sa derivam campurile afisabile prin helper-ul partajat, nu il expunem.
cols = (
"id, status, id_prezentare, rar_status_code, retry_count, "
"created_at, updated_at, payload_json"
@@ -357,13 +351,13 @@ def list_prezentari(
conn.close()
# Campuri expuse de GET /v1/prezentari/{id} — allowlist explicita (B4).
# Exclude: rar_creds_enc, payload_json, idempotency_key, rar_error, sending_since.
# Campuri expuse de GET /v1/prezentari/{id} — allowlist explicita.
# Exclude: rar_creds_enc, payload_json, idempotency_key, sending_since.
_PREZENTARE_FIELDS = frozenset({
"id", "status", "id_prezentare", "rar_status_code", "retry_count",
"next_attempt_at", "created_at", "updated_at", "account_id",
"batch_id", "row_index", "purge_after",
# T9: rar_error e SIGUR de expus — contine doar coduri/mesaje de validare RAR si
# rar_error e SIGUR de expus — contine doar coduri/mesaje de validare RAR si
# erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza
# "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API.
"rar_error",
@@ -383,7 +377,7 @@ def get_prezentare(
[submission_id] + scope_params,
).fetchone()
if not row:
# B3: acelasi mesaj indiferent daca randul exista dar apartine altui cont
# Acelasi mesaj indiferent daca randul exista dar apartine altui cont
# sau nu exista deloc — nu confirmam existenta.
raise HTTPException(status_code=404, detail="submission inexistent")
row_dict = dict(row)
@@ -397,11 +391,11 @@ def delete_prezentare(
submission_id: int,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Sterge o trimitere blocata a contului cheii API (US-010).
"""Sterge o trimitere blocata a contului cheii API.
Raspuns 200 + body JSON (NU 204 — clienti VFP fac string-parse). Scope evaluat
INAINTEA starii (decizie /autoplan #20): cross-account / inexistent -> 404 (acelasi
mesaj, B3); own-account `sent`/`sending` -> 409 (conflict de stare).
INAINTEA starii: cross-account / inexistent -> 404 (acelasi mesaj);
own-account `sent`/`sending` -> 409 (conflict de stare).
"""
conn = get_connection()
try:
@@ -424,10 +418,10 @@ def repune_prezentare(
submission_id: int,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Re-pune in coada o trimitere blocata a contului cheii API (US-010).
"""Re-pune in coada o trimitere blocata a contului cheii API.
`error -> queued` (peste helper US-009), re-ruleaza classify. Acelasi oracol de
scope/stare ca DELETE (404 cross-account/inexistent, 409 sent/sending).
`error -> queued`, re-ruleaza classify. Acelasi oracol de scope/stare ca DELETE
(404 cross-account/inexistent, 409 sent/sending).
"""
conn = get_connection()
try:
@@ -478,8 +472,7 @@ def _audit_rows(conn, date_from: str | None, date_to: str | None, status: str, a
"""Randuri audit filtrate pe cont + data(updated_at) in [from, to].
account_id = contul cheii API (scope obligatoriu — PII in CSV). Randuri cu
account_id IS NULL apartin contului 1 (legacy/OV-2). payload_json e text in
schelet; b64_image NU intra in CSV.
account_id IS NULL apartin contului 1. b64_image NU intra in CSV.
"""
scope_sql, scope_params = account_scope_clause(account_id)
sql = (
@@ -514,7 +507,7 @@ def _audit_rows(conn, date_from: str | None, date_to: str | None, status: str, a
"submission_id": r["id"],
"status": r["status"],
"id_prezentare": r["id_prezentare"] or "",
# NULL→cont 1 (OV-2): coloana reflecta invariantul de scope, nu "" ambiguu.
# NULL→cont 1: coloana reflecta invariantul de scope, nu "" ambiguu.
"account_id": account_or_default(r["account_id"]),
"vin": p.get("vin") or "",
"nr_inmatriculare": p.get("nr_inmatriculare") or "",
@@ -539,7 +532,7 @@ def audit_export(
pe data(updated_at). `status` implicit `sent` (ce a ajuns efectiv la RAR);
`status=all` exporta toata coada contului. Leaga retinerea 90 zile prin coloana
`purge_after` (plan.md sect. 4 + 8). b64_image nu se exporta.
`purge_after`. b64_image nu se exporta.
"""
conn = get_connection()
try:
@@ -568,7 +561,7 @@ def get_mapari(
"""Maparile operatie->cod ale contului curent.
Parametrul `account_id` din query e pastrat pentru compatibilitate, dar contul
efectiv vine MEREU din cheia API (TD-3.2). Daca e prezent si difera -> 400.
efectiv vine MEREU din cheia API. Daca e prezent si difera -> 400.
"""
if account_id is not None and account_id != key_account:
raise HTTPException(
@@ -635,7 +628,7 @@ def create_mapare(
class RarCredsIn(BaseModel):
"""Creds RAR durabile per-cont (D4). Stocate criptate (Fernet) in accounts.rar_creds_enc."""
"""Creds RAR durabile per-cont. Stocate criptate (Fernet) in accounts.rar_creds_enc."""
email: str = Field(..., min_length=1)
password: str = Field(..., min_length=1, repr=False)
@@ -646,7 +639,7 @@ def set_rar_creds(
req: RarCredsIn,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Seteaza creds RAR durabile per-cont (D4/T1).
"""Seteaza creds RAR durabile per-cont.
Criptate Fernet in accounts.rar_creds_enc. Worker-ul le foloseste ca fallback
cand submission-ul nu mai are creds (canal web fara re-pusher, restart worker).