Files
gomag-vending/api/tests/test_order_items_overwrite.py
Claude Agent 3bcb26b0bd
Some checks failed
Tests / fast-tests (push) Has been cancelled
Tests / full-tests (push) Has been cancelled
fix(import): 3 production bugs — items cache, CUI lookup, ANAF name
1. SQLite order_items overwrite on re-import (VELA CAFE #484669620):
   add_order_items, save_orders_batch, mark_order_deleted_in_roa now use
   DELETE + INSERT so GoMag quantity changes propagate to dashboard.

2. PL/SQL strict CUI lookup tolerates whitespace (FG COFFE #485065210):
   cauta_partener_dupa_cod_fiscal regex ^RO\d → ^RO\s*\d; IN-set uses
   canonical v_ro_cui. Platitor/neplatitor business rule preserved.
   Python defensive: re.sub whitespace collapse in determine_partner_data.

3. New PJ partners use ANAF official denumire (denumire_override) instead
   of GoMag company_name. Existing partners (found by CUI) untouched.

Tests: 18 new (5 SQLite unit, 8 Python unit, 5 Oracle PL/SQL). All green
locally: 228 unit + 26 oracle + 33 e2e.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:32:59 +00:00

197 lines
6.6 KiB
Python

"""
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
@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="IMPORTED",
)
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 — must purge items
# ===========================================================================
@pytest.mark.asyncio
async def test_mark_order_deleted_removes_items():
"""Soft-delete must remove order_items (no ghost rows)."""
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 purged
assert await _items_for("ORD-DEL") == []
# 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"] == "DELETED_IN_ROA"
assert row["id_comanda"] is None