Merge fix/retry-ensure-prices: pre-populate price list before retry

This commit is contained in:
Claude Agent
2026-04-29 10:20:11 +00:00
4 changed files with 608 additions and 130 deletions

View File

@@ -59,6 +59,40 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome
id_gestiune = app_settings.get("id_gestiune", "")
id_gestiuni = [int(g.strip()) for g in id_gestiune.split(",") if g.strip()] if id_gestiune else None
# Pre-validate prices: auto-insert PRET=0 in CRM_POLITICI_PRET_ART for missing
# CODMATs so PL/SQL doesn't crash with COM-001. Mirrors sync_service flow.
from .. import database
validation = {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}}
if database.pool is not None:
conn = await asyncio.to_thread(database.get_oracle_connection)
try:
skus = {item.sku for item in target_order.items if item.sku}
if skus:
validation = await asyncio.to_thread(
validation_service.validate_skus, skus, conn, id_gestiuni,
)
if id_pol and skus:
id_pol_productie = int(app_settings.get("id_pol_productie") or 0) or None
cota_tva = float(app_settings.get("discount_vat") or 21)
await asyncio.to_thread(
validation_service.pre_validate_order_prices,
[target_order], app_settings, conn, id_pol, id_pol_productie,
id_gestiuni, validation, None, cota_tva,
)
except Exception as e:
logger.error(f"Retry pre-validation failed for {order_number}: {e}")
await sqlite_service.upsert_order(
sync_run_id="retry",
order_number=order_number,
order_date=order_date_str,
customer_name=customer_name,
status=OrderStatus.ERROR.value,
error_message=f"Retry pre-validation failed: {e}",
)
return {"success": False, "message": f"Eroare pre-validare preturi: {e}"}
finally:
await asyncio.to_thread(database.pool.release, conn)
try:
result = await asyncio.to_thread(
import_service.import_single_order,
@@ -77,17 +111,6 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome
)
return {"success": False, "message": f"Eroare import: {e}"}
# Build order_items data from fresh GoMag download (mirrors sync_service:882-891).
# Resolves ARTICOLE_TERTI mapping so UI shows mapped/direct badge.
try:
skus = {item.sku for item in target_order.items if item.sku}
validation = await asyncio.to_thread(
validation_service.validate_skus, skus, None, id_gestiuni
) if skus else {"mapped": set(), "direct": set()}
except Exception as e:
logger.warning(f"Retry: validate_skus failed for {order_number}, defaulting mapping_status=direct: {e}")
validation = {"mapped": set(), "direct": set()}
order_items_data = [
{
"sku": item.sku, "product_name": item.name,

View File

@@ -592,120 +592,22 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
# Step 2d: Pre-validate prices for importable articles
if id_pol and (truly_importable or already_in_roa):
_update_progress("validation", "Validating prices...", 0, len(truly_importable))
_log_line(run_id, "Validare preturi...")
all_codmats = set()
for order in (truly_importable + already_in_roa):
for item in order.items:
if item.sku in validation["mapped"]:
pass
elif item.sku in validation["direct"]:
all_codmats.add(item.sku)
# Get standard VAT rate from settings for PROC_TVAV metadata
cota_tva = float(app_settings.get("discount_vat") or 21)
# Dual pricing policy support
id_pol_productie = int(app_settings.get("id_pol_productie") or 0) or None
codmat_policy_map = {}
if all_codmats:
if id_pol_productie:
# Dual-policy: classify articles by cont (sales vs production)
codmat_policy_map = await asyncio.to_thread(
validation_service.validate_and_ensure_prices_dual,
all_codmats, id_pol, id_pol_productie,
conn, validation.get("direct_id_map"),
cota_tva=cota_tva
)
_log_line(run_id,
f"Politici duale: {sum(1 for v in codmat_policy_map.values() if v == id_pol)} vanzare, "
f"{sum(1 for v in codmat_policy_map.values() if v == id_pol_productie)} productie")
else:
# Single-policy (backward compatible)
price_result = await asyncio.to_thread(
validation_service.validate_prices, all_codmats, id_pol,
conn, validation.get("direct_id_map")
)
if price_result["missing_price"]:
logger.info(
f"Auto-adding price 0 for {len(price_result['missing_price'])} "
f"direct articles in policy {id_pol}"
)
await asyncio.to_thread(
validation_service.ensure_prices,
price_result["missing_price"], id_pol,
conn, validation.get("direct_id_map"),
cota_tva=cota_tva
pv_result = await asyncio.to_thread(
validation_service.pre_validate_order_prices,
truly_importable + already_in_roa,
app_settings, conn, id_pol, id_pol_productie,
id_gestiuni, validation,
lambda msg: _log_line(run_id, msg),
cota_tva,
)
# Also validate mapped SKU prices (cherry-pick 1)
mapped_skus_in_orders = set()
for order in (truly_importable + already_in_roa):
for item in order.items:
if item.sku in validation["mapped"]:
mapped_skus_in_orders.add(item.sku)
mapped_codmat_data = {}
if mapped_skus_in_orders:
mapped_codmat_data = await asyncio.to_thread(
validation_service.resolve_mapped_codmats, mapped_skus_in_orders, conn,
id_gestiuni=id_gestiuni
)
# Build id_map for mapped codmats and validate/ensure their prices
mapped_id_map = {}
for sku, entries in mapped_codmat_data.items():
for entry in entries:
mapped_id_map[entry["codmat"]] = {
"id_articol": entry["id_articol"],
"cont": entry.get("cont")
}
mapped_codmats = set(mapped_id_map.keys())
if mapped_codmats:
if id_pol_productie:
mapped_policy_map = await asyncio.to_thread(
validation_service.validate_and_ensure_prices_dual,
mapped_codmats, id_pol, id_pol_productie,
conn, mapped_id_map, cota_tva=cota_tva
)
codmat_policy_map.update(mapped_policy_map)
else:
mp_result = await asyncio.to_thread(
validation_service.validate_prices,
mapped_codmats, id_pol, conn, mapped_id_map
)
if mp_result["missing_price"]:
await asyncio.to_thread(
validation_service.ensure_prices,
mp_result["missing_price"], id_pol,
conn, mapped_id_map, cota_tva=cota_tva
)
# Add SKU → policy entries for mapped articles (1:1 and kits)
# codmat_policy_map has CODMAT keys, but build_articles_json
# looks up by GoMag SKU — bridge the gap here
if codmat_policy_map and mapped_codmat_data:
for sku, entries in mapped_codmat_data.items():
if len(entries) == 1:
# 1:1 mapping: SKU inherits the CODMAT's policy
codmat = entries[0]["codmat"]
if codmat in codmat_policy_map:
codmat_policy_map[sku] = codmat_policy_map[codmat]
# Pass codmat_policy_map to import via app_settings
if codmat_policy_map:
app_settings["_codmat_policy_map"] = codmat_policy_map
# ── Kit component price validation ──
kit_pricing_mode = app_settings.get("kit_pricing_mode")
if kit_pricing_mode and mapped_codmat_data:
id_pol_prod = int(app_settings.get("id_pol_productie") or 0) or None
kit_missing = await asyncio.to_thread(
validation_service.validate_kit_component_prices,
mapped_codmat_data, id_pol, id_pol_prod, conn
)
# Filter truly_importable for kits with missing component prices
kit_missing = pv_result.get("kit_missing") or {}
if kit_missing:
kit_skus_missing = set(kit_missing.keys())
for sku, missing_codmats in kit_missing.items():
_log_line(run_id, f"Kit {sku}: prețuri lipsă pentru {', '.join(missing_codmats)}")
new_truly = []
for order in truly_importable:
order_skus = {item.sku for item in order.items}
@@ -717,7 +619,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
truly_importable = new_truly
# Mode B config validation
if kit_pricing_mode == "separate_line":
if app_settings.get("kit_pricing_mode") == "separate_line":
if not app_settings.get("kit_discount_codmat"):
_log_line(run_id, "EROARE: Kit mode 'separate_line' dar kit_discount_codmat nu e configurat!")
finally:

View File

@@ -529,6 +529,147 @@ def resolve_mapped_codmats(mapped_skus: set[str], conn,
return result
def pre_validate_order_prices(orders, app_settings: dict, conn, id_pol: int,
id_pol_productie: int = None,
id_gestiuni: list[int] = None,
validation: dict = None,
log_callback=None,
cota_tva: float = 21) -> dict:
"""Pre-validate prices for orders before importing them via PACK_IMPORT_COMENZI.
Auto-inserts PRET=0 rows in CRM_POLITICI_PRET_ART for missing CODMATs so
PL/SQL adauga_articol_comanda doesn't raise COM-001. Mutates
app_settings["_codmat_policy_map"] for build_articles_json routing.
Used by both bulk sync (sync_service.run_sync) and retry (retry_service).
Args:
orders: list of orders to scan for SKUs/CODMATs
app_settings: mutated with _codmat_policy_map (SKU/CODMAT → id_pol)
conn: Oracle connection (caller manages lifecycle)
id_pol: default sales price policy
id_pol_productie: production policy for cont 341/345 (None = single-policy)
id_gestiuni: gestiune filter for resolve_mapped_codmats
validation: output of validate_skus; computed internally if None
log_callback: optional Callable[[str], None] for progress messages
cota_tva: VAT rate for PROC_TVAV metadata (default 21)
Returns: {"codmat_policy_map": dict, "kit_missing": dict, "validation": dict}
- codmat_policy_map: {codmat_or_sku: id_pol}
- kit_missing: {sku: [missing_codmats]} for kits with unprice components
- validation: validate_skus result (for caller convenience)
"""
log = log_callback or (lambda _msg: None)
if not orders:
return {"codmat_policy_map": {}, "kit_missing": {}, "validation": validation or {}}
if validation is None:
all_skus = {item.sku for o in orders for item in o.items if item.sku}
validation = validate_skus(all_skus, conn, id_gestiuni)
log("Validare preturi...")
# Direct CODMATs (SKU exists in NOM_ARTICOLE without ARTICOLE_TERTI mapping)
all_codmats = set()
for order in orders:
for item in order.items:
if item.sku in validation["mapped"]:
continue
if item.sku in validation["direct"]:
all_codmats.add(item.sku)
codmat_policy_map = {}
if all_codmats:
if id_pol_productie:
codmat_policy_map = validate_and_ensure_prices_dual(
all_codmats, id_pol, id_pol_productie,
conn, validation.get("direct_id_map"), cota_tva=cota_tva,
)
log(f"Politici duale: "
f"{sum(1 for v in codmat_policy_map.values() if v == id_pol)} vanzare, "
f"{sum(1 for v in codmat_policy_map.values() if v == id_pol_productie)} productie")
else:
price_result = validate_prices(
all_codmats, id_pol, conn, validation.get("direct_id_map"),
)
if price_result["missing_price"]:
logger.info(
f"Auto-adding price 0 for {len(price_result['missing_price'])} "
f"direct articles in policy {id_pol}"
)
ensure_prices(
price_result["missing_price"], id_pol,
conn, validation.get("direct_id_map"), cota_tva=cota_tva,
)
# Mapped SKUs (via ARTICOLE_TERTI)
mapped_skus_in_orders = {
item.sku for o in orders for item in o.items
if item.sku in validation["mapped"]
}
mapped_codmat_data = {}
if mapped_skus_in_orders:
mapped_codmat_data = resolve_mapped_codmats(
mapped_skus_in_orders, conn, id_gestiuni=id_gestiuni,
)
mapped_id_map = {}
for sku, entries in mapped_codmat_data.items():
for entry in entries:
mapped_id_map[entry["codmat"]] = {
"id_articol": entry["id_articol"],
"cont": entry.get("cont"),
}
mapped_codmats = set(mapped_id_map.keys())
if mapped_codmats:
if id_pol_productie:
mapped_policy_map = validate_and_ensure_prices_dual(
mapped_codmats, id_pol, id_pol_productie,
conn, mapped_id_map, cota_tva=cota_tva,
)
codmat_policy_map.update(mapped_policy_map)
else:
mp_result = validate_prices(
mapped_codmats, id_pol, conn, mapped_id_map,
)
if mp_result["missing_price"]:
ensure_prices(
mp_result["missing_price"], id_pol,
conn, mapped_id_map, cota_tva=cota_tva,
)
# Bridge SKU → policy via 1:1 mappings (build_articles_json reads by SKU)
if codmat_policy_map and mapped_codmat_data:
for sku, entries in mapped_codmat_data.items():
if len(entries) == 1:
codmat = entries[0]["codmat"]
if codmat in codmat_policy_map:
codmat_policy_map[sku] = codmat_policy_map[codmat]
if codmat_policy_map:
app_settings["_codmat_policy_map"] = codmat_policy_map
# Kit component price gating
kit_missing = {}
kit_pricing_mode = app_settings.get("kit_pricing_mode")
if kit_pricing_mode and mapped_codmat_data:
kit_missing = validate_kit_component_prices(
mapped_codmat_data, id_pol, id_pol_productie, conn,
)
if kit_missing:
for sku, missing_codmats in kit_missing.items():
log(f"Kit {sku}: prețuri lipsă pentru {', '.join(missing_codmats)}")
return {
"codmat_policy_map": codmat_policy_map,
"kit_missing": kit_missing,
"validation": validation,
"mapped_codmat_data": mapped_codmat_data,
}
def validate_kit_component_prices(mapped_codmat_data: dict, id_pol: int,
id_pol_productie: int = None, conn=None) -> dict:
"""Pre-validate that kit components have non-zero prices in crm_politici_pret_art.

View File

@@ -0,0 +1,412 @@
"""Tests for validation_service.pre_validate_order_prices and retry pre-validation.
Regression source: production VENDING orders #485841978 and #485841895 (2026-04-28)
crashed with PL/SQL COM-001 'Pretul pentru acest articol nu a fost gasit in lista
de preturi' because the Retry button skipped the price-list pre-population step
that bulk sync runs.
These tests verify:
- pre_validate_order_prices auto-inserts PRET=0 in CRM_POLITICI_PRET_ART for
CODMATs missing entries (so PL/SQL doesn't crash).
- Dual-policy routing: cont 341/345 → id_pol_productie; else → id_pol.
- Empty input returns empty result without DB calls.
- Idempotent: running twice when prices already exist does no inserts.
- retry_service propagates pre-validation failures as ERROR with clear message.
"""
import os
import sys
from unittest.mock import patch, MagicMock, AsyncMock
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.services.order_reader import OrderData, OrderItem, OrderShipping, OrderBilling
def _make_order(number: str, items: list[tuple[str, float, float]]) -> OrderData:
"""items = [(sku, quantity, price), ...]"""
return OrderData(
id=number,
number=number,
date="2026-04-28 10:00:00",
items=[
OrderItem(sku=sku, name=f"Product {sku}", price=price,
quantity=qty, vat=21.0, baseprice=price)
for sku, qty, price in items
],
billing=OrderBilling(firstname="Ion", lastname="Test"),
shipping=OrderShipping(firstname="Ion", lastname="Test"),
)
# ============================================================
# UNIT TESTS — no Oracle
# ============================================================
@pytest.mark.unit
def test_pre_validate_empty_orders_returns_empty():
"""Empty orders list short-circuits without any DB calls."""
from app.services import validation_service
app_settings = {}
mock_conn = MagicMock()
result = validation_service.pre_validate_order_prices(
orders=[],
app_settings=app_settings,
conn=mock_conn,
id_pol=1,
)
assert result["codmat_policy_map"] == {}
assert result["kit_missing"] == {}
# No DB cursor was opened
mock_conn.cursor.assert_not_called()
# app_settings unchanged
assert "_codmat_policy_map" not in app_settings
@pytest.mark.unit
def test_pre_validate_no_skus_in_orders():
"""Orders with no items skip price validation entirely."""
from app.services import validation_service
order = OrderData(id="1", number="1", date="2026-04-28", items=[],
billing=OrderBilling())
app_settings = {}
mock_conn = MagicMock()
# validation passed-in is empty
validation = {"mapped": set(), "direct": set(), "missing": set(),
"direct_id_map": {}}
result = validation_service.pre_validate_order_prices(
orders=[order],
app_settings=app_settings,
conn=mock_conn,
id_pol=1,
validation=validation,
)
assert result["codmat_policy_map"] == {}
assert result["kit_missing"] == {}
@pytest.mark.unit
async def test_retry_propagates_pre_validation_error():
"""Pre-validation failure in retry path returns ERROR with clear message."""
from app.services import retry_service
target_order = _make_order("RETRY-FAIL-1", [("SKU-X", 1, 100)])
fake_pool = MagicMock()
fake_conn = MagicMock()
fake_pool.release = MagicMock()
with patch("app.services.gomag_client.download_orders",
new=AsyncMock(return_value=None)), \
patch("app.services.order_reader.read_json_orders",
return_value=([target_order], 1)), \
patch("app.services.sqlite_service.upsert_order",
new=AsyncMock()) as mock_upsert, \
patch("app.services.validation_service.validate_skus",
return_value={"mapped": set(), "direct": {"SKU-X"},
"missing": set(), "direct_id_map": {"SKU-X": 1}}), \
patch("app.services.validation_service.pre_validate_order_prices",
side_effect=RuntimeError("ORA-12541: TNS no listener")), \
patch("app.database.pool", fake_pool), \
patch("app.database.get_oracle_connection", return_value=fake_conn):
app_settings = {"id_pol": "1", "id_gestiune": "1", "discount_vat": "21"}
result = await retry_service._download_and_reimport(
order_number="RETRY-FAIL-1",
order_date_str="2026-04-28T10:00:00",
customer_name="Ion Test",
app_settings=app_settings,
)
assert result["success"] is False
assert "pre-validare" in result["message"].lower()
assert "TNS" in result["message"]
# Verify ERROR persisted to SQLite
mock_upsert.assert_called_once()
call_kwargs = mock_upsert.call_args.kwargs
assert call_kwargs["status"] == "ERROR"
# ============================================================
# ORACLE INTEGRATION TESTS — require live Oracle
# ============================================================
# Oracle connection setup (lazy import to keep unit tests isolated)
def _get_oracle_conn():
import oracledb
from dotenv import load_dotenv
load_dotenv('.env')
user = os.environ['ORACLE_USER']
password = os.environ['ORACLE_PASSWORD']
dsn = os.environ['ORACLE_DSN']
try:
instantclient_path = os.environ.get(
'INSTANTCLIENTPATH', '/opt/oracle/instantclient_23_9'
)
oracledb.init_oracle_client(lib_dir=instantclient_path)
except Exception:
pass
return oracledb.connect(user=user, password=password, dsn=dsn)
def _has_price_entry(cur, id_pol: int, id_articol: int) -> tuple[bool, float | None]:
"""Returns (exists, pret). pret is None if row doesn't exist."""
cur.execute("""
SELECT PRET FROM crm_politici_pret_art
WHERE id_pol = :p AND id_articol = :a
""", {"p": id_pol, "a": id_articol})
row = cur.fetchone()
return (row is not None, row[0] if row else None)
def _pick_unpriced_article(cur, id_pol: int, count: int = 1) -> list[tuple[int, str]]:
"""Find existing NOM_ARTICOLE rows without CRM_POLITICI_PRET_ART entry for id_pol.
Returns: [(id_articol, codmat), ...]. Skips test if not enough found.
"""
cur.execute("""
SELECT id_articol, codmat FROM nom_articole na
WHERE sters = 0 AND inactiv = 0
AND NOT EXISTS (
SELECT 1 FROM crm_politici_pret_art pa
WHERE pa.id_articol = na.id_articol AND pa.id_pol = :pol
)
AND ROWNUM <= :n
""", {"pol": id_pol, "n": count})
return cur.fetchall()
def _pick_priced_article(cur, id_pol: int) -> tuple[int, str, float] | None:
"""Find any (id_articol, codmat, pret) with existing CRM_POLITICI_PRET_ART entry."""
cur.execute("""
SELECT na.id_articol, na.codmat, pa.pret
FROM nom_articole na
JOIN crm_politici_pret_art pa ON pa.id_articol = na.id_articol
WHERE pa.id_pol = :pol AND na.sters = 0 AND na.inactiv = 0
AND ROWNUM <= 1
""", {"pol": id_pol})
return cur.fetchone()
def _pick_default_id_pol(cur) -> int | None:
"""Pick first usable id_pol from CRM_POLITICI_PRETURI."""
cur.execute("""
SELECT id_pol FROM crm_politici_preturi
WHERE sters = 0 AND ROWNUM <= 1
ORDER BY id_pol
""")
row = cur.fetchone()
return row[0] if row else None
@pytest.mark.oracle
def test_pre_validate_inserts_missing_prices_for_direct_sku():
"""REGRESSION (prod orders #485841978, #485841895):
A SKU that resolves directly to a CODMAT in NOM_ARTICOLE with NO entry
in CRM_POLITICI_PRET_ART must auto-insert PRET=0 so the import doesn't
crash with COM-001.
Uses a real unpriced article from the test schema. Cleans up after.
"""
from app.services import validation_service
with _get_oracle_conn() as conn:
with conn.cursor() as cur:
id_pol = _pick_default_id_pol(cur)
assert id_pol is not None, "No usable id_pol found in CRM_POLITICI_PRETURI"
unpriced = _pick_unpriced_article(cur, id_pol, count=1)
if not unpriced:
pytest.skip(f"All articles in policy {id_pol} already have prices")
id_art, codmat = unpriced[0]
inserted = False
try:
# Pre-condition
exists, _ = _has_price_entry(cur, id_pol, id_art)
assert not exists, f"Pre-condition: {codmat} should be unpriced"
# Use codmat as direct SKU. validate_skus → direct (matches NOM_ARTICOLE)
order = _make_order("VEN-PV-DIRECT", [(codmat, 1, 100)])
app_settings = {}
validation = {
"mapped": set(),
"direct": {codmat},
"missing": set(),
"direct_id_map": {codmat: {"id_articol": id_art, "cont": None}},
}
validation_service.pre_validate_order_prices(
orders=[order], app_settings=app_settings, conn=conn,
id_pol=id_pol, validation=validation, cota_tva=21,
)
conn.commit()
inserted = True
# Post-condition: PRET=0 row created
exists, pret = _has_price_entry(cur, id_pol, id_art)
assert exists, (
f"REGRESSION: price entry for {codmat} (id={id_art}) "
f"in policy {id_pol} should be auto-created"
)
assert pret == 0, f"Auto-inserted price should be 0, got {pret}"
finally:
if inserted:
cur.execute(
"DELETE FROM crm_politici_pret_art "
"WHERE id_pol = :p AND id_articol = :a AND pret = 0",
{"p": id_pol, "a": id_art},
)
conn.commit()
@pytest.mark.oracle
def test_pre_validate_idempotent_when_prices_exist():
"""When all CODMATs already have CRM_POLITICI_PRET_ART entries, no INSERTs run.
Verifies idempotency on a second pre-validation pass — existing prices untouched."""
from app.services import validation_service
with _get_oracle_conn() as conn:
with conn.cursor() as cur:
id_pol = _pick_default_id_pol(cur)
assert id_pol is not None, "No usable id_pol found"
priced = _pick_priced_article(cur, id_pol)
if not priced:
pytest.skip(f"No priced articles in policy {id_pol}")
id_art, codmat, pret_orig = priced
cur.execute("""SELECT COUNT(*) FROM crm_politici_pret_art
WHERE id_articol = :a AND id_pol = :p""",
{"a": id_art, "p": id_pol})
count_before = cur.fetchone()[0]
order = _make_order("VEN-IDEM", [(codmat, 1, 200)])
app_settings = {}
validation = {
"mapped": set(), "direct": {codmat}, "missing": set(),
"direct_id_map": {codmat: {"id_articol": id_art, "cont": None}},
}
for _ in range(2): # Run twice
validation_service.pre_validate_order_prices(
orders=[order], app_settings=app_settings, conn=conn,
id_pol=id_pol, validation=validation, cota_tva=21,
)
conn.commit()
cur.execute("""SELECT COUNT(*), MAX(pret) FROM crm_politici_pret_art
WHERE id_articol = :a AND id_pol = :p""",
{"a": id_art, "p": id_pol})
count_after, pret_after = cur.fetchone()
assert count_after == count_before, (
f"Idempotency violated: {count_before}{count_after} rows"
)
assert pret_after == pret_orig, (
f"Existing price changed: {pret_orig}{pret_after}"
)
@pytest.mark.oracle
def test_pre_validate_dual_policy_routing():
"""Articles with cont 341/345 route to id_pol_productie; others to id_pol_vanzare.
Picks two existing unpriced articles, marks one with cont=341, runs
pre_validate, asserts each landed in the expected policy.
"""
from app.services import validation_service
with _get_oracle_conn() as conn:
with conn.cursor() as cur:
id_pol = _pick_default_id_pol(cur)
assert id_pol is not None, "No usable id_pol"
# Find a second policy to use as productie (any other usable id_pol)
cur.execute("""SELECT id_pol FROM crm_politici_preturi
WHERE sters = 0 AND id_pol != :p AND ROWNUM <= 1
ORDER BY id_pol""", {"p": id_pol})
row = cur.fetchone()
if not row:
pytest.skip("Need 2 distinct id_pol values for dual-policy test")
id_pol_productie = row[0]
unpriced = _pick_unpriced_article(cur, id_pol, count=2)
if len(unpriced) < 2:
pytest.skip("Need 2 unpriced articles for dual-policy test")
(id_prod, codmat_prod), (id_sales, codmat_sales) = unpriced[0], unpriced[1]
# Save original cont values for cleanup
cur.execute("SELECT cont FROM nom_articole WHERE id_articol = :a",
{"a": id_prod})
cont_prod_orig = cur.fetchone()[0]
try:
cur.execute("UPDATE nom_articole SET cont = '341' "
"WHERE id_articol = :a", {"a": id_prod})
conn.commit()
order = _make_order(
"VEN-DUAL",
[(codmat_prod, 1, 50), (codmat_sales, 1, 80)],
)
app_settings = {}
validation = {
"mapped": set(),
"direct": {codmat_prod, codmat_sales},
"missing": set(),
"direct_id_map": {
codmat_prod: {"id_articol": id_prod, "cont": "341"},
codmat_sales: {"id_articol": id_sales, "cont": cont_prod_orig or "302"},
},
}
result = validation_service.pre_validate_order_prices(
orders=[order], app_settings=app_settings, conn=conn,
id_pol=id_pol, id_pol_productie=id_pol_productie,
validation=validation, cota_tva=21,
)
conn.commit()
policy_map = result["codmat_policy_map"]
assert policy_map.get(codmat_prod) == id_pol_productie, (
f"cont=341 article ({codmat_prod}) should route to "
f"productie={id_pol_productie}, got {policy_map.get(codmat_prod)}"
)
assert policy_map.get(codmat_sales) == id_pol, (
f"non-341 article ({codmat_sales}) should route to "
f"vanzare={id_pol}, got {policy_map.get(codmat_sales)}"
)
# Verify rows landed in the right policy
exists_prod_in_prod, _ = _has_price_entry(cur, id_pol_productie, id_prod)
exists_prod_in_sales, _ = _has_price_entry(cur, id_pol, id_prod)
exists_sales_in_sales, _ = _has_price_entry(cur, id_pol, id_sales)
exists_sales_in_prod, _ = _has_price_entry(cur, id_pol_productie, id_sales)
assert exists_prod_in_prod and not exists_prod_in_sales, (
"cont=341 row should be in productie policy only"
)
assert exists_sales_in_sales and not exists_sales_in_prod, (
"Non-341 row should be in sales policy only"
)
finally:
# Cleanup: restore cont, delete inserted PRET=0 rows
cur.execute("UPDATE nom_articole SET cont = :c "
"WHERE id_articol = :a",
{"c": cont_prod_orig, "a": id_prod})
cur.execute(
"DELETE FROM crm_politici_pret_art "
"WHERE id_pol IN (:p1, :p2) "
"AND id_articol IN (:a1, :a2) AND pret = 0",
{"p1": id_pol, "p2": id_pol_productie,
"a1": id_prod, "a2": id_sales},
)
conn.commit()