feat(sync): gate CUI invalid/ANAF-notFound → ERROR inainte de import Oracle
Incident 22.04.2026 (#485225171 NONA ROYAL SRL): clientul a inversat cod_fiscal cu registru in GoMag → sistem a creat partener cu CUI=J1994000194225. Adauga evaluate_cui_gate() care blocheaza comanda (ERROR) daca: - CUI format invalid (ex: J.. in loc de cifre) - CUI nu trece cifra de control - ANAF returneaza explicit notFound (scpTVA=None + denumire_anaf="") ANAF down (anaf_data=None) → fallback pass, comportament existent pastrat. _record_order_error() DRY helper evita duplicarea upsert/add_items. Contract ANAF down/notFound/found documentat in anaf_service._call_anaf_api. 9 teste unit (inclusiv T5 CRITIC: ANAF down nu blocheaza) + T7 COALESCE. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,12 @@ async def _call_anaf_api(body: list[dict], retry: int = 0, log_fn=None) -> dict[
|
||||
|
||||
checked_at = datetime.now().isoformat()
|
||||
|
||||
# CONTRACT (consumed by sync_service.evaluate_cui_gate):
|
||||
# Return {} → transient error (down/429/5xx/timeout)
|
||||
# Return {cui: {scpTVA: None, denumire_anaf: ""}} → ANAF notFound explicit
|
||||
# Return {cui: {scpTVA: bool, denumire_anaf: str}} → ANAF found
|
||||
# If you change this semantics, update the gate in sync_service too.
|
||||
|
||||
# Parse ANAF response
|
||||
found_list = data.get("found", [])
|
||||
for item in found_list:
|
||||
|
||||
@@ -112,6 +112,58 @@ async def _record_phase_err(run_id: str, phase: str, err: Exception) -> None:
|
||||
logger.warning(f"record_phase_failure failed for phase={phase}: {rec_err}")
|
||||
|
||||
|
||||
def evaluate_cui_gate(
|
||||
is_ro_company: bool,
|
||||
company_code_raw: str | None,
|
||||
bare_cui: str,
|
||||
anaf_data: dict | None,
|
||||
) -> str | None:
|
||||
"""Return block reason or None if the order passes the CUI gate.
|
||||
|
||||
CONTRACT on anaf_data:
|
||||
- None → ANAF down / transient error → tolerate (pass)
|
||||
- {scpTVA: None, denumire_anaf: ""} → ANAF notFound explicit → block
|
||||
- {scpTVA: bool, denumire_anaf: str} → ANAF found → pass
|
||||
"""
|
||||
if not is_ro_company or not company_code_raw:
|
||||
return None
|
||||
if not anaf_service.validate_cui(bare_cui):
|
||||
return f"CUI invalid (format): {company_code_raw!r}"
|
||||
if not anaf_service.validate_cui_checksum(bare_cui):
|
||||
return f"CUI invalid (cifra de control): {bare_cui}"
|
||||
if (
|
||||
anaf_data is not None
|
||||
and anaf_data.get("scpTVA") is None
|
||||
and not (anaf_data.get("denumire_anaf") or "").strip()
|
||||
):
|
||||
return (
|
||||
f"CUI {company_code_raw!r} (sanitizat: {bare_cui}) nu exista in registrul ANAF — "
|
||||
f"verifica daca nu e inversat cu numarul de la registrul comertului"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def _record_order_error(
|
||||
run_id: str, order, customer: str, shipping_name: str, billing_name: str,
|
||||
payment_method: str, delivery_method: str, discount_split_json: str | None,
|
||||
order_items_data: list, reason: str, id_partener: int | None = None,
|
||||
) -> None:
|
||||
"""Write an ERROR row to SQLite (orders + sync_run_orders + order_items)."""
|
||||
await sqlite_service.upsert_order(
|
||||
sync_run_id=run_id, order_number=order.number, order_date=order.date,
|
||||
customer_name=customer, status=OrderStatus.ERROR.value,
|
||||
id_partener=id_partener, error_message=reason,
|
||||
items_count=len(order.items),
|
||||
shipping_name=shipping_name, billing_name=billing_name,
|
||||
payment_method=payment_method, delivery_method=delivery_method,
|
||||
order_total=order.total or None, delivery_cost=order.delivery_cost or None,
|
||||
discount_total=order.discount_total or None, web_status=order.status or None,
|
||||
discount_split=discount_split_json,
|
||||
)
|
||||
await sqlite_service.add_sync_run_order(run_id, order.number, OrderStatus.ERROR.value)
|
||||
await sqlite_service.add_order_items(order.number, order_items_data)
|
||||
|
||||
|
||||
async def _check_escalation() -> tuple[str | None, dict[str, int]]:
|
||||
"""Return (phase_to_halt_on, recent_counts).
|
||||
|
||||
@@ -919,6 +971,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
cod_fiscal_override = None
|
||||
anaf_data_for_order = None
|
||||
raw_cf = ""
|
||||
bare_cui = ""
|
||||
if order.billing.is_company and order.billing.company_code:
|
||||
raw_cf = import_service.clean_web_text(order.billing.company_code) or ""
|
||||
bare_cui, cui_warning = anaf_service.sanitize_cui(raw_cf)
|
||||
@@ -938,6 +991,36 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
if is_ro_company and anaf_data_for_order and anaf_data_for_order.get("scpTVA") is not None:
|
||||
anaf_strict = 1 # ANAF data available → strict search
|
||||
|
||||
# Build order items data and discount split (needed by gate error path)
|
||||
order_items_data = []
|
||||
for item in order.items:
|
||||
ms = "mapped" if item.sku in validation["mapped"] else "direct"
|
||||
order_items_data.append({
|
||||
"sku": item.sku, "product_name": item.name,
|
||||
"quantity": item.quantity, "price": item.price,
|
||||
"baseprice": item.baseprice, "vat": item.vat,
|
||||
"mapping_status": ms, "codmat": None, "id_articol": None,
|
||||
"cantitate_roa": None
|
||||
})
|
||||
ds = import_service.compute_discount_split(order, app_settings)
|
||||
discount_split_json = json.dumps(ds) if ds else None
|
||||
|
||||
# Gate CUI (RO PJ): block if CUI invalid or ANAF explicit notFound
|
||||
block_reason = evaluate_cui_gate(
|
||||
is_ro_company, order.billing.company_code, bare_cui, anaf_data_for_order
|
||||
)
|
||||
if block_reason:
|
||||
error_count += 1
|
||||
_log_line(run_id, f"#{order.number} BLOCAT: {block_reason}")
|
||||
await _record_order_error(
|
||||
run_id, order, customer, shipping_name, billing_name,
|
||||
payment_method, delivery_method, discount_split_json,
|
||||
order_items_data, block_reason,
|
||||
)
|
||||
if error_count > 10:
|
||||
break
|
||||
continue
|
||||
|
||||
# ANAF official name override: used at partner creation (not lookup).
|
||||
# Strip before truthy check → reject whitespace-only values.
|
||||
denumire_override = None
|
||||
@@ -955,22 +1038,6 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
denumire_override=denumire_override,
|
||||
)
|
||||
|
||||
# Build order items data for storage (R9)
|
||||
order_items_data = []
|
||||
for item in order.items:
|
||||
ms = "mapped" if item.sku in validation["mapped"] else "direct"
|
||||
order_items_data.append({
|
||||
"sku": item.sku, "product_name": item.name,
|
||||
"quantity": item.quantity, "price": item.price,
|
||||
"baseprice": item.baseprice, "vat": item.vat,
|
||||
"mapping_status": ms, "codmat": None, "id_articol": None,
|
||||
"cantitate_roa": None
|
||||
})
|
||||
|
||||
# Compute discount split for SQLite storage
|
||||
ds = import_service.compute_discount_split(order, app_settings)
|
||||
discount_split_json = json.dumps(ds) if ds else None
|
||||
|
||||
if result["success"]:
|
||||
imported_count += 1
|
||||
await sqlite_service.upsert_order(
|
||||
@@ -1040,28 +1107,13 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
|
||||
if not result["success"]:
|
||||
error_count += 1
|
||||
await sqlite_service.upsert_order(
|
||||
sync_run_id=run_id,
|
||||
order_number=order.number,
|
||||
order_date=order.date,
|
||||
customer_name=customer,
|
||||
status=OrderStatus.ERROR.value,
|
||||
id_partener=result.get("id_partener"),
|
||||
error_message=result["error"],
|
||||
items_count=len(order.items),
|
||||
shipping_name=shipping_name,
|
||||
billing_name=billing_name,
|
||||
payment_method=payment_method,
|
||||
delivery_method=delivery_method,
|
||||
order_total=order.total or None,
|
||||
delivery_cost=order.delivery_cost or None,
|
||||
discount_total=order.discount_total or None,
|
||||
web_status=order.status or None,
|
||||
discount_split=discount_split_json,
|
||||
)
|
||||
await sqlite_service.add_sync_run_order(run_id, order.number, OrderStatus.ERROR.value)
|
||||
await sqlite_service.add_order_items(order.number, order_items_data)
|
||||
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → EROARE: {result['error']}")
|
||||
await _record_order_error(
|
||||
run_id, order, customer, shipping_name, billing_name,
|
||||
payment_method, delivery_method, discount_split_json,
|
||||
order_items_data, result["error"],
|
||||
id_partener=result.get("id_partener"),
|
||||
)
|
||||
|
||||
# Safety: stop if too many errors
|
||||
if error_count > 10:
|
||||
|
||||
Reference in New Issue
Block a user