fix(anaf): handle notFound integers, skip 4xx retry, propagate errors to run log

ANAF notFound items are plain integers, not dicts — caused 'int has no
attribute get'. 4xx errors (like 404) no longer retry uselessly. ANAF
errors now appear in the UI sync log via log_fn callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-07 12:47:25 +00:00
parent ecde7fe440
commit 5b4b317636
3 changed files with 220 additions and 20 deletions

View File

@@ -73,7 +73,7 @@ def sanitize_cui(raw_cf: str) -> tuple[str, str | None]:
return bare, f"CUI {raw_cf!r} contine caractere invalide dupa sanitizare: {bare!r}"
async def check_vat_status_batch(cui_list: list[str], date: str = None) -> dict[str, dict]:
async def check_vat_status_batch(cui_list: list[str], date: str = None, log_fn=None) -> dict[str, dict]:
"""POST to ANAF API to check VAT status for a batch of CUIs.
Chunks in batches of 500 (ANAF API limit).
@@ -91,35 +91,49 @@ async def check_vat_status_batch(cui_list: list[str], date: str = None) -> dict[
if not body:
continue
chunk_results = await _call_anaf_api(body)
chunk_results = await _call_anaf_api(body, log_fn=log_fn)
results.update(chunk_results)
return results
async def _call_anaf_api(body: list[dict], retry: int = 0) -> dict[str, dict]:
async def _call_anaf_api(body: list[dict], retry: int = 0, log_fn=None) -> dict[str, dict]:
"""Internal: single ANAF API call with retry logic."""
url = "https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva"
results = {}
def _log_error(msg: str):
logger.error(msg)
if log_fn:
log_fn(f"ANAF eroare: {msg}")
def _log_warning(msg: str):
logger.warning(msg)
if log_fn:
log_fn(f"ANAF warn: {msg}")
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(url, json=body)
if response.status_code == 429:
if retry < 1:
logger.warning("ANAF API rate limited (429), retrying in 10s...")
_log_warning("ANAF API rate limited (429), retrying in 10s...")
await asyncio.sleep(10)
return await _call_anaf_api(body, retry + 1)
logger.error("ANAF API rate limited after retry")
return await _call_anaf_api(body, retry + 1, log_fn)
_log_error("ANAF API rate limited after retry")
return {}
if response.status_code >= 500:
if retry < 1:
logger.warning(f"ANAF API server error ({response.status_code}), retrying in 3s...")
_log_warning(f"ANAF API server error ({response.status_code}), retrying in 3s...")
await asyncio.sleep(3)
return await _call_anaf_api(body, retry + 1)
logger.error(f"ANAF API server error after retry: {response.status_code}")
return await _call_anaf_api(body, retry + 1, log_fn)
_log_error(f"ANAF API server error after retry: {response.status_code}")
return {}
if 400 <= response.status_code < 500:
_log_error(f"ANAF API client error {response.status_code} (nu se reincearca)")
return {}
response.raise_for_status()
@@ -138,11 +152,14 @@ async def _call_anaf_api(body: list[dict], retry: int = 0) -> dict[str, dict]:
"checked_at": checked_at,
}
# Not found CUIs
# Not found CUIs — ANAF returns plain integers (CUI values), not dicts
notfound_list = data.get("notFound", [])
for item in notfound_list:
date_gen = item.get("date_generale", {})
cui_str = str(date_gen.get("cui", item.get("cui", "")))
if isinstance(item, int):
cui_str = str(item)
else:
date_gen = item.get("date_generale", {})
cui_str = str(date_gen.get("cui", item.get("cui", "")))
results[cui_str] = {
"scpTVA": None,
"denumire_anaf": "",
@@ -153,16 +170,16 @@ async def _call_anaf_api(body: list[dict], retry: int = 0) -> dict[str, dict]:
except httpx.TimeoutException:
if retry < 1:
logger.warning("ANAF API timeout, retrying in 3s...")
_log_warning("ANAF API timeout, retrying in 3s...")
await asyncio.sleep(3)
return await _call_anaf_api(body, retry + 1)
logger.error("ANAF API timeout after retry")
return await _call_anaf_api(body, retry + 1, log_fn)
_log_error("ANAF API timeout after retry")
except Exception as e:
if retry < 1:
logger.warning(f"ANAF API error: {e}, retrying in 3s...")
_log_warning(f"ANAF API error: {e}, retrying in 3s...")
await asyncio.sleep(3)
return await _call_anaf_api(body, retry + 1)
logger.error(f"ANAF API error after retry: {e}")
return await _call_anaf_api(body, retry + 1, log_fn)
_log_error(f"ANAF API error after retry: {e}")
return results

View File

@@ -683,7 +683,9 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
prepop_cuis = await sqlite_service.get_expired_cuis_for_prepopulate()
if prepop_cuis:
_log_line(run_id, f"ANAF pre-populare: {len(prepop_cuis)} CUI-uri cu cache expirat")
prepop_results = await anaf_service.check_vat_status_batch(prepop_cuis)
prepop_results = await anaf_service.check_vat_status_batch(
prepop_cuis, log_fn=lambda msg: _log_line(run_id, msg)
)
if prepop_results:
await sqlite_service.bulk_populate_anaf_cache(prepop_results)
_log_line(run_id, f"ANAF pre-populare: {len(prepop_results)} rezultate stocate")
@@ -716,7 +718,9 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
# Batch ANAF call for uncached CUIs only
if uncached_cuis:
_log_line(run_id, f"ANAF: verificare {len(uncached_cuis)} CUI-uri noi...")
anaf_results = await anaf_service.check_vat_status_batch(uncached_cuis)
anaf_results = await anaf_service.check_vat_status_batch(
uncached_cuis, log_fn=lambda msg: _log_line(run_id, msg)
)
if anaf_results:
await sqlite_service.bulk_populate_anaf_cache(anaf_results)
cached_results.update(anaf_results)