feat(api): scope pe cont la GET-urile de listare /v1/* (PRD 3.2)
Inchide scurgerea cross-account pe GET /v1/prezentari(/{id}),
/v1/mapari(/pending) si /v1/audit/export. Toate primesc
Depends(resolve_account_id) + account_scope_clause (regula NULL->cont 1,
OV-2). Nomenclatorul ramane global (referinta partajata, fara PII).
- B3: 404 cross-account byte-identic cu 404 inexistent (fara oracol enumerare)
- B4: get_prezentare cu allowlist de campuri (nu mai expune rar_creds_enc/
payload_json/idempotency_key/rar_error)
- B1: pending_unmapped filtreaza in SQL; default None = global doar pentru web
- B2: helper account_scope_clause (DRY, doar pe submissions nullable)
- B5: index idx_submissions_account_status
- B8: regula de scope documentata in api-rar-contract.md
- TD-3.2: ?account_id != contul cheii -> 400
14 teste noi (cross-account, legacy NULL, B3, B4); suita 313 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,19 @@ def account_or_default(account_id: int | None) -> int:
|
||||
return account_id if account_id is not None else DEFAULT_ACCOUNT_ID
|
||||
|
||||
|
||||
def account_scope_clause(account_id: int) -> tuple[str, list]:
|
||||
"""Fragment SQL + params pentru filtrarea pe cont in tabele cu account_id nullable.
|
||||
|
||||
Aplica regula: NULL apartine contului 1 (legacy/OV-2).
|
||||
Foloseste DOAR pe submissions (account_id NULLABLE).
|
||||
NU folosi pe operations_mapping (account_id NOT NULL) — acolo WHERE account_id=? simplu.
|
||||
"""
|
||||
return (
|
||||
"(account_id = ? OR (account_id IS NULL AND ? = 1))",
|
||||
[account_id, account_id],
|
||||
)
|
||||
|
||||
|
||||
def seed_nomenclator_if_empty(conn) -> int:
|
||||
"""Seed fallback (18 coduri din contract) DOAR daca nomenclator_rar e gol.
|
||||
|
||||
@@ -217,18 +230,28 @@ def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> boo
|
||||
return False
|
||||
|
||||
|
||||
def pending_unmapped(conn) -> list[dict]:
|
||||
def pending_unmapped(conn, account_id=None) -> list[dict]:
|
||||
"""Operatii distincte nemapate, agregate din submission-urile `needs_mapping`.
|
||||
|
||||
Pentru fiecare (account_id, cod_op_service) intoarce o denumire reprezentativa,
|
||||
nr. de submission-uri blocate si sugestiile fuzzy pe nomenclator. Sursa de
|
||||
adevar = payload_json (nu o tabela separata): un item nemapat are cod_prestatie
|
||||
null + cod_op_service setat.
|
||||
account_id=None (default): global — intentionat pentru web/routes.py (back-compat).
|
||||
Apelantii noi din API TREBUIE sa paseze account_id explicit; None global e
|
||||
footgun (scurge cross-account) si e rezervat exclusiv pentru dashboard-ul intern.
|
||||
|
||||
account_id=int: filtreaza in SQL pe cont inclusiv randuri legacy (account_id IS NULL
|
||||
apartine contului 1, OV-2). Filtrarea in SQL, nu post-hoc in Python.
|
||||
"""
|
||||
nomenclator = load_nomenclator(conn)
|
||||
rows = conn.execute(
|
||||
"SELECT id, account_id, payload_json FROM submissions WHERE status='needs_mapping'"
|
||||
).fetchall()
|
||||
if account_id is not None:
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
rows = conn.execute(
|
||||
f"SELECT id, account_id, payload_json FROM submissions "
|
||||
f"WHERE status='needs_mapping' AND {scope_sql}",
|
||||
scope_params,
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id, account_id, payload_json FROM submissions WHERE status='needs_mapping'"
|
||||
).fetchall()
|
||||
|
||||
agg: dict[tuple[int, str], dict[str, Any]] = {}
|
||||
for r in rows:
|
||||
|
||||
Reference in New Issue
Block a user