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>
This commit is contained in:
Claude Agent
2026-07-06 10:12:57 +00:00
parent 3b5cf7a7d9
commit 458c3fd43c
4 changed files with 117 additions and 9 deletions

View File

@@ -53,7 +53,7 @@ from ..api.v1.import_router import (
apply_row_override,
EDIT_FIELDS,
)
from ..accounts import get_auto_send, set_auto_send
from ..accounts import get_auto_send, held_for_account, set_auto_send
from ..config import get_settings
from ..crypto import decrypt_creds, encrypt_creds
from ..db import get_connection, read_app_events, read_heartbeat
@@ -1746,11 +1746,12 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
)
try:
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la re-punere in coada.
conn.execute(
"UPDATE submissions SET idempotency_key=?, status='queued', payload_json=?, "
"rar_error=NULL, retry_count=0, next_attempt_at=datetime('now'), "
"updated_at=datetime('now') WHERE id=?",
(new_key, payload_json, row["id"]),
"held=?, updated_at=datetime('now') WHERE id=?",
(new_key, payload_json, held_for_account(conn, account_or_default(account_id)), row["id"]),
)
except sqlite3.IntegrityError:
# Plasa de siguranta pentru cursa TOCTOU pe UNIQUE(idempotency_key):
@@ -1883,11 +1884,12 @@ async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLRes
),
)
try:
# PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la re-punere in coada.
conn.execute(
"UPDATE submissions SET idempotency_key=?, status='queued', payload_json=?, "
"rar_error=NULL, retry_count=0, next_attempt_at=datetime('now'), "
"updated_at=datetime('now') WHERE id=? AND account_id=?",
(new_key, payload_json, row["id"], account_id),
"held=?, updated_at=datetime('now') WHERE id=? AND account_id=?",
(new_key, payload_json, held_for_account(conn, account_or_default(account_id)), row["id"], account_id),
)
conn.commit()
except sqlite3.IntegrityError:
@@ -2197,6 +2199,9 @@ async def post_bulk_fix(request: Request) -> HTMLResponse:
mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()}
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, account_id)
# PRD 5.19 US-002: snapshot `held` din comutatorul contului (nu depinde de rand)
# re-aplicat la re-punerea in coada — paritate cu create_prezentari/requeue_submission.
held_val = held_for_account(conn, account_or_default(account_id))
for raw in ids:
try:
@@ -2290,8 +2295,8 @@ async def post_bulk_fix(request: Request) -> HTMLResponse:
conn.execute(
"UPDATE submissions SET idempotency_key=?, status='queued', payload_json=?, "
"rar_error=NULL, retry_count=0, next_attempt_at=datetime('now'), "
"updated_at=datetime('now') WHERE id=?",
(new_key, payload_json, sid),
"held=?, updated_at=datetime('now') WHERE id=?",
(new_key, payload_json, held_val, sid),
)
reusite += 1
except sqlite3.IntegrityError: