Files
rar-autopass/app/idempotency.py
Claude Agent 4a2afc68bf 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>
2026-06-25 21:44:24 +00:00

124 lines
5.0 KiB
Python

"""Cheie de idempotenta = hash de continut canonic.
RAR nu are camp nr. comanda si accepta duplicate -> dedup-ul e in sarcina noastra.
Hash stabil peste o reprezentare canonica a prezentarii.
canonicalize_row + build_key sunt helpere publice partajate intre canalul API si
canalul import:
- canonicalize_row: normeaza VIN/nr/odometru (strip ".0" Excel coercion) INAINTE
de validare si INAINTE de cheie.
- build_key: aplica account_or_default INAINTE de hash (None si 1 => o cheie).
Altfel acelasi rand logic din canale diferite (account_id None pe canalul API,
1 pe import) ar primi chei diferite -> al doilea FINALIZATA duplicat.
Randuri vechi cu cheie-None nu sunt gasite de build_key nou: dual-lookup la
already_sent (cheia noua, apoi build_key_legacy) sau recompute-keys o singura data.
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
def _op_identity(p: Any) -> str:
"""Cod RAR (normalizat) daca exista, altfel codul intern ROAAUTO."""
get = p.get if isinstance(p, dict) else (lambda k, d=None: getattr(p, k, d))
cod = (get("cod_prestatie", "") or "").strip().upper()
if cod:
return cod
return (get("cod_op_service", "") or "").strip()
def canonicalize_row(raw: dict[str, Any]) -> dict[str, Any]:
"""Normalizare canonica a unui rand brut. Apelata INAINTE de validare si de build_key.
- VIN, nr_inmatriculare: strip + upper.
- odometru_final: strip ".0" (Excel coercion numeric 123456.0 -> "123456").
Necesar ca validation._parse_int (isdigit()) sa nu respinga float-string.
- data_prestatie: strip (normalizarea la YYYY-MM-DD se face in parser).
- prestatii: pastrate ca-atare (rezolvarea e in resolve_prestatii).
"""
vin = (raw.get("vin") or "").strip().upper()
nr = (raw.get("nr_inmatriculare") or "").strip().upper()
# Odometru: strip ".0" Excel float coercion
odo_raw = raw.get("odometru_final")
if odo_raw is not None:
odo_s = str(odo_raw).strip()
# "123456.0" -> "123456"; "123456.50" nu (nu e coercion Excel pur)
if "." in odo_s:
before, after = odo_s.split(".", 1)
if after == "0" and before.lstrip("-").isdigit():
odo_s = before
else:
odo_s = ""
# Data (pastrata ca string; parsarea la YYYY-MM-DD e in parser)
data = str(raw.get("data_prestatie") or "").strip()
# Prestatii (copie superficiala; rezolvarea e upstream)
prestatii = list(raw.get("prestatii") or [])
return {
"vin": vin,
"nr_inmatriculare": nr,
"data_prestatie": data,
"odometru_final": odo_s,
"prestatii": prestatii,
}
def build_key(account_id: int | None, canon: dict[str, Any]) -> str:
"""SHA-256 partajat canal-API + canal-import.
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.
"""
# Import local ca sa evitam import circular (mapping importa din idempotency via validator)
from .mapping import account_or_default
acct = account_or_default(account_id)
canonic = {
"account_id": acct,
"vin": canon.get("vin", ""),
"nr_inmatriculare": canon.get("nr_inmatriculare", ""),
"data_prestatie": canon.get("data_prestatie"),
"odometru_final": canon.get("odometru_final", ""),
"prestatii": sorted(_op_identity(p) for p in (canon.get("prestatii") or [])),
}
blob = json.dumps(canonic, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
def idempotency_key(account_id: int | None, prezentare: dict[str, Any]) -> str:
"""SHA-256 peste (account_id + campurile semnificative ale prezentarii).
Wrapper backward-compat peste canonicalize_row + build_key.
Exclude obs si b64Image (cosmetice, nu definesc unicitatea declaratiei).
NOTA: account_id=None si account_id=1 produc ACEEASI cheie (via
account_or_default in build_key). Randuri vechi cu cheie-None nu sunt
acoperite automat — dual-lookup sau recompute-keys la migrare productie.
"""
canon = canonicalize_row(prezentare)
return build_key(account_id, canon)
def build_key_legacy(account_id: int | None, prezentare: dict[str, Any]) -> str:
"""Cheia in formatul vechi (account_id AS-PASSED, fara canonicalize).
Folosita EXCLUSIV pentru dual-lookup la already_sent pe DB cu randuri vechi.
Nu folosi pentru randuri noi.
"""
canonic = {
"account_id": account_id,
"vin": (prezentare.get("vin") or "").strip().upper(),
"nr_inmatriculare": (prezentare.get("nr_inmatriculare") or "").strip().upper(),
"data_prestatie": prezentare.get("data_prestatie"),
"odometru_final": str(prezentare.get("odometru_final") or "").strip(),
"prestatii": sorted(_op_identity(p) for p in (prezentare.get("prestatii") or [])),
}
blob = json.dumps(canonic, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256(blob.encode("utf-8")).hexdigest()