Files
rar-autopass/app/errors.py
Claude Agent c842e3352a feat(5.6): observabilitate + jurnal aplicatie + lifecycle trimiteri blocate
Implementeaza PRD 5.6 complet (14 stories, TDD). Doua axe:

Lifecycle trimiteri blocate (Val A):
- submissions_admin.py: sterge/repune scoped (404 cross-account inaintea lui 409 stare)
- reactivare dedup peste `error` cu CAS (WHERE id=? AND status='error'), creds noi in
  submissions + accounts.rar_creds_enc; worker invalideaza sesiunea RAR la creds proaspete
  (JWT 30h vechi nu mai trimite cu parola gresita); camp aditiv `reactivated:true`
- retentie randuri blocate 30z; purge_expired exclude queued/sending; purge_after curatat
  la reactivare/requeue
- API DELETE /v1/prezentari/{id} + /repune (200+JSON); UI butoane + bulk + banner actionabil

Observabilitate:
- app/observ.py log_event: dublu canal app_events (DB) + RotatingFileHandler per-proces,
  redactare creds/PII la scriere (redact_pii/vin_partial)
- request_id middleware + X-Request-ID pe toate raspunsurile
- handler global excepții -> 500 envelope 6-chei + request_id (traceback doar in jurnal)
- audit cerere API (api_prezentari/api_auth_esuat) + audit worker (rar_login/tranzitii)
- tab "Jurnal" filtrabil scoped (non-admin doar contul sau); retentie jurnal 90z
- rar_error expus in GET /v1/prezentari/{id} (recovery observabil)

pytest -q: 741 passed, 0 failed. Docs: PRD raport VERIFY, contract endpointuri noi, ROADMAP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:45:39 +00:00

212 lines
7.0 KiB
Python

"""Catalog central de erori AutoPass (PRD 5.4).
Singura sursa de adevar care mapeaza fiecare cod de eroare la (problema, fix),
cu un helper care construieste obiectul de eroare pe 3 niveluri:
- nivel 1 (tehnic): `cod` + `cauza` — ce s-a intamplat exact
- nivel 2 (utilizator): `problema` — descriere scurta, inteligibila
- nivel 3 (actiune): `fix` — ce trebuie facut pentru a remedia
Modul PUR — fara import DB sau HTTP.
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# CATALOG
# cheie = cod (string), valoare = {"problema": str, "fix": str}
# ---------------------------------------------------------------------------
CATALOG: dict[str, dict[str, str]] = {
"VIN_FORMAT": {
"problema": "VIN invalid",
"fix": (
"Verifica VIN-ul pe talon (pozitia E) sau pe caroserie: exact 17 caractere"
" majuscule, fara spatii si fara literele O, I, Q."
),
},
"NR_INMATRICULARE_FORMAT": {
"problema": "Numar de inmatriculare invalid",
"fix": (
"Foloseste doar litere si cifre majuscule, maxim 10 caractere, fara spatii"
" sau cratima (ex. B123ABC)."
),
},
"DATA_FORMAT": {
"problema": "Data prestatiei in format gresit",
"fix": "Scrie data ca AAAA-LL-ZZ (ex. 2026-06-22).",
},
"DATA_PREA_VECHE": {
"problema": "Data prestatiei prea veche",
"fix": (
"RAR accepta prestatii doar incepand cu 01.12.2024;"
" verifica data prestatiei."
),
},
"DATA_VIITOR": {
"problema": "Data prestatiei in viitor",
"fix": "Data prestatiei nu poate fi dupa ziua de azi; corecteaza data.",
},
"ODOMETRU_FINAL_FORMAT": {
"problema": "Odometru final invalid",
"fix": (
"Scrie kilometrajul final ca numar intreg, fara zecimale sau text"
" (ex. 145000)."
),
},
"ODOMETRU_INITIAL_LIPSA": {
"problema": "Lipseste odometrul initial",
"fix": (
"Prestatiile R-ODO / I-ODO cer kilometrajul initial; completeaza-l."
),
},
"ODOMETRU_INITIAL_FORMAT": {
"problema": "Odometru initial invalid",
"fix": (
"Scrie kilometrajul initial ca numar intreg, fara zecimale sau text."
),
},
"ODOMETRU_INITIAL_ORDINE": {
"problema": "Odometru initial mai mare decat finalul",
"fix": (
"Kilometrajul initial trebuie sa fie mai mic sau egal cu cel final;"
" verifica cele doua valori."
),
},
"PRESTATII_GOALE": {
"problema": "Nicio prestatie",
"fix": "Adauga cel putin o prestatie cu cod RAR valid.",
},
"B64_INVALID": {
"problema": "Imaginea nu este base64 valid",
"fix": (
"Trimite imaginea codata base64 corect, sau omite campul daca nu ai imagine."
),
},
"COD_NEMAPAT": {
"problema": "Lipseste codul RAR al operatiei",
"fix": (
"Alege codul RAR pentru aceasta operatie in tab-ul Mapari"
" (ai sugestii automate)."
),
},
"AUTO_SEND_OPRIT": {
"problema": "Necesita confirmare manuala",
"fix": (
"Codul e mapat cu trimitere automata oprita; verifica randul si"
" pune-l manual in coada."
),
},
"RAR_VALIDARE": {
"problema": "RAR a respins prezentarea",
"fix": (
"Corecteaza campul semnalat de RAR (vezi cauza) si reincearca;"
" detaliile exacte sunt in mesajul tehnic RAR."
),
},
"RAR_CREDS_INVALIDE": {
"problema": "Credentiale RAR invalide",
"fix": (
"Verifica email-ul si parola contului RAR in tab-ul Cont;"
" trimiterea nu se reincearca automat la credentiale gresite."
),
},
"IMPORT_FISIER_PREA_MARE": {
"problema": "Fisier prea mare",
"fix": (
"Imparte fisierul in bucati de maxim 5000 de randuri si incarca-le pe rand."
),
},
"IMPORT_ANTET_NECLAR": {
"problema": "Antet de coloane neclar",
"fix": (
"Asigura-te ca primul rand contine numele coloanelor"
" (ex. VIN, Numar, Data)."
),
},
"IMPORT_ENCODING": {
"problema": "Codare de caractere nesuportata",
"fix": "Salveaza fisierul ca CSV UTF-8 (sau xlsx) si reincarca.",
},
"IMPORT_FISIER_NERECUNOSCUT": {
"problema": "Fisier nerecunoscut",
"fix": "Incarca un fisier .xlsx sau .csv valid.",
},
"IMPORT_MULTIPLE_SHEETS": {
"problema": "Mai multe foi in fisier",
"fix": "Pastreaza datele intr-o singura foaie sau alege foaia de import.",
},
"IMPORT_FARA_MAPARE_COLOANE": {
"problema": "Coloanele nu sunt mapate",
"fix": (
"Mapeaza intai coloanele fisierului la campurile cerute, apoi continua."
),
},
"IMPORT_CONFIRMARE_GRESITA": {
"problema": "Numar confirmat gresit",
"fix": (
"Numarul confirmat difera de randurile gata de trimis;"
" verifica preview-ul si reconfirma."
),
},
"IMPORT_OVERRIDE_ILIZIBIL": {
"problema": "Editarea anterioara nu se poate citi",
"fix": (
"Editarea salvata este ilizibila (probabil cheia s-a schimbat);"
" reediteaza randul."
),
},
"COLOANE_FORMAT_JSON": {
"problema": "Format de coloane (JSON) invalid",
"fix": (
"Verifica sintaxa JSON a maparii de coloane"
" (ghilimele duble, acolade inchise corect)."
),
},
"EROARE_INTERNA": {
"problema": "Eroare interna a gateway-ului",
"fix": (
"Nu e o problema de date trimise de tine. Reincearca peste cateva"
" momente; daca persista, contacteaza administratorul cu identificatorul"
" cererii (request_id) afisat."
),
},
}
# ---------------------------------------------------------------------------
# eroare()
# ---------------------------------------------------------------------------
def eroare(
cod: str,
*,
field: str | None = None,
cauza: str | None = None,
) -> dict:
"""Construieste un obiect de eroare pe 3 niveluri din CATALOG.
Parametri
---------
cod: Codul de eroare (cheie in CATALOG). Ridica KeyError daca absent.
field: Campul care a generat eroarea (optional, pentru context).
cauza: Descrierea tehnica a erorii concrete (optional).
Daca lipseste, `cauza` si `message` preiau valoarea `problema` din catalog.
Returneaza
----------
dict cu exact cheile: field, cod, problema, cauza, fix, message.
"""
entry = CATALOG[cod] # ridica KeyError daca cod absent
problema = entry["problema"]
fix = entry["fix"]
cauza_efectiva = cauza if cauza is not None else problema
message = cauza if cauza is not None else problema
return {
"field": field,
"cod": cod,
"problema": problema,
"cauza": cauza_efectiva,
"fix": fix,
"message": message,
}