diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index d01521d..3a7f6b4 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -484,6 +484,16 @@ async def order_detail(order_number: str): return detail +@router.post("/api/orders/{order_number}/retry") +async def retry_order(order_number: str): + """Retry importing a failed/skipped order.""" + from ..services import retry_service + + app_settings = await sqlite_service.get_app_settings() + result = await retry_service.retry_single_order(order_number, app_settings) + return result + + @router.get("/api/dashboard/orders") async def dashboard_orders(page: int = 1, per_page: int = 50, search: str = "", status: str = "all", diff --git a/api/app/services/retry_service.py b/api/app/services/retry_service.py new file mode 100644 index 0000000..2f72d14 --- /dev/null +++ b/api/app/services/retry_service.py @@ -0,0 +1,131 @@ +"""Retry service — re-import individual failed/skipped orders.""" +import asyncio +import logging +import tempfile +from datetime import datetime, timedelta + +logger = logging.getLogger(__name__) + + +async def retry_single_order(order_number: str, app_settings: dict) -> dict: + """Re-download and re-import a single order from GoMag. + + Steps: + 1. Read order from SQLite to get order_date / customer_name + 2. Check sync lock (no retry during active sync) + 3. Download narrow date range from GoMag (order_date ± 1 day) + 4. Find the specific order in downloaded data + 5. Run import_single_order() + 6. Update status in SQLite + + Returns: {"success": bool, "message": str, "status": str|None} + """ + from . import sqlite_service, sync_service, gomag_client, import_service, order_reader + + # Check sync lock + if sync_service._sync_lock.locked(): + return {"success": False, "message": "Sync in curs — asteapta finalizarea"} + + # Get order from SQLite + detail = await sqlite_service.get_order_detail(order_number) + if not detail: + return {"success": False, "message": "Comanda nu a fost gasita"} + + order_data = detail["order"] + status = order_data.get("status", "") + if status not in ("ERROR", "SKIPPED"): + return {"success": False, "message": f"Retry permis doar pentru ERROR/SKIPPED (status actual: {status})"} + + order_date_str = order_data.get("order_date", "") + customer_name = order_data.get("customer_name", "") + + # Parse order date for narrow download window + try: + order_date = datetime.fromisoformat(order_date_str.replace("Z", "+00:00")).date() + except (ValueError, AttributeError): + order_date = datetime.now().date() - timedelta(days=1) + + gomag_key = app_settings.get("gomag_api_key") or None + gomag_shop = app_settings.get("gomag_api_shop") or None + + with tempfile.TemporaryDirectory() as tmp_dir: + try: + today = datetime.now().date() + days_back = (today - order_date).days + 1 + if days_back < 2: + days_back = 2 + + await gomag_client.download_orders( + tmp_dir, days_back=days_back, + api_key=gomag_key, api_shop=gomag_shop, + limit=200, + ) + except Exception as e: + logger.error(f"Retry download failed for {order_number}: {e}") + return {"success": False, "message": f"Eroare download GoMag: {e}"} + + # Find the specific order in downloaded data + target_order = None + orders, _ = order_reader.read_json_orders(json_dir=tmp_dir) + for o in orders: + if str(o.number) == str(order_number): + target_order = o + break + + if not target_order: + return {"success": False, "message": f"Comanda {order_number} nu a fost gasita in GoMag API"} + + # Import the order + id_pol = int(app_settings.get("id_pol") or 0) + id_sectie = int(app_settings.get("id_sectie") or 0) + id_gestiune = app_settings.get("id_gestiune", "") + id_gestiuni = [int(g.strip()) for g in id_gestiune.split(",") if g.strip()] if id_gestiune else None + + try: + result = await asyncio.to_thread( + import_service.import_single_order, + target_order, id_pol=id_pol, id_sectie=id_sectie, + app_settings=app_settings, id_gestiuni=id_gestiuni + ) + except Exception as e: + logger.error(f"Retry import failed for {order_number}: {e}") + await sqlite_service.upsert_order( + sync_run_id="retry", + order_number=order_number, + order_date=order_date_str, + customer_name=customer_name, + status="ERROR", + error_message=f"Retry failed: {e}", + ) + return {"success": False, "message": f"Eroare import: {e}"} + + if result.get("success"): + await sqlite_service.upsert_order( + sync_run_id="retry", + order_number=order_number, + order_date=order_date_str, + customer_name=customer_name, + status="IMPORTED", + id_comanda=result.get("id_comanda"), + id_partener=result.get("id_partener"), + error_message=None, + ) + if result.get("id_adresa_facturare") or result.get("id_adresa_livrare"): + await sqlite_service.update_import_order_addresses( + order_number=order_number, + id_adresa_facturare=result.get("id_adresa_facturare"), + id_adresa_livrare=result.get("id_adresa_livrare"), + ) + logger.info(f"Retry successful for order {order_number} → IMPORTED") + return {"success": True, "message": "Comanda reimportata cu succes", "status": "IMPORTED"} + else: + error = result.get("error", "Unknown error") + await sqlite_service.upsert_order( + sync_run_id="retry", + order_number=order_number, + order_date=order_date_str, + customer_name=customer_name, + status="ERROR", + error_message=f"Retry: {error}", + ) + return {"success": False, "message": f"Import esuat: {error}", "status": "ERROR"} diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js index b150c09..6ee13e1 100644 --- a/api/app/static/js/shared.js +++ b/api/app/static/js/shared.js @@ -471,6 +471,8 @@ async function renderOrderDetailModal(orderNumber, opts) { document.getElementById('detailIdAdresaLivr').textContent = '-'; document.getElementById('detailItemsBody').innerHTML = '