From 136c4e9986b365ede84909fd20487a3e322a7e57 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Tue, 29 Sep 2026 11:01:09 +0300 Subject: [PATCH] fix(retry): pass ANAF kwargs on retry/resync so VAT-payer partner is matched retry_service called import_single_order without cod_fiscal_override / anaf_strict / denumire_override, so re-imports searched the partner by the raw GoMag CUI in dual mode and could match the non-payer twin (bare CUI) of a payer (RO CUI). Add anaf_import_kwargs mirroring the sync flow, plus test and agent docs for prod partner diagnostics. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01L3v311J3SfjroTebzR96ng --- CLAUDE.md | 4 ++- api/app/services/retry_service.py | 37 ++++++++++++++++++- api/tests/test_retry_anaf_kwargs.py | 47 +++++++++++++++++++++++++ docs/diagnostic-prod-partener-gresit.md | 25 +++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 api/tests/test_retry_anaf_kwargs.py create mode 100644 docs/diagnostic-prod-partener-gresit.md diff --git a/CLAUDE.md b/CLAUDE.md index 3c2c578..45bfe3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ Documentatie completa: [README.md](README.md) **Flow zilnic:** 1. Lucrezi pe branch `fix/*` sau `feat/*` -2. `git push` → pre-push hook ruleaza `./test.sh ci` automat (~30s) +2. `./test.sh ci` manual inainte de push (~30s); la `git push` pe branch non-main, CI Gitea (`.gitea/workflows/test.yaml`) ruleaza `./test.sh ci` automat 3. Inainte de PR → `./test.sh full` manual (~2-3 min) 4. Dupa deploy pe prod → `./test.sh smoke-prod --base-url http://79.119.86.134/gomag` @@ -87,6 +87,8 @@ Cand avem date ANAF (`anaf_strict=1`), PL/SQL `cauta_partener_dupa_cod_fiscal` d Python normalizeaza CUI-ul (`re.sub(r'\s+', '', ...)`) inainte de apel Oracle. La creare partener NOU PJ, se foloseste numele oficial ANAF (`denumire_anaf`) in loc de GoMag company_name (poate avea typos); partenerii existenti nu sunt atinsi. +Retry/resync/batch retry TREBUIE sa paseze `retry_service.anaf_import_kwargs(order)` la `import_single_order` (altfel cauta dual si prinde gemenul fara RO). Diagnostic prod partener gresit + acces SSH read-only: [docs/diagnostic-prod-partener-gresit.md](docs/diagnostic-prod-partener-gresit.md) + ### Preturi - Dual policy: articolele sunt rutate la `id_pol_vanzare` sau `id_pol_productie` pe baza contului contabil (341/345 = productie) - Daca pretul lipseste, se insereaza automat pret=0 diff --git a/api/app/services/retry_service.py b/api/app/services/retry_service.py index 4d8629a..ae0c8ef 100644 --- a/api/app/services/retry_service.py +++ b/api/app/services/retry_service.py @@ -9,6 +9,40 @@ from ..constants import OrderStatus logger = logging.getLogger(__name__) +async def anaf_import_kwargs(order) -> dict: + """ANAF-derived kwargs for import_single_order (mirrors the sync flow). + + Without these, a re-import searches the partner by the raw GoMag CUI in dual + mode and can match the non-VAT-payer twin (bare CUI) of a VAT payer (RO CUI). + Empty dict when not an RO company or ANAF has no verdict (sync tolerates that too). + """ + from . import sqlite_service, import_service, anaf_service + + b = order.billing + if not (b.is_company and b.company_code and (b.country or "").strip().lower() == "romania"): + return {} + bare, _ = anaf_service.sanitize_cui(import_service.clean_web_text(b.company_code) or "") + if not anaf_service.validate_cui(bare): + return {} + data = await sqlite_service.get_anaf_cache(bare) + if not data: + try: + fresh = await anaf_service.check_vat_status_batch([bare]) + if fresh: + await sqlite_service.bulk_populate_anaf_cache(fresh) + data = fresh.get(bare) + except Exception as e: + logger.warning(f"ANAF check failed for {bare}: {e}") + if not data or data.get("scpTVA") is None: + return {} + name = (data.get("denumire_anaf") or "").strip().upper() + return { + "cod_fiscal_override": anaf_service.determine_correct_cod_fiscal(bare, data["scpTVA"]), + "anaf_strict": 1, + "denumire_override": name or None, + } + + async def _download_and_reimport(order_number: str, order_date_str: str, customer_name: str, app_settings: dict) -> dict: """Download order from GoMag and re-import it into Oracle. @@ -97,7 +131,8 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome result = await asyncio.to_thread( import_service.import_single_order, target_order, id_pol=id_pol, id_sectie=id_sectie, - app_settings=app_settings, id_gestiuni=id_gestiuni + app_settings=app_settings, id_gestiuni=id_gestiuni, + **await anaf_import_kwargs(target_order), ) except Exception as e: logger.error(f"Retry import failed for {order_number}: {e}") diff --git a/api/tests/test_retry_anaf_kwargs.py b/api/tests/test_retry_anaf_kwargs.py new file mode 100644 index 0000000..f070e99 --- /dev/null +++ b/api/tests/test_retry_anaf_kwargs.py @@ -0,0 +1,47 @@ +"""Retry/resync must pass ANAF-derived kwargs so the VAT-payer partner (RO CUI) is found, not its bare-CUI twin.""" +import os +import sys +import tempfile +from unittest.mock import AsyncMock, patch + +import pytest + +pytestmark = pytest.mark.unit + +_tmpdir = tempfile.mkdtemp() +os.environ.setdefault("FORCE_THIN_MODE", "true") +os.environ.setdefault("SQLITE_DB_PATH", os.path.join(_tmpdir, "test_retry_anaf.db")) +os.environ.setdefault("ORACLE_DSN", "dummy") +os.environ.setdefault("ORACLE_USER", "dummy") +os.environ.setdefault("ORACLE_PASSWORD", "dummy") +os.environ.setdefault("JSON_OUTPUT_DIR", _tmpdir) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.services import retry_service +from app.services.order_reader import OrderBilling, OrderData + + +def _order(country="Romania"): + return OrderData(id="1", number="517596384", date="2026-09-23 08:14:51", status="", status_id="", + billing=OrderBilling(is_company=True, company_code="40874913", country=country)) + + +async def _kwargs(cache, order): + with patch("app.services.sqlite_service.get_anaf_cache", AsyncMock(return_value=cache)), \ + patch("app.services.anaf_service.check_vat_status_batch", AsyncMock(return_value={})): + return await retry_service.anaf_import_kwargs(order) + + +async def test_vat_payer_gets_ro_cui_and_strict(): + kw = await _kwargs({"scpTVA": True, "denumire_anaf": "ctc ben prod s.r.l."}, _order()) + assert kw == {"cod_fiscal_override": "RO40874913", "anaf_strict": 1, "denumire_override": "CTC BEN PROD S.R.L."} + + +async def test_non_payer_gets_bare_cui_and_strict(): + kw = await _kwargs({"scpTVA": False, "denumire_anaf": "X"}, _order()) + assert kw["cod_fiscal_override"] == "40874913" and kw["anaf_strict"] == 1 + + +async def test_no_anaf_verdict_or_foreign_company_gives_nothing(): + assert await _kwargs(None, _order()) == {} + assert await _kwargs({"scpTVA": True, "denumire_anaf": "X"}, _order(country="Germany")) == {} diff --git a/docs/diagnostic-prod-partener-gresit.md b/docs/diagnostic-prod-partener-gresit.md new file mode 100644 index 0000000..97b5e89 --- /dev/null +++ b/docs/diagnostic-prod-partener-gresit.md @@ -0,0 +1,25 @@ +# Diagnostic prod: comanda pe partener gresit (agenti) + +## Acces prod (read-only) +- `ssh -p 22122 -i ~/.ssh/id_ed25519 romfast@79.119.86.134` — user `romfast` (NU `gomag`; `gomag@` cere parola). Sursa: profil Tunnelier `D:\vm303-profile\vending.tlp`. +- Script: `ssh ... "C:\gomag-vending\venv\Scripts\python.exe -" < script.py` (stdin). Scriptul incepe cu `sys.stdout.reconfigure(encoding='utf-8')`. +- Oracle: `dotenv_values(r"C:\gomag-vending\api\.env")`, `TNS_ADMIN` din env, `oracledb.init_oracle_client(lib_dir=INSTANTCLIENTPATH)`, user/dsn din `.env`. +- SQLite: `file:C:/gomag-vending/api/data/import.db?mode=ro` (uri=True). `api/data/import.db` local = vechi, inutil. +- Loguri: `C:\gomag-vending\logs\service_stderr-*.log` (au ore); `service_stdout.log` = access log HTTP, FARA ore. +- Capcane locale: `/tmp` din Bash != `/tmp` din Python Windows; `venv/` din repo e Linux -> pytest cu venv temporar (`pip install -r api/requirements.txt pytest pytest-asyncio`); `tests/e2e`,`tests/qa` cer Playwright (`--ignore` la unit). + +## Schema Oracle (verificat) +- `comenzi.comanda_externa` = numar comanda GoMag; fara `id_adresa_*`. Reimport/resync = comanda noua (`id_comanda` nou), veche `sters=1`. +- `vanzari`: `serie_act`, `numar_act`, `data_act`, `id_part`, `id_comanda`, `sters` (nu `nr_act`). +- 54 tabele cu `ID_PART`; cu date pt un partener: `VANZARI, IREG_PARTENERI, BALANTA_PARTENERI, JV2007, ADRESE_PARTENERI, CORESP_TIP_PART, COMENZI, NOM_PARTENERI`. Verificare mutare partener = numarare pe toate (`user_tab_columns` JOIN `user_tables`). +- Registru TVA / jurnal: tabelele nu sunt identificate; nu s-au verificat. + +## Cauza gasita (2026-09, CTC BEN PROD, CUI 40874913) +- Doi parteneri: 24174 `RO40874913` (platitor) si 2567 `40874913` (fara RO). Comenzi 505768386, 517596384. +- `retry_service._download_and_reimport` (butoane retry/resync/batch retry) apela `import_single_order` FARA `cod_fiscal_override/anaf_strict/denumire_override` -> cautare duala pe CUI brut GoMag -> gemenul fara RO. +- Fix: `retry_service.anaf_import_kwargs(order)` (oglindeste `sync_service` ~L904-965), test `api/tests/test_retry_anaf_kwargs.py`. Orice apelant nou al `import_single_order` trebuie sa il foloseasca. +- Mesajul log `Retry successful` e emis si de `resync` (`POST /api/orders/{n}/resync`); NU inseamna eroare/retry automat. Nu exista retry automat. + +## Reguli +- `orders.cod_fiscal_roa/denumire_roa` (SQLite) vin din override ANAF, nu din Oracle; `partner_mismatch=0` NU dovedeste partenerul corect. Adevar = `comenzi.id_part` / `vanzari.id_part`. +- Corectia partenerului pe factura emisa (comanda, vanzari, registru jurnal, registru TVA) o face utilizatorul in ROA. Agentii NU muta `id_part` pe documente contabile; doar verifica. Scop cerut aici: doar factura/comanda din luna respectiva, nu istoricul (2567 pastreaza 18 vanzari 2020-2026).