fix(sync): sticky exclusion for DELETED_IN_ROA orders
Orders deleted via "Sterge" button were re-imported on the next sync because classify step only checked Oracle (sters=0), not SQLite status. Adds a filter step after cancellation handling that drops orders already marked DELETED_IN_ROA before validation. "Reimporta" remains the explicit override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1253,6 +1253,19 @@ async def get_all_imported_orders() -> list:
|
|||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_deleted_in_roa_order_numbers() -> set[str]:
|
||||||
|
"""Return set of order_numbers marked DELETED_IN_ROA (sticky-excluded from auto-sync)."""
|
||||||
|
db = await get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = await db.execute(
|
||||||
|
f"SELECT order_number FROM orders WHERE status = '{OrderStatus.DELETED_IN_ROA.value}'"
|
||||||
|
)
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return {r[0] for r in rows}
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
async def clear_order_invoice(order_number: str):
|
async def clear_order_invoice(order_number: str):
|
||||||
"""Clear cached invoice data when invoice was deleted in ROA."""
|
"""Clear cached invoice data when invoice was deleted in ROA."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
|
|||||||
@@ -476,6 +476,18 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
|
|
||||||
orders = active_orders
|
orders = active_orders
|
||||||
|
|
||||||
|
# ── Sticky exclusion: skip orders previously marked DELETED_IN_ROA ──
|
||||||
|
deleted_set = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
if deleted_set:
|
||||||
|
excluded_deleted = [o for o in orders if o.number in deleted_set]
|
||||||
|
orders = [o for o in orders if o.number not in deleted_set]
|
||||||
|
if excluded_deleted:
|
||||||
|
_log_line(run_id,
|
||||||
|
f"Excluse {len(excluded_deleted)} comenzi marcate DELETED_IN_ROA "
|
||||||
|
f"(stergeri sticky — foloseste 'Reimporta' pentru override)")
|
||||||
|
for o in excluded_deleted:
|
||||||
|
_log_line(run_id, f"#{o.number} [{o.date or '?'}] → IGNORAT (DELETED_IN_ROA)")
|
||||||
|
|
||||||
if not orders:
|
if not orders:
|
||||||
_log_line(run_id, "Nicio comanda activa dupa filtrare anulate.")
|
_log_line(run_id, "Nicio comanda activa dupa filtrare anulate.")
|
||||||
await sqlite_service.update_sync_run(run_id, "completed", cancelled_count, 0, 0, 0)
|
await sqlite_service.update_sync_run(run_id, "completed", cancelled_count, 0, 0, 0)
|
||||||
|
|||||||
140
api/tests/test_sticky_deleted_filter.py
Normal file
140
api/tests/test_sticky_deleted_filter.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
Sticky DELETED_IN_ROA Filter Tests
|
||||||
|
===================================
|
||||||
|
Unit tests for get_deleted_in_roa_order_numbers() helper and integration
|
||||||
|
test for the sticky-exclusion filter applied in sync_service before
|
||||||
|
order classification.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
cd api && python -m pytest tests/test_sticky_deleted_filter.py -v
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.unit
|
||||||
|
|
||||||
|
_tmpdir = tempfile.mkdtemp()
|
||||||
|
os.environ.setdefault("FORCE_THIN_MODE", "true")
|
||||||
|
os.environ.setdefault("SQLITE_DB_PATH", os.path.join(_tmpdir, "test_sticky_deleted.db"))
|
||||||
|
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 _clean_orders():
|
||||||
|
"""Ensure schema exists, clear orders table before each test."""
|
||||||
|
database.init_sqlite()
|
||||||
|
db = await database.get_sqlite()
|
||||||
|
try:
|
||||||
|
await db.execute("DELETE FROM orders")
|
||||||
|
await db.commit()
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
async def _insert_order(order_number: str, status: str, id_comanda: int | None = None):
|
||||||
|
db = await database.get_sqlite()
|
||||||
|
try:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO orders (order_number, order_date, customer_name, status, id_comanda)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(order_number, "2026-04-22", "Test Customer", status, id_comanda),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_returns_empty_set_when_no_orders():
|
||||||
|
"""Helper unit: empty table → empty set."""
|
||||||
|
result = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
assert result == set()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_returns_only_deleted_in_roa_status():
|
||||||
|
"""Helper unit: filters only DELETED_IN_ROA, ignores other statuses."""
|
||||||
|
await _insert_order("ORD-1", OrderStatus.IMPORTED.value, id_comanda=100)
|
||||||
|
await _insert_order("ORD-2", OrderStatus.DELETED_IN_ROA.value)
|
||||||
|
await _insert_order("ORD-3", OrderStatus.CANCELLED.value)
|
||||||
|
await _insert_order("ORD-4", OrderStatus.ERROR.value)
|
||||||
|
await _insert_order("ORD-5", OrderStatus.DELETED_IN_ROA.value)
|
||||||
|
await _insert_order("ORD-6", OrderStatus.SKIPPED.value)
|
||||||
|
|
||||||
|
result = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
assert result == {"ORD-2", "ORD-5"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mark_order_deleted_then_helper_returns_it():
|
||||||
|
"""Integration: mark_order_deleted_in_roa → helper picks it up."""
|
||||||
|
await _insert_order("ORD-100", OrderStatus.IMPORTED.value, id_comanda=500)
|
||||||
|
|
||||||
|
before = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
assert "ORD-100" not in before
|
||||||
|
|
||||||
|
await sqlite_service.mark_order_deleted_in_roa("ORD-100")
|
||||||
|
|
||||||
|
after = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
assert "ORD-100" in after
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_filter_excludes_deleted_orders():
|
||||||
|
"""Integration: simulates sync filter step.
|
||||||
|
|
||||||
|
Pre-mark ORD-2 as DELETED_IN_ROA, run the same filter logic from
|
||||||
|
sync_service:478-489, assert ORD-2 is excluded.
|
||||||
|
"""
|
||||||
|
await _insert_order("ORD-1", OrderStatus.IMPORTED.value, id_comanda=100)
|
||||||
|
await _insert_order("ORD-2", OrderStatus.DELETED_IN_ROA.value)
|
||||||
|
await _insert_order("ORD-3", OrderStatus.IMPORTED.value, id_comanda=300)
|
||||||
|
|
||||||
|
incoming = [
|
||||||
|
type("O", (), {"number": "ORD-1", "date": "2026-04-22"})(),
|
||||||
|
type("O", (), {"number": "ORD-2", "date": "2026-04-22"})(),
|
||||||
|
type("O", (), {"number": "ORD-3", "date": "2026-04-22"})(),
|
||||||
|
type("O", (), {"number": "ORD-NEW", "date": "2026-04-22"})(),
|
||||||
|
]
|
||||||
|
|
||||||
|
deleted_set = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
excluded = [o for o in incoming if o.number in deleted_set]
|
||||||
|
survivors = [o for o in incoming if o.number not in deleted_set]
|
||||||
|
|
||||||
|
assert {o.number for o in excluded} == {"ORD-2"}
|
||||||
|
assert {o.number for o in survivors} == {"ORD-1", "ORD-3", "ORD-NEW"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_filter_with_no_deleted_is_noop():
|
||||||
|
"""Integration: deleted_set empty → all orders pass through."""
|
||||||
|
await _insert_order("ORD-1", OrderStatus.IMPORTED.value, id_comanda=100)
|
||||||
|
|
||||||
|
incoming = [
|
||||||
|
type("O", (), {"number": "ORD-1", "date": "2026-04-22"})(),
|
||||||
|
type("O", (), {"number": "ORD-NEW", "date": "2026-04-22"})(),
|
||||||
|
]
|
||||||
|
|
||||||
|
deleted_set = await sqlite_service.get_deleted_in_roa_order_numbers()
|
||||||
|
survivors = [o for o in incoming if o.number not in deleted_set]
|
||||||
|
|
||||||
|
assert deleted_set == set()
|
||||||
|
assert {o.number for o in survivors} == {"ORD-1", "ORD-NEW"}
|
||||||
Reference in New Issue
Block a user