2025-08-16 15:00:36 +02:00
|
|
|
# services/pos/app/api/sync.py
|
|
|
|
|
"""
|
|
|
|
|
POS Sync API Endpoints
|
|
|
|
|
Handles data synchronization with POS systems
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Body
|
|
|
|
|
from typing import List, Optional, Dict, Any
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
import structlog
|
|
|
|
|
|
|
|
|
|
from app.core.database import get_db
|
2025-09-04 23:19:53 +02:00
|
|
|
from shared.auth.decorators import get_current_user_dep
|
2025-08-16 15:00:36 +02:00
|
|
|
|
|
|
|
|
router = APIRouter(tags=["sync"])
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/pos/configurations/{config_id}/sync")
|
|
|
|
|
async def trigger_sync(
|
|
|
|
|
sync_request: Dict[str, Any] = Body(...),
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
config_id: UUID = Path(..., description="Configuration ID"),
|
|
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Trigger manual synchronization with POS system"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
sync_type = sync_request.get("sync_type", "incremental") # full, incremental
|
|
|
|
|
data_types = sync_request.get("data_types", ["transactions"]) # transactions, products, customers
|
|
|
|
|
from_date = sync_request.get("from_date")
|
|
|
|
|
to_date = sync_request.get("to_date")
|
|
|
|
|
|
|
|
|
|
logger.info("Manual sync triggered",
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
config_id=config_id,
|
|
|
|
|
sync_type=sync_type,
|
|
|
|
|
data_types=data_types,
|
|
|
|
|
user_id=current_user.get("user_id"))
|
|
|
|
|
|
|
|
|
|
# TODO: Implement sync logic
|
|
|
|
|
# TODO: Queue sync job for background processing
|
|
|
|
|
# TODO: Return sync job ID for tracking
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"message": "Sync triggered successfully",
|
|
|
|
|
"sync_id": "placeholder-sync-id",
|
|
|
|
|
"status": "queued",
|
|
|
|
|
"sync_type": sync_type,
|
|
|
|
|
"data_types": data_types,
|
|
|
|
|
"estimated_duration": "5-10 minutes"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to trigger sync", error=str(e),
|
|
|
|
|
tenant_id=tenant_id, config_id=config_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to trigger sync: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/pos/configurations/{config_id}/sync/status")
|
|
|
|
|
async def get_sync_status(
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
config_id: UUID = Path(..., description="Configuration ID"),
|
|
|
|
|
limit: int = Query(10, ge=1, le=100, description="Number of sync logs to return"),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get synchronization status and recent sync history"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
# TODO: Get sync status from database
|
|
|
|
|
# TODO: Get recent sync logs
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"current_sync": None,
|
|
|
|
|
"last_successful_sync": None,
|
|
|
|
|
"recent_syncs": [],
|
|
|
|
|
"sync_health": {
|
|
|
|
|
"status": "healthy",
|
|
|
|
|
"success_rate": 95.5,
|
|
|
|
|
"average_duration_minutes": 3.2,
|
|
|
|
|
"last_error": None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to get sync status", error=str(e),
|
|
|
|
|
tenant_id=tenant_id, config_id=config_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get sync status: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/pos/configurations/{config_id}/sync/logs")
|
|
|
|
|
async def get_sync_logs(
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
config_id: UUID = Path(..., description="Configuration ID"),
|
|
|
|
|
limit: int = Query(50, ge=1, le=200, description="Number of logs to return"),
|
|
|
|
|
offset: int = Query(0, ge=0, description="Number of logs to skip"),
|
|
|
|
|
status: Optional[str] = Query(None, description="Filter by sync status"),
|
|
|
|
|
sync_type: Optional[str] = Query(None, description="Filter by sync type"),
|
|
|
|
|
data_type: Optional[str] = Query(None, description="Filter by data type"),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get detailed sync logs"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
# TODO: Implement log retrieval with filters
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"logs": [],
|
|
|
|
|
"total": 0,
|
|
|
|
|
"has_more": False
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to get sync logs", error=str(e),
|
|
|
|
|
tenant_id=tenant_id, config_id=config_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get sync logs: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/pos/transactions")
|
|
|
|
|
async def get_pos_transactions(
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
pos_system: Optional[str] = Query(None, description="Filter by POS system"),
|
|
|
|
|
start_date: Optional[datetime] = Query(None, description="Start date filter"),
|
|
|
|
|
end_date: Optional[datetime] = Query(None, description="End date filter"),
|
|
|
|
|
status: Optional[str] = Query(None, description="Filter by transaction status"),
|
|
|
|
|
is_synced: Optional[bool] = Query(None, description="Filter by sync status"),
|
|
|
|
|
limit: int = Query(50, ge=1, le=200, description="Number of transactions to return"),
|
|
|
|
|
offset: int = Query(0, ge=0, description="Number of transactions to skip"),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get POS transactions for a tenant"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
# TODO: Implement transaction retrieval with filters
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"transactions": [],
|
|
|
|
|
"total": 0,
|
|
|
|
|
"has_more": False,
|
|
|
|
|
"summary": {
|
|
|
|
|
"total_amount": 0,
|
|
|
|
|
"transaction_count": 0,
|
|
|
|
|
"sync_status": {
|
|
|
|
|
"synced": 0,
|
|
|
|
|
"pending": 0,
|
|
|
|
|
"failed": 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to get POS transactions", error=str(e), tenant_id=tenant_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get POS transactions: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/pos/transactions/{transaction_id}/sync")
|
|
|
|
|
async def sync_single_transaction(
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
transaction_id: UUID = Path(..., description="Transaction ID"),
|
|
|
|
|
force: bool = Query(False, description="Force sync even if already synced"),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Manually sync a single transaction to sales service"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
# TODO: Implement single transaction sync
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"message": "Transaction sync completed",
|
|
|
|
|
"transaction_id": str(transaction_id),
|
|
|
|
|
"sync_status": "success",
|
|
|
|
|
"sales_record_id": "placeholder"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to sync transaction", error=str(e),
|
|
|
|
|
tenant_id=tenant_id, transaction_id=transaction_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to sync transaction: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/pos/analytics/sync-performance")
|
|
|
|
|
async def get_sync_analytics(
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
days: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get sync performance analytics"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
# TODO: Implement analytics calculation
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"period_days": days,
|
|
|
|
|
"total_syncs": 0,
|
|
|
|
|
"successful_syncs": 0,
|
|
|
|
|
"failed_syncs": 0,
|
|
|
|
|
"success_rate": 0.0,
|
|
|
|
|
"average_duration_minutes": 0.0,
|
|
|
|
|
"total_transactions_synced": 0,
|
|
|
|
|
"total_revenue_synced": 0.0,
|
|
|
|
|
"sync_frequency": {
|
|
|
|
|
"daily_average": 0.0,
|
|
|
|
|
"peak_day": None,
|
|
|
|
|
"peak_count": 0
|
|
|
|
|
},
|
|
|
|
|
"error_analysis": {
|
|
|
|
|
"common_errors": [],
|
|
|
|
|
"error_trends": []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to get sync analytics", error=str(e), tenant_id=tenant_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get sync analytics: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/pos/data/resync")
|
|
|
|
|
async def resync_failed_transactions(
|
|
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
days_back: int = Query(7, ge=1, le=90, description="How many days back to resync"),
|
|
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Resync failed transactions from the specified time period"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
logger.info("Resync failed transactions requested",
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
days_back=days_back,
|
|
|
|
|
user_id=current_user.get("user_id"))
|
|
|
|
|
|
|
|
|
|
# TODO: Implement failed transaction resync
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"message": "Resync job queued successfully",
|
|
|
|
|
"job_id": "placeholder-resync-job-id",
|
|
|
|
|
"scope": f"Failed transactions from last {days_back} days",
|
|
|
|
|
"estimated_transactions": 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to queue resync job", error=str(e), tenant_id=tenant_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to queue resync job: {str(e)}")
|