Files
rar-autopass/app/submissions_admin.py
Claude Agent 458c3fd43c fix(5.19): re-snapshot held pe caile de re-punere in coada
Finding #1 (/code-review high): create_prezentari (reactivare) si reresolve_account
re-calculau `held` din comutatorul contului la tranzitia -> queued, dar caile de
re-punere din dashboard/admin lasau `held` pe valoarea VECHE:
  - submissions_admin.requeue_submission (API /repune + web)
  - web post_corectie_trimitere
  - web post_repune_trimitere (calea cod_prestatie)
  - web post_bulk_fix

Consecinta: un rand ingerat pe Auto ON (held=0) care esueaza, apoi contul trecut pe
Auto OFF, la re-punere pastra held=0 -> worker-ul (claim_one AND held=0) il auto-trimitea
la RAR (FINALIZATA ireversibil) desi contul e Auto OFF. Directia inversa: rand held=1
repus pe cont trecut Auto ON ramanea blocat.

Fix: held=held_for_account(conn, account_or_default(account_id)) pe toate cele 4 UPDATE-uri
-> queued (paritate cu caile deja corecte). Bulk-fix hoisteaza snapshot-ul o data inainte
de bucla. Worker requeue_with_backoff neatins (opereaza doar pe randuri deja claim-uite,
held=0 -> corect).

Test: tests/test_held_requeue_snapshot.py (ambele directii). 1535 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 10:12:57 +00:00

125 lines
4.9 KiB
Python

"""Lifecycle trimiteri blocate: sterge / re-pune in coada.
Inchide lacuna: un rand `error` (creds RAR gresite) ar ramane altfel permanent si
nereparabil. Aceste helpere adauga DOUA tranzitii controlate — stergere de randuri
ne-sent si `blocate -> queued` (re-clasificat) — fara a atinge logica de trimitere
a worker-ului.
Invariante:
- Opereaza DOAR pe `error`/`needs_data`/`needs_mapping`. `sent` (dovada de trimitere
la RAR, audit) si `sending` (lease worker in zbor) sunt INTERZISE.
- Scope-ul (apartenenta la cont) se evalueaza INAINTEA starii: un rand inexistent SAU
al altui cont -> SubmissionNotFound (404, nu confirmam existenta). Doar pe randuri
proprii in stare gresita -> SubmissionStateConflict (409).
- Ambele emit eveniment in jurnal: `submission_sters` / `submission_repus`.
Functii cu `conn` (persistenta).
"""
from __future__ import annotations
import json
from .accounts import held_for_account
from .mapping import (
account_or_default,
account_scope_clause,
classify_prezentare,
load_mapping_meta,
load_nomenclator_codes,
)
from .observ import log_event
# Stari pe care le putem sterge / re-pune in coada (ne-sent, ne-in-zbor).
_GESTIONABILE = ("error", "needs_data", "needs_mapping")
class SubmissionNotFound(Exception):
"""Randul nu exista SAU apartine altui cont (acelasi mesaj — nu confirmam existenta)."""
class SubmissionStateConflict(Exception):
"""Randul exista si e al contului, dar e intr-o stare protejata (sent/sending)."""
def __init__(self, status: str):
super().__init__(f"stare protejata: {status}")
self.status = status
def _fetch_scoped(conn, account_id: int, sid: int):
scope_sql, scope_params = account_scope_clause(account_id)
return conn.execute(
f"SELECT * FROM submissions WHERE id=? AND {scope_sql}",
[sid] + scope_params,
).fetchone()
def delete_submission(conn, account_id: int, sid: int) -> dict:
"""Sterge un rand ne-sent al contului. Ridica SubmissionNotFound / SubmissionStateConflict.
Intoarce {"submission_id", "status_anterior"} la succes.
"""
row = _fetch_scoped(conn, account_id, sid)
if row is None:
raise SubmissionNotFound()
status = row["status"]
if status not in _GESTIONABILE:
raise SubmissionStateConflict(status)
conn.execute("DELETE FROM submissions WHERE id=?", (sid,))
log_event(
"submission_sters",
account_id=account_or_default(account_id),
mesaj=f"trimitere #{sid} stearsa din {status}",
context={"submission_id": sid, "status_anterior": status},
conn=conn,
)
return {"submission_id": sid, "status_anterior": status}
def requeue_submission(conn, account_id: int, sid: int) -> dict:
"""Re-pune in coada un rand blocat al contului: re-ruleaza classify pe payload.
`error -> queued` (cand continutul e valid) sau ramane `needs_data`/`needs_mapping`
daca clasificarea o cere. Reseteaza retry_count/next_attempt_at/sending_since si
CURATA `purge_after` (randul redevine activ, nu mai e candidat la purjare).
Ridica SubmissionNotFound / SubmissionStateConflict. Intoarce
{"submission_id", "status_anterior", "status_nou"}.
"""
row = _fetch_scoped(conn, account_id, sid)
if row is None:
raise SubmissionNotFound()
status = row["status"]
if status not in _GESTIONABILE:
raise SubmissionStateConflict(status)
try:
content = json.loads(row["payload_json"]) if row["payload_json"] else {}
if not isinstance(content, dict):
content = {}
except (ValueError, TypeError):
content = {}
mapping_meta = load_mapping_meta(conn, account_id)
mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()}
valid_codes = load_nomenclator_codes(conn) or None
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes)
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la re-punere in coada
# (paritate cu create_prezentari/reresolve_account). Fara asta un rand repus pastra
# `held` VECHI si s-ar auto-trimite desi contul e Auto OFF (sau ar ramane blocat cand e ON).
held_val = held_for_account(conn, account_or_default(account_id))
conn.execute(
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, retry_count=0, "
"next_attempt_at=NULL, sending_since=NULL, purge_after=NULL, held=?, updated_at=datetime('now') "
"WHERE id=?",
(cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], held_val, sid),
)
log_event(
"submission_repus",
account_id=account_or_default(account_id),
mesaj=f"trimitere #{sid} re-pusa: {status} -> {cl['status']}",
context={"submission_id": sid, "status_anterior": status, "status_nou": cl["status"]},
conn=conn,
)
return {"submission_id": sid, "status_anterior": status, "status_nou": cl["status"]}