""" Order Items Overwrite Regression Tests ======================================== Re-import must replace SQLite order_items (not INSERT OR IGNORE) so quantity changes in GoMag propagate to the dashboard. Regression for VELA CAFE #484669620. Also: soft-delete (mark_order_deleted_in_roa) must purge stale items. Run: cd api && python -m pytest tests/test_order_items_overwrite.py -v """ import os import sys import tempfile import pytest pytestmark = pytest.mark.unit # --- Set env vars BEFORE any app import --- _tmpdir = tempfile.mkdtemp() _sqlite_path = os.path.join(_tmpdir, "test_items.db") os.environ.setdefault("FORCE_THIN_MODE", "true") os.environ.setdefault("SQLITE_DB_PATH", _sqlite_path) 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) _api_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _api_dir not in sys.path: sys.path.insert(0, _api_dir) from app import database from app.services import sqlite_service from app.constants import OrderStatus @pytest.fixture(autouse=True) async def _init_db(): database.init_sqlite() # Clean state before each test db = await sqlite_service.get_sqlite() try: await db.execute("DELETE FROM order_items") await db.execute("DELETE FROM sync_run_orders") await db.execute("DELETE FROM orders") await db.execute("DELETE FROM sync_runs") await db.commit() finally: await db.close() yield def _item(sku="SKU1", qty=1.0, price=10.0): return { "sku": sku, "product_name": f"Product {sku}", "quantity": qty, "price": price, "baseprice": price, "vat": 19, "mapping_status": "direct", "codmat": None, "id_articol": None, "cantitate_roa": None, } async def _seed_order(order_number="TEST-001"): """Create an orders row so FK constraints (if any) pass.""" await sqlite_service.upsert_order( sync_run_id="test-run", order_number=order_number, order_date="2026-01-01", customer_name="Test", status=OrderStatus.IMPORTED.value, ) async def _items_for(order_number): return await sqlite_service.get_order_items(order_number) # =========================================================================== # add_order_items — replace semantics # =========================================================================== @pytest.mark.asyncio async def test_add_order_items_deletes_before_insert(): """Re-import with changed quantities must overwrite, not preserve old rows.""" await _seed_order("ORD-A") # Initial import: 3 items await sqlite_service.add_order_items("ORD-A", [ _item("SKU1", qty=5), _item("SKU2", qty=10), _item("SKU3", qty=2), ]) rows = await _items_for("ORD-A") assert len(rows) == 3 # Re-import: only 2 items, different quantities (simulates user edit in GoMag) await sqlite_service.add_order_items("ORD-A", [ _item("SKU1", qty=99), _item("SKU4", qty=1), ]) rows = await _items_for("ORD-A") skus = {r["sku"]: r["quantity"] for r in rows} assert skus == {"SKU1": 99, "SKU4": 1}, f"old rows leaked: {skus}" @pytest.mark.asyncio async def test_add_order_items_empty_list_no_delete(): """Empty list is a no-op — existing items must remain (early return).""" await _seed_order("ORD-B") await sqlite_service.add_order_items("ORD-B", [_item("SKU1", qty=5)]) await sqlite_service.add_order_items("ORD-B", []) # should not wipe rows = await _items_for("ORD-B") assert len(rows) == 1 assert rows[0]["sku"] == "SKU1" @pytest.mark.asyncio async def test_add_order_items_isolation_between_orders(): """add_order_items on ORD-A must not affect ORD-B items.""" await _seed_order("ORD-A") await _seed_order("ORD-B") await sqlite_service.add_order_items("ORD-A", [_item("SKU1", qty=5)]) await sqlite_service.add_order_items("ORD-B", [_item("SKU2", qty=7)]) # Re-import A await sqlite_service.add_order_items("ORD-A", [_item("SKU1", qty=99)]) rows_a = await _items_for("ORD-A") rows_b = await _items_for("ORD-B") assert len(rows_a) == 1 and rows_a[0]["quantity"] == 99 assert len(rows_b) == 1 and rows_b[0]["quantity"] == 7 # =========================================================================== # save_orders_batch — replace semantics for batch flow # =========================================================================== @pytest.mark.asyncio async def test_save_orders_batch_overwrite(): """save_orders_batch must also replace existing items for re-run order numbers.""" await _seed_order("ORD-BATCH") await sqlite_service.add_order_items("ORD-BATCH", [ _item("SKU_OLD", qty=1), ]) assert len(await _items_for("ORD-BATCH")) == 1 batch = [{ "sync_run_id": "run-1", "order_number": "ORD-BATCH", "status_at_run": "PENDING", "order_date": "2026-01-02", "customer_name": "Batch", "status": "PENDING", "items": [_item("SKU_NEW_1", qty=3), _item("SKU_NEW_2", qty=4)], }] # save_orders_batch requires sync_runs row first db = await sqlite_service.get_sqlite() try: await db.execute( "INSERT OR IGNORE INTO sync_runs (run_id, started_at, status) VALUES (?, datetime('now'), 'running')", ("run-1",), ) await db.commit() finally: await db.close() await sqlite_service.save_orders_batch(batch) rows = await _items_for("ORD-BATCH") skus = {r["sku"] for r in rows} assert skus == {"SKU_NEW_1", "SKU_NEW_2"}, f"old items leaked: {skus}" # =========================================================================== # mark_order_deleted_in_roa — preserves items so detail view stays useful # =========================================================================== @pytest.mark.asyncio async def test_mark_order_deleted_preserves_items(): """Soft-delete keeps order_items so the detail view shows what was ordered. On 'Reimporta', add_order_items replaces them (DELETE+INSERT inside _safe_upsert_order_items). """ await _seed_order("ORD-DEL") await sqlite_service.add_order_items("ORD-DEL", [ _item("SKU1", qty=5), _item("SKU2", qty=3), ]) assert len(await _items_for("ORD-DEL")) == 2 await sqlite_service.mark_order_deleted_in_roa("ORD-DEL") # Items preserved — detail view can still display them alongside "Comanda stearsa din ROA" items = await _items_for("ORD-DEL") assert len(items) == 2 assert {i["sku"] for i in items} == {"SKU1", "SKU2"} # Orders row still present with DELETED_IN_ROA status (not hard-deleted) db = await sqlite_service.get_sqlite() try: cur = await db.execute("SELECT status, id_comanda FROM orders WHERE order_number = ?", ("ORD-DEL",)) row = await cur.fetchone() finally: await db.close() assert row is not None assert row["status"] == OrderStatus.DELETED_IN_ROA.value assert row["id_comanda"] is None