feat(api): filtru perioada data_de/data_pana pe GET /v1/prezentari

data_prestatie traieste in payload_json, nu e coloana SQL: filtrarea se aplica
in Python dupa derivarea prezentarii (fetch scope+status fara LIMIT, filtreaza,
apoi LIMIT dupa filtrare), acelasi pattern deja folosit de dashboard-ul web
(/_fragments/submissions). Extrage helper-ul iso_date_prefix in payload_view.py
ca sa fie partajat intre canalul web si cel API (elimina duplicarea).

Format invalid pe data_de/data_pana -> 422. Fara parametri, comportamentul
raspunsului ramane identic cu inainte.
This commit is contained in:
2026-07-10 12:43:37 +03:00
committed by Claude Agent
parent cc8c712ed2
commit 1091745789
4 changed files with 235 additions and 27 deletions

View File

@@ -51,7 +51,7 @@ from ...models import (
ValidareResult,
)
from ...observ import log_event
from ...payload_view import prezentare_din_payload
from ...payload_view import iso_date_prefix, prezentare_din_payload
from ...submissions_admin import (
SubmissionNotFound,
SubmissionStateConflict,
@@ -503,12 +503,35 @@ def valideaza_prezentari(
return ValidareResponse(results=results)
def _valida_data_filtru(nume_param: str, valoare: str | None) -> str | None:
"""Valideaza formatul YYYY-MM-DD al unui parametru de filtrare pe perioada.
None/gol -> None (filtru inactiv). Format invalid -> 422, consecvent cu restul
rutelor API (vezi celelalte `status_code=422` din acest fisier).
"""
v = (valoare or "").strip()
if not v:
return None
try:
datetime.strptime(v, "%Y-%m-%d")
except ValueError:
raise HTTPException(
status_code=422,
detail=f"{nume_param} trebuie sa fie in format YYYY-MM-DD",
)
return v
@router.get("/prezentari")
def list_prezentari(
status: str | None = None,
data_de: str | None = None,
data_pana: str | None = None,
limit: int = 100,
account_id: int = Depends(resolve_account_id),
) -> dict:
data_de = _valida_data_filtru("data_de", data_de)
data_pana = _valida_data_filtru("data_pana", data_pana)
conn = get_connection()
try:
scope_sql, scope_params = account_scope_clause(account_id)
@@ -519,24 +542,49 @@ def list_prezentari(
"id, status, id_prezentare, rar_status_code, retry_count, "
"created_at, updated_at, payload_json, rar_env, held"
)
where = [scope_sql]
params: list = list(scope_params)
if status:
where.append("status=?")
params.append(status)
where_sql = " AND ".join(where)
# data_prestatie traieste in payload_json, nu e coloana SQL (vezi
# payload_view.prezentare_din_payload) — nu poate intra in clauza WHERE.
# Cu filtru de perioada activ: fetch scope+status FARA LIMIT, filtreaza in
# Python dupa derivarea prezentarii, apoi LIMIT-eaza DUPA filtrare (altfel
# LIMIT SQL ar trunchia gresit inainte de filtru). Acelasi pattern ca
# /_fragments/submissions (dashboard web, app/web/routes.py).
filtru_perioada = bool(data_de or data_pana)
if filtru_perioada:
rows = conn.execute(
f"SELECT {cols} FROM submissions WHERE {scope_sql} AND status=? "
f"ORDER BY id DESC LIMIT ?",
scope_params + [status, limit],
f"SELECT {cols} FROM submissions WHERE {where_sql} ORDER BY id DESC",
params,
).fetchall()
else:
rows = conn.execute(
f"SELECT {cols} FROM submissions WHERE {scope_sql} ORDER BY id DESC LIMIT ?",
scope_params + [limit],
f"SELECT {cols} FROM submissions WHERE {where_sql} ORDER BY id DESC LIMIT ?",
params + [limit],
).fetchall()
out = []
for r in rows:
d = dict(r)
# Campuri afisabile derivate din payload (acelasi helper ca dashboardul web);
# payload_json brut nu se intoarce in raspuns.
d["prezentare"] = prezentare_din_payload(d.pop("payload_json", None))
prezentare = prezentare_din_payload(d.pop("payload_json", None))
if filtru_perioada:
d_prefix = iso_date_prefix(prezentare["data_prestatie"])
if d_prefix is None:
continue
if data_de and d_prefix < data_de:
continue
if data_pana and d_prefix > data_pana:
continue
d["prezentare"] = prezentare
out.append(d)
if filtru_perioada and len(out) >= limit:
break
return {"submissions": out}
finally:
conn.close()

View File

@@ -13,11 +13,34 @@ la em-dash. Citeste cheile tolerant (canalele API si import pot diferi usor:
from __future__ import annotations
import json
from datetime import datetime
from typing import Any
EMPTY = ""
def iso_date_prefix(value: Any) -> str | None:
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
fara a exclude timestamp-urile. Valori care nu incep cu o data ISO valida
(ex. '05.12.2024') intorc None si sunt excluse din filtru.
Partajat intre canalul web (dashboard Trimiteri, filtrul data_de/data_pana) si
canalul API (`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua.
"""
s = str(value or "").strip()
if len(s) < 10:
return None
prefix = s[:10]
try:
datetime.strptime(prefix, "%Y-%m-%d")
return prefix
except (ValueError, TypeError):
return None
def _clean_str(value: Any) -> str:
"""str() defensiv: None/'' -> '', altfel string strip-uit (coercion Excel safe)."""
if value is None:

View File

@@ -27,7 +27,7 @@ from .. import __version__
from .. import errors as _errors
from ..auth import rotate_api_key
from ..plans import effective_tier as _eff_tier, monthly_usage as _monthly_usage, PLANS as _PLANS
from ..payload_view import prezentare_din_payload
from ..payload_view import iso_date_prefix as _iso_date_prefix, prezentare_din_payload
from ..web.csrf import get_csrf_token, verify_csrf
from .labels import (
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
@@ -1100,25 +1100,6 @@ def fragment_trimiteri_versiune(request: Request) -> JSONResponse:
conn.close()
def _iso_date_prefix(value: object) -> str | None:
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
fara a exclude timestamp-urile. Valori care nu incep cu o data ISO valida
(ex. '05.12.2024') intorc None si sunt excluse din filtru.
"""
s = str(value or "").strip()
if len(s) < 10:
return None
prefix = s[:10]
try:
datetime.strptime(prefix, "%Y-%m-%d")
return prefix
except (ValueError, TypeError):
return None
# Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana
# scurta de pe rand e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
_STARI_CU_PROBLEMA = ("error", "needs_data", "needs_mapping")