import asyncio import logging import uuid from datetime import datetime from . import order_reader, validation_service, import_service, sqlite_service logger = logging.getLogger(__name__) # Sync state _sync_lock = asyncio.Lock() _current_sync = None # dict with run_id, status, progress info async def get_sync_status(): """Get current sync status.""" if _current_sync: return {**_current_sync} return {"status": "idle"} async def run_sync(id_pol: int = None, id_sectie: int = None) -> dict: """Run a full sync cycle. Returns summary dict.""" global _current_sync if _sync_lock.locked(): return {"error": "Sync already running"} async with _sync_lock: run_id = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + uuid.uuid4().hex[:6] _current_sync = { "run_id": run_id, "status": "running", "started_at": datetime.now().isoformat(), "progress": "Reading JSON files..." } try: # Step 1: Read orders orders, json_count = order_reader.read_json_orders() await sqlite_service.create_sync_run(run_id, json_count) if not orders: await sqlite_service.update_sync_run(run_id, "completed", 0, 0, 0, 0) _current_sync = None return { "run_id": run_id, "status": "completed", "message": "No orders found", "json_files": json_count } _current_sync["progress"] = f"Validating {len(orders)} orders..." # Step 2: Validate SKUs (blocking Oracle call -> run in thread) all_skus = order_reader.get_all_skus(orders) validation = await asyncio.to_thread(validation_service.validate_skus, all_skus) importable, skipped = validation_service.classify_orders(orders, validation) # Track missing SKUs for sku in validation["missing"]: product_name = "" for order in orders: for item in order.items: if item.sku == sku: product_name = item.name break if product_name: break await sqlite_service.track_missing_sku(sku, product_name) # Step 3: Record skipped orders for order, missing_skus in skipped: customer = order.billing.company_name or \ f"{order.billing.firstname} {order.billing.lastname}" await sqlite_service.add_import_order( sync_run_id=run_id, order_number=order.number, order_date=order.date, customer_name=customer, status="SKIPPED", missing_skus=missing_skus, items_count=len(order.items) ) # Step 4: Import valid orders imported_count = 0 error_count = 0 for i, order in enumerate(importable): _current_sync["progress"] = f"Importing {i+1}/{len(importable)}: #{order.number}" result = await asyncio.to_thread( import_service.import_single_order, order, id_pol=id_pol, id_sectie=id_sectie ) customer = order.billing.company_name or \ f"{order.billing.firstname} {order.billing.lastname}" if result["success"]: imported_count += 1 await sqlite_service.add_import_order( sync_run_id=run_id, order_number=order.number, order_date=order.date, customer_name=customer, status="IMPORTED", id_comanda=result["id_comanda"], id_partener=result["id_partener"], items_count=len(order.items) ) else: error_count += 1 await sqlite_service.add_import_order( sync_run_id=run_id, order_number=order.number, order_date=order.date, customer_name=customer, status="ERROR", id_partener=result.get("id_partener"), error_message=result["error"], items_count=len(order.items) ) # Safety: stop if too many errors if error_count > 10: logger.warning("Too many errors, stopping sync") break # Step 5: Update sync run status = "completed" if error_count <= 10 else "failed" await sqlite_service.update_sync_run( run_id, status, len(orders), imported_count, len(skipped), error_count ) summary = { "run_id": run_id, "status": status, "json_files": json_count, "total_orders": len(orders), "imported": imported_count, "skipped": len(skipped), "errors": error_count, "missing_skus": len(validation["missing"]) } logger.info( f"Sync {run_id} completed: {imported_count} imported, " f"{len(skipped)} skipped, {error_count} errors" ) return summary except Exception as e: logger.error(f"Sync {run_id} failed: {e}") await sqlite_service.update_sync_run(run_id, "failed", 0, 0, 0, 1) return {"run_id": run_id, "status": "failed", "error": str(e)} finally: _current_sync = None def stop_sync(): """Signal sync to stop. Currently sync runs to completion.""" # For now, sync runs are not cancellable mid-flight. # Future: use an asyncio.Event for cooperative cancellation. pass