refactor: split bulk-sync into backend/routers/sync.py

This commit is contained in:
2026-04-19 12:37:09 +03:00
parent 90e9a60640
commit 6dc300d339
5 changed files with 86 additions and 74 deletions

View File

@@ -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)

View File

@@ -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,

77
backend/routers/sync.py Normal file
View File

@@ -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

View File

@@ -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": [

View File

@@ -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