From 6dc300d339dc0a1553ccda96854ce441f97044b5 Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Sun, 19 Apr 2026 12:37:09 +0300 Subject: [PATCH] refactor: split bulk-sync into backend/routers/sync.py --- backend/main.py | 3 +- backend/routers/operations.py | 66 ------------------------- backend/routers/sync.py | 77 ++++++++++++++++++++++++++++++ backend/tests/test_offline_sync.py | 8 ++-- backend/tests/test_operations.py | 6 +-- 5 files changed, 86 insertions(+), 74 deletions(-) create mode 100644 backend/routers/sync.py diff --git a/backend/main.py b/backend/main.py index 28f0a5af..07c8f0a5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,7 +6,7 @@ from slowapi import Limiter from slowapi.util import get_remote_address from . import models from .database import engine -from .routers import items, operations, users, auth, categories +from .routers import items, operations, users, auth, sync, categories from .routers.admin import backups, config from .logger import log from .scheduler import scheduler, sync_scheduler_config @@ -88,6 +88,7 @@ app.include_router(items.router) app.include_router(operations.router) app.include_router(users.router) app.include_router(auth.router) +app.include_router(sync.router) app.include_router(categories.router) app.include_router(backups.router) app.include_router(config.router) diff --git a/backend/routers/operations.py b/backend/routers/operations.py index 7c01784b..1477070c 100644 --- a/backend/routers/operations.py +++ b/backend/routers/operations.py @@ -190,72 +190,6 @@ def bulk_check_out( db.commit() return results -@router.post("/bulk-sync") -def bulk_sync( - payload: schemas.SyncPayload, - db: Session = Depends(get_db), - current_user: auth.TokenData = Depends(auth.get_current_user) -): - """[C-01] Bulk sync offline operations — only for authenticated users.""" - results = {"success": [], "errors": []} - - for op in payload.operations: - try: - # DEDUPLICATION CHECK: If this UUID already exists, skip it - if op.uuid: - existing = db.query(models.AuditLog).filter(models.AuditLog.uuid == op.uuid).first() - if existing: - results["success"].append({"barcode": op.barcode, "type": op.type, "note": "Already synced"}) - continue - - item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first() - if not item: - results["errors"].append({"barcode": op.barcode, "error": "Item not found"}) - continue - - if op.type == "CHECK_IN": - item.quantity += op.quantity - change = op.quantity - elif op.type == "CHECK_OUT" or op.type == "TRASH": - if item.quantity < op.quantity: - results["errors"].append({"barcode": op.barcode, "error": f"Insufficient stock (Available: {item.quantity})"}) - continue - item.quantity -= op.quantity - change = -op.quantity - else: - results["errors"].append({"barcode": op.barcode, "error": f"Invalid operation type: {op.type}"}) - continue - - # Log audit with original offline timestamp and UUID - item_snapshot = { - "barcode": item.barcode, - "name": item.name, - "category": item.category, - "part_number": item.part_number - } - - audit = models.AuditLog( - user_id=current_user.sub, - action=op.type, - target_item_id=item.id, - target_item_name=item.name, - target_item_pn=item.part_number, - target_item_barcode=item.barcode, - target_snapshot=json.dumps(item_snapshot), - quantity_change=change, - timestamp=op.timestamp, - uuid=op.uuid, - details="Offline Synchronization" - ) - db.add(audit) - results["success"].append({"barcode": op.barcode, "type": op.type}) - - except Exception as e: - results["errors"].append({"barcode": op.barcode, "error": str(e)}) - - db.commit() - return results - @router.get("/logs", response_model=List[schemas.AuditLogResponse]) def get_logs( limit: int = 50, diff --git a/backend/routers/sync.py b/backend/routers/sync.py new file mode 100644 index 00000000..c6985cf9 --- /dev/null +++ b/backend/routers/sync.py @@ -0,0 +1,77 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from .. import models, schemas, auth +from ..database import get_db +import json + +router = APIRouter( + prefix="/sync", + tags=["Sync"] +) + + +@router.post("/bulk-sync") +def bulk_sync( + payload: schemas.SyncPayload, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Bulk sync offline operations — only for authenticated users.""" + results = {"success": [], "errors": []} + + for op in payload.operations: + try: + # DEDUPLICATION CHECK: If this UUID already exists, skip it + if op.uuid: + existing = db.query(models.AuditLog).filter(models.AuditLog.uuid == op.uuid).first() + if existing: + results["success"].append({"barcode": op.barcode, "type": op.type, "note": "Already synced"}) + continue + + item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first() + if not item: + results["errors"].append({"barcode": op.barcode, "error": "Item not found"}) + continue + + if op.type == "CHECK_IN": + item.quantity += op.quantity + change = op.quantity + elif op.type == "CHECK_OUT" or op.type == "TRASH": + if item.quantity < op.quantity: + results["errors"].append({"barcode": op.barcode, "error": f"Insufficient stock (Available: {item.quantity})"}) + continue + item.quantity -= op.quantity + change = -op.quantity + else: + results["errors"].append({"barcode": op.barcode, "error": f"Invalid operation type: {op.type}"}) + continue + + # Log audit with original offline timestamp and UUID + item_snapshot = { + "barcode": item.barcode, + "name": item.name, + "category": item.category, + "part_number": item.part_number + } + + audit = models.AuditLog( + user_id=current_user.sub, + action=op.type, + target_item_id=item.id, + target_item_name=item.name, + target_item_pn=item.part_number, + target_item_barcode=item.barcode, + target_snapshot=json.dumps(item_snapshot), + quantity_change=change, + timestamp=op.timestamp, + uuid=op.uuid, + details="Offline Synchronization" + ) + db.add(audit) + results["success"].append({"barcode": op.barcode, "type": op.type}) + + except Exception as e: + results["errors"].append({"barcode": op.barcode, "error": str(e)}) + + db.commit() + return results diff --git a/backend/tests/test_offline_sync.py b/backend/tests/test_offline_sync.py index e7e827db..9504c02e 100644 --- a/backend/tests/test_offline_sync.py +++ b/backend/tests/test_offline_sync.py @@ -19,7 +19,7 @@ class TestOfflineSync: user = test_db.query(User).filter(User.username == "user").first() operation_uuid = str(uuid4()) response = test_client.post( - "/operations/bulk-sync", + "/sync/bulk-sync", json={ "user_id": user.id, "operations": [ @@ -65,7 +65,7 @@ class TestOfflineSync: # First sync response1 = test_client.post( - "/operations/bulk-sync", json=payload, + "/sync/bulk-sync", json=payload, headers={"Authorization": f"Bearer {user_token}"} ) assert response1.status_code == status.HTTP_200_OK @@ -73,7 +73,7 @@ class TestOfflineSync: # Second sync (same UUID) — returns "Already synced" note response2 = test_client.post( - "/operations/bulk-sync", json=payload, + "/sync/bulk-sync", json=payload, headers={"Authorization": f"Bearer {user_token}"} ) assert response2.status_code == status.HTTP_200_OK @@ -90,7 +90,7 @@ class TestOfflineSync: user = test_db.query(User).filter(User.username == "user").first() response = test_client.post( - "/operations/bulk-sync", + "/sync/bulk-sync", json={ "user_id": user.id, "operations": [ diff --git a/backend/tests/test_operations.py b/backend/tests/test_operations.py index 2f9a0c20..4ae85ef9 100644 --- a/backend/tests/test_operations.py +++ b/backend/tests/test_operations.py @@ -103,7 +103,7 @@ class TestBulkSync: user = test_db.query(User).filter(User.username == "user").first() response = test_client.post( - "/operations/bulk-sync", + "/sync/bulk-sync", json={ "user_id": user.id, "operations": [ @@ -141,7 +141,7 @@ class TestBulkSync: # Sync once response1 = test_client.post( - "/operations/bulk-sync", json=operation, + "/sync/bulk-sync", json=operation, headers={"Authorization": f"Bearer {user_token}"} ) assert response1.status_code == status.HTTP_200_OK @@ -149,7 +149,7 @@ class TestBulkSync: # Sync again with same UUID — should be idempotent (returns "Already synced") response2 = test_client.post( - "/operations/bulk-sync", json=operation, + "/sync/bulk-sync", json=operation, headers={"Authorization": f"Bearer {user_token}"} ) assert response2.status_code == status.HTTP_200_OK