"""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()