From 458c3fd43c22bb10e6d6e4abfa7b257cdbf8fbf0 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Mon, 6 Jul 2026 10:12:57 +0000 Subject: [PATCH] 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) --- TODOS.md | 9 +++ app/submissions_admin.py | 9 ++- app/web/routes.py | 19 +++--- tests/test_held_requeue_snapshot.py | 89 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 tests/test_held_requeue_snapshot.py diff --git a/TODOS.md b/TODOS.md index 50745c6..6806668 100644 --- a/TODOS.md +++ b/TODOS.md @@ -2,6 +2,15 @@ Elemente deferate din review-uri. Negrupte de un PRD curent; de promovat cand devin prioritare. +## Din hardening 80/20 (/autoplan, 2026-07-03) + +- [ ] **Traefik IP-allowlist pe /v1 + /metrics in fereastra de lansare** (T4) — cu zero clienti API, + suprafata anonima de ingestie n-are apelant legitim extern (ROAAUTO propriu vine de la IP cunoscut). + O regula Traefik declarativa (allowlist IP ROAAUTO + admin) elimina complet ingestia anonima in + perioada cea mai riscanta. Deferat: P0-1 (auth obligatoriu pe /v1) + T3 (startup guard) acopera deja + gaura principala. De activat cand: primesc trafic de scanare pe /v1 SAU inainte de expunere cu presa. + Effort: S (config Traefik). Depinde de: acces la config Traefik in Dokploy. + ## Din PRD 5.12 (2026-06-26) - [ ] **Mai multi utilizatori per firma (flux de invitatie / alaturare la cont)** — azi CUI e unic, deci diff --git a/app/submissions_admin.py b/app/submissions_admin.py index 8ed7734..2e648f6 100644 --- a/app/submissions_admin.py +++ b/app/submissions_admin.py @@ -20,6 +20,7 @@ from __future__ import annotations import json +from .accounts import held_for_account from .mapping import ( account_or_default, account_scope_clause, @@ -103,11 +104,15 @@ def requeue_submission(conn, account_id: int, sid: int) -> dict: 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, updated_at=datetime('now') " + "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"], sid), + (cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], held_val, sid), ) log_event( "submission_repus", diff --git a/app/web/routes.py b/app/web/routes.py index 0ff0895..5ab9b55 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -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: diff --git a/tests/test_held_requeue_snapshot.py b/tests/test_held_requeue_snapshot.py new file mode 100644 index 0000000..40f6d18 --- /dev/null +++ b/tests/test_held_requeue_snapshot.py @@ -0,0 +1,89 @@ +"""Regresie PRD 5.19 US-002 — re-snapshot `held` pe caile de re-punere in coada. + +Bug (gasit la /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 (requeue_submission, corectie, repune-cu-cod, bulk-fix) +lasau `held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a +esuat, apoi contul trecut pe Auto OFF, la re-punere pastra held=0 -> worker-ul il +auto-trimitea la RAR (FINALIZATA ireversibil) desi contul e Auto OFF. + +Testam sursa comuna requeue_submission (backing API /repune + web) pe ambele directii. +""" + +from __future__ import annotations + +import json +import os +import tempfile + +import pytest + + +@pytest.fixture() +def env(monkeypatch): + tmp = tempfile.mkdtemp() + monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db")) + from app.config import get_settings + get_settings.cache_clear() + from app.db import get_connection, init_db + init_db() + conn = get_connection() + yield conn + conn.close() + get_settings.cache_clear() + + +_CONTENT = { + "vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST", + "data_prestatie": "2026-06-15", "odometru_final": "123456", + "prestatii": [{"cod_prestatie": "OE-1"}], "sistem_reparat": "null", +} + + +def _insert_error(conn, account_id, held): + cur = conn.execute( + "INSERT INTO submissions (idempotency_key, status, payload_json, account_id, held) " + "VALUES (?, 'error', ?, ?, ?)", + (f"key-{os.urandom(4).hex()}", json.dumps(_CONTENT), account_id, held), + ) + return int(cur.lastrowid) + + +def _held(conn, sid): + return conn.execute("SELECT held FROM submissions WHERE id=?", (sid,)).fetchone()["held"] + + +def test_requeue_tine_randul_cand_cont_auto_off(env): + """Auto OFF + rand held=0 (ingerat pe Auto ON) -> requeue re-snapshot -> held=1. + + Fara fix worker-ul l-ar auto-trimite desi contul e Auto OFF. + """ + from app.accounts import create_account, set_auto_send + from app.submissions_admin import requeue_submission + + conn = env + acct = create_account(conn, "Service AutoOff", active=True) + set_auto_send(conn, acct, False) # cont Auto OFF + sid = _insert_error(conn, acct, held=0) # rand cu held vechi (de pe Auto ON) + + requeue_submission(conn, acct, sid) + + assert _held(conn, sid) == 1, "rand repus pe cont Auto OFF trebuie TINUT (held=1)" + + +def test_requeue_elibereaza_randul_cand_cont_auto_on(env): + """Auto ON + rand held=1 (ingerat pe Auto OFF) -> requeue re-snapshot -> held=0. + + Altfel randul ar ramane blocat (worker sare peste held=1) dupa ce contul e Auto ON. + """ + from app.accounts import create_account, set_auto_send + from app.submissions_admin import requeue_submission + + conn = env + acct = create_account(conn, "Service AutoOn", active=True) + set_auto_send(conn, acct, True) # cont Auto ON + sid = _insert_error(conn, acct, held=1) # rand cu held vechi (de pe Auto OFF) + + requeue_submission(conn, acct, sid) + + assert _held(conn, sid) == 0, "rand repus pe cont Auto ON NU trebuie tinut (held=0)"