feat(orders): add resync and delete order buttons
Resync soft-deletes from Oracle then re-imports from GoMag with fresh article data. Delete soft-deletes and marks DELETED_IN_ROA. Both have invoice safety gates (refuse if invoiced or Oracle unavailable). UI: split modal footer (Delete left, Resync+Close right), inline confirm pattern (no native confirm()), dashboard row hover action icons, disabled+tooltip for invoiced orders. 8 unit tests for safety gates and happy paths. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -899,3 +899,190 @@ class TestRefreshOrderAddress:
|
||||
data = res.json()
|
||||
assert data["adresa_livrare_roa"]["strada"] == "VASILE GOLDIS"
|
||||
assert data["adresa_livrare_roa"]["bloc"] == "30"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group N: Resync / Delete — safety gates and happy paths
|
||||
# ===========================================================================
|
||||
|
||||
from unittest.mock import AsyncMock # noqa: E402 (already imported MagicMock/patch above)
|
||||
|
||||
|
||||
def _make_order_detail(status='IMPORTED', id_comanda=12345, factura_numar=None):
|
||||
return {
|
||||
"order": {
|
||||
"order_number": "1001",
|
||||
"status": status,
|
||||
"id_comanda": id_comanda,
|
||||
"order_date": "2025-01-15T10:00:00",
|
||||
"customer_name": "Test Client",
|
||||
"factura_numar": factura_numar,
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
|
||||
|
||||
def _unlocked_lock():
|
||||
m = MagicMock()
|
||||
m.locked.return_value = False
|
||||
return m
|
||||
|
||||
|
||||
class TestResyncDeleteSafetyGates:
|
||||
"""Safety gates: invoiced orders and Oracle-down must be refused."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_delete_refuses_invoiced_order(self):
|
||||
"""delete_single_order → success=False when factura_numar is set."""
|
||||
from app.services import retry_service
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail(factura_numar='FAC-001'))), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.database.pool', new=MagicMock()):
|
||||
result = await retry_service.delete_single_order("1001")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "facturata" in result["message"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_delete_refuses_when_oracle_down(self):
|
||||
"""delete_single_order → success=False when database.pool is None."""
|
||||
from app.services import retry_service
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail())), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.database.pool', new=None):
|
||||
result = await retry_service.delete_single_order("1001")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "indisponibil" in result["message"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_resync_refuses_invoiced_order(self):
|
||||
"""resync_single_order → success=False when factura_numar is set."""
|
||||
from app.services import retry_service
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail(factura_numar='FAC-001'))), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.database.pool', new=MagicMock()):
|
||||
result = await retry_service.resync_single_order("1001", {})
|
||||
|
||||
assert result["success"] is False
|
||||
assert "facturata" in result["message"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_resync_refuses_wrong_status(self):
|
||||
"""resync_single_order → success=False when status is ERROR (not IMPORTED)."""
|
||||
from app.services import retry_service
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail(status='ERROR'))), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()):
|
||||
result = await retry_service.resync_single_order("1001", {})
|
||||
|
||||
assert result["success"] is False
|
||||
assert "ERROR" in result["message"]
|
||||
|
||||
|
||||
class TestResyncDeleteHappyPaths:
|
||||
"""Happy paths and partial-failure recovery for resync / delete / retry."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_resync_happy_path(self):
|
||||
"""resync_single_order succeeds: IMPORTED, no invoice, soft-delete OK, reimport OK."""
|
||||
from app.services import retry_service
|
||||
|
||||
async def _fake_to_thread(fn, *args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail())), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.database.pool', new=MagicMock()), \
|
||||
patch('app.services.invoice_service.check_invoices_for_orders',
|
||||
new=MagicMock(return_value={})), \
|
||||
patch('app.services.import_service.soft_delete_order_in_roa',
|
||||
new=MagicMock(return_value={"success": True})), \
|
||||
patch('app.services.sqlite_service.mark_order_deleted_in_roa', new=AsyncMock()), \
|
||||
patch('asyncio.to_thread', new=_fake_to_thread), \
|
||||
patch('app.services.retry_service._download_and_reimport',
|
||||
new=AsyncMock(return_value={"success": True,
|
||||
"message": "Comanda reimportata cu succes"})):
|
||||
result = await retry_service.resync_single_order("1001", {})
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_delete_happy_path(self):
|
||||
"""delete_single_order succeeds: IMPORTED, no invoice, soft-delete OK."""
|
||||
from app.services import retry_service
|
||||
|
||||
async def _fake_to_thread(fn, *args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail())), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.database.pool', new=MagicMock()), \
|
||||
patch('app.services.invoice_service.check_invoices_for_orders',
|
||||
new=MagicMock(return_value={})), \
|
||||
patch('app.services.import_service.soft_delete_order_in_roa',
|
||||
new=MagicMock(return_value={"success": True})), \
|
||||
patch('app.services.sqlite_service.mark_order_deleted_in_roa', new=AsyncMock()), \
|
||||
patch('asyncio.to_thread', new=_fake_to_thread):
|
||||
result = await retry_service.delete_single_order("1001")
|
||||
|
||||
assert result["success"] is True
|
||||
assert "stearsa" in result["message"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_retry_accepts_deleted_in_roa(self):
|
||||
"""retry_single_order proceeds (not refused) for DELETED_IN_ROA status."""
|
||||
from app.services import retry_service
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail(status='DELETED_IN_ROA'))), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.services.retry_service._download_and_reimport',
|
||||
new=AsyncMock(return_value={"success": True, "message": "ok"})):
|
||||
result = await retry_service.retry_single_order("1001", {})
|
||||
|
||||
assert result["success"] is True
|
||||
# Must NOT be refused with the "retry permis" status guard message
|
||||
assert "retry permis" not in result.get("message", "").lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.unit
|
||||
async def test_resync_partial_failure(self):
|
||||
"""resync returns success=False when soft-delete OK but reimport fails."""
|
||||
from app.services import retry_service
|
||||
|
||||
async def _fake_to_thread(fn, *args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
with patch('app.services.sqlite_service.get_order_detail',
|
||||
new=AsyncMock(return_value=_make_order_detail())), \
|
||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||
patch('app.database.pool', new=MagicMock()), \
|
||||
patch('app.services.invoice_service.check_invoices_for_orders',
|
||||
new=MagicMock(return_value={})), \
|
||||
patch('app.services.import_service.soft_delete_order_in_roa',
|
||||
new=MagicMock(return_value={"success": True})), \
|
||||
patch('app.services.sqlite_service.mark_order_deleted_in_roa', new=AsyncMock()), \
|
||||
patch('asyncio.to_thread', new=_fake_to_thread), \
|
||||
patch('app.services.retry_service._download_and_reimport',
|
||||
new=AsyncMock(return_value={"success": False, "message": "download failed"})):
|
||||
result = await retry_service.resync_single_order("1001", {})
|
||||
|
||||
assert result["success"] is False
|
||||
assert "reimport" in result["message"].lower()
|
||||
|
||||
Reference in New Issue
Block a user