REFACTOR ALL APIs
This commit is contained in:
93
services/pos/app/api/analytics.py
Normal file
93
services/pos/app/api/analytics.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
POS Service Analytics API Endpoints
|
||||
ANALYTICS layer - Channel and sync performance analytics
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
|
||||
from app.core.database import get_db
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('pos')
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("sync-performance"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_sync_performance_analytics(
|
||||
tenant_id: UUID = Path(...),
|
||||
days: int = Query(30, ge=1, le=365),
|
||||
config_id: Optional[UUID] = Query(None),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Analyze sync performance metrics"""
|
||||
try:
|
||||
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 analytics: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("channel-performance"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_channel_performance_analytics(
|
||||
tenant_id: UUID = Path(...),
|
||||
days: int = Query(30, ge=1, le=365),
|
||||
pos_system: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Analyze POS channel performance by system"""
|
||||
try:
|
||||
return {
|
||||
"period_days": days,
|
||||
"pos_system": pos_system,
|
||||
"channel_metrics": {
|
||||
"total_transactions": 0,
|
||||
"total_revenue": 0.0,
|
||||
"average_transaction_value": 0.0,
|
||||
"transaction_growth_rate": 0.0
|
||||
},
|
||||
"system_breakdown": [],
|
||||
"performance_trends": {
|
||||
"daily_trends": [],
|
||||
"hourly_trends": [],
|
||||
"day_of_week_trends": []
|
||||
},
|
||||
"top_performing_channels": []
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get channel analytics", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get channel analytics: {str(e)}")
|
||||
@@ -1,6 +1,6 @@
|
||||
# services/pos/app/api/pos_config.py
|
||||
"""
|
||||
POS Configuration API Endpoints
|
||||
ATOMIC layer - Basic CRUD operations for POS configurations
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
@@ -10,137 +10,143 @@ import structlog
|
||||
|
||||
from app.core.database import get_db
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role, admin_role_required
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
router = APIRouter(tags=["pos-config"])
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('pos')
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/pos/configurations")
|
||||
async def get_pos_configurations(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
pos_system: Optional[str] = Query(None, description="Filter by POS system"),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
@router.get(
|
||||
route_builder.build_base_route("configurations"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def list_pos_configurations(
|
||||
tenant_id: UUID = Path(...),
|
||||
pos_system: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Get POS configurations for a tenant"""
|
||||
"""List all POS configurations for a tenant"""
|
||||
try:
|
||||
|
||||
# TODO: Implement configuration retrieval
|
||||
# This is a placeholder for the basic structure
|
||||
return {
|
||||
"configurations": [],
|
||||
"total": 0,
|
||||
"supported_systems": ["square", "toast", "lightspeed"]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get POS configurations", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get POS configurations: {str(e)}")
|
||||
logger.error("Failed to list POS configurations", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list configurations: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/pos/configurations")
|
||||
@router.post(
|
||||
route_builder.build_base_route("configurations"),
|
||||
response_model=dict,
|
||||
status_code=201
|
||||
)
|
||||
@admin_role_required
|
||||
async def create_pos_configuration(
|
||||
configuration_data: Dict[str, Any],
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Create a new POS configuration"""
|
||||
"""Create a new POS configuration (Admin/Owner only)"""
|
||||
try:
|
||||
|
||||
# TODO: Implement configuration creation
|
||||
logger.info("Creating POS configuration",
|
||||
logger.info("Creating POS configuration",
|
||||
tenant_id=tenant_id,
|
||||
pos_system=configuration_data.get("pos_system"),
|
||||
user_id=current_user.get("user_id"))
|
||||
|
||||
return {"message": "POS configuration created successfully", "id": "placeholder"}
|
||||
|
||||
|
||||
return {
|
||||
"message": "POS configuration created successfully",
|
||||
"id": "placeholder",
|
||||
"pos_system": configuration_data.get("pos_system")
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to create POS configuration", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create POS configuration: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create configuration: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/pos/configurations/{config_id}")
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("configurations", "config_id"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_pos_configuration(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
config_id: UUID = Path(..., description="Configuration ID"),
|
||||
tenant_id: UUID = Path(...),
|
||||
config_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Get a specific POS configuration"""
|
||||
try:
|
||||
|
||||
# TODO: Implement configuration retrieval
|
||||
return {"message": "Configuration details", "id": str(config_id)}
|
||||
|
||||
return {
|
||||
"id": str(config_id),
|
||||
"tenant_id": str(tenant_id),
|
||||
"pos_system": "square",
|
||||
"is_active": True
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get POS configuration", error=str(e),
|
||||
logger.error("Failed to get POS configuration", error=str(e),
|
||||
tenant_id=tenant_id, config_id=config_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get POS configuration: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get configuration: {str(e)}")
|
||||
|
||||
|
||||
@router.put("/tenants/{tenant_id}/pos/configurations/{config_id}")
|
||||
@router.put(
|
||||
route_builder.build_resource_detail_route("configurations", "config_id"),
|
||||
response_model=dict
|
||||
)
|
||||
@admin_role_required
|
||||
async def update_pos_configuration(
|
||||
configuration_data: Dict[str, Any],
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
config_id: UUID = Path(..., description="Configuration ID"),
|
||||
tenant_id: UUID = Path(...),
|
||||
config_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Update a POS configuration"""
|
||||
"""Update a POS configuration (Admin/Owner only)"""
|
||||
try:
|
||||
|
||||
# TODO: Implement configuration update
|
||||
return {"message": "Configuration updated successfully"}
|
||||
|
||||
return {"message": "Configuration updated successfully", "id": str(config_id)}
|
||||
except Exception as e:
|
||||
logger.error("Failed to update POS configuration", error=str(e),
|
||||
logger.error("Failed to update POS configuration", error=str(e),
|
||||
tenant_id=tenant_id, config_id=config_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update POS configuration: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update configuration: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/pos/configurations/{config_id}")
|
||||
@router.delete(
|
||||
route_builder.build_resource_detail_route("configurations", "config_id"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['owner'])
|
||||
async def delete_pos_configuration(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
config_id: UUID = Path(..., description="Configuration ID"),
|
||||
tenant_id: UUID = Path(...),
|
||||
config_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Delete a POS configuration"""
|
||||
"""Delete a POS configuration (Owner only)"""
|
||||
try:
|
||||
|
||||
# TODO: Implement configuration deletion
|
||||
return {"message": "Configuration deleted successfully"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete POS configuration", error=str(e),
|
||||
logger.error("Failed to delete POS configuration", error=str(e),
|
||||
tenant_id=tenant_id, config_id=config_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete POS configuration: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete configuration: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/pos/configurations/{config_id}/test-connection")
|
||||
async def test_pos_connection(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
config_id: UUID = Path(..., description="Configuration ID"),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Test connection to POS system"""
|
||||
try:
|
||||
|
||||
# TODO: Implement connection testing
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Connection test successful",
|
||||
"tested_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to test POS connection", error=str(e),
|
||||
tenant_id=tenant_id, config_id=config_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to test POS connection: {str(e)}")
|
||||
# ============================================================================
|
||||
# Reference Data
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/pos/supported-systems")
|
||||
@router.get(
|
||||
route_builder.build_global_route("supported-systems"),
|
||||
response_model=dict
|
||||
)
|
||||
async def get_supported_pos_systems():
|
||||
"""Get list of supported POS systems"""
|
||||
"""Get list of supported POS systems (no tenant context required)"""
|
||||
return {
|
||||
"systems": [
|
||||
{
|
||||
@@ -165,4 +171,4 @@ async def get_supported_pos_systems():
|
||||
"supported_regions": ["US", "CA", "EU", "AU"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
344
services/pos/app/api/pos_operations.py
Normal file
344
services/pos/app/api/pos_operations.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
POS Operations API Endpoints
|
||||
BUSINESS layer - Sync operations, webhooks, reconciliation, and test connection
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Body, Request, Header
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
import json
|
||||
|
||||
from app.core.database import get_db
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role, admin_role_required
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('pos')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Sync Operations
|
||||
# ============================================================================
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("sync"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['member', 'admin', 'owner'])
|
||||
async def trigger_sync(
|
||||
sync_request: Dict[str, Any],
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Trigger manual synchronization with POS system (Member+)"""
|
||||
try:
|
||||
sync_type = sync_request.get("sync_type", "incremental")
|
||||
data_types = sync_request.get("data_types", ["transactions"])
|
||||
config_id = sync_request.get("config_id")
|
||||
|
||||
logger.info("Manual sync triggered",
|
||||
tenant_id=tenant_id,
|
||||
config_id=config_id,
|
||||
sync_type=sync_type,
|
||||
user_id=current_user.get("user_id"))
|
||||
|
||||
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)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to trigger sync: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_operations_route("sync-status"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_sync_status(
|
||||
tenant_id: UUID = Path(...),
|
||||
config_id: Optional[UUID] = Query(None),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Get synchronization status and recent sync history"""
|
||||
try:
|
||||
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)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get sync status: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_operations_route("sync-logs"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_sync_logs(
|
||||
tenant_id: UUID = Path(...),
|
||||
config_id: Optional[UUID] = Query(None),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
status: Optional[str] = Query(None),
|
||||
sync_type: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Get detailed sync logs"""
|
||||
try:
|
||||
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)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get sync logs: {str(e)}")
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("resync-failed"),
|
||||
response_model=dict
|
||||
)
|
||||
@admin_role_required
|
||||
async def resync_failed_transactions(
|
||||
tenant_id: UUID = Path(...),
|
||||
days_back: int = Query(7, ge=1, le=90),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Resync failed transactions from the specified time period (Admin/Owner only)"""
|
||||
try:
|
||||
logger.info("Resync failed transactions requested",
|
||||
tenant_id=tenant_id,
|
||||
days_back=days_back,
|
||||
user_id=current_user.get("user_id"))
|
||||
|
||||
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)}")
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("test-connection"),
|
||||
response_model=dict
|
||||
)
|
||||
@admin_role_required
|
||||
async def test_pos_connection(
|
||||
tenant_id: UUID = Path(...),
|
||||
config_id: UUID = Query(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Test connection to POS system (Admin/Owner only)"""
|
||||
try:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Connection test successful",
|
||||
"tested_at": datetime.utcnow().isoformat(),
|
||||
"config_id": str(config_id)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to test POS connection", error=str(e),
|
||||
tenant_id=tenant_id, config_id=config_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to test connection: {str(e)}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Webhook Operations
|
||||
# ============================================================================
|
||||
|
||||
@router.post(
|
||||
route_builder.build_webhook_route("{pos_system}"),
|
||||
response_model=dict
|
||||
)
|
||||
async def receive_webhook(
|
||||
request: Request,
|
||||
pos_system: str = Path(..., description="POS system name"),
|
||||
content_type: Optional[str] = Header(None),
|
||||
x_signature: Optional[str] = Header(None),
|
||||
x_webhook_signature: Optional[str] = Header(None),
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
Receive webhooks from POS systems
|
||||
Supports Square, Toast, and Lightspeed webhook formats
|
||||
"""
|
||||
try:
|
||||
# Validate POS system
|
||||
supported_systems = ["square", "toast", "lightspeed"]
|
||||
if pos_system.lower() not in supported_systems:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported POS system: {pos_system}")
|
||||
|
||||
# Get request details
|
||||
method = request.method
|
||||
url_path = str(request.url.path)
|
||||
query_params = dict(request.query_params)
|
||||
headers = dict(request.headers)
|
||||
|
||||
# Get client IP
|
||||
client_ip = None
|
||||
if hasattr(request, 'client') and request.client:
|
||||
client_ip = request.client.host
|
||||
|
||||
# Read payload
|
||||
try:
|
||||
body = await request.body()
|
||||
raw_payload = body.decode('utf-8') if body else ""
|
||||
payload_size = len(body) if body else 0
|
||||
|
||||
# Parse JSON if possible
|
||||
parsed_payload = None
|
||||
if raw_payload:
|
||||
try:
|
||||
parsed_payload = json.loads(raw_payload)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse webhook payload as JSON",
|
||||
pos_system=pos_system, payload_size=payload_size)
|
||||
except Exception as e:
|
||||
logger.error("Failed to read webhook payload", error=str(e))
|
||||
raise HTTPException(status_code=400, detail="Failed to read request payload")
|
||||
|
||||
# Determine signature from various header formats
|
||||
signature = x_signature or x_webhook_signature or authorization
|
||||
|
||||
# Log webhook receipt
|
||||
logger.info("Webhook received",
|
||||
pos_system=pos_system,
|
||||
method=method,
|
||||
url_path=url_path,
|
||||
payload_size=payload_size,
|
||||
client_ip=client_ip,
|
||||
has_signature=bool(signature),
|
||||
content_type=content_type)
|
||||
|
||||
# TODO: Store webhook log in database
|
||||
# TODO: Verify webhook signature
|
||||
# TODO: Extract tenant_id from payload
|
||||
# TODO: Process webhook based on POS system type
|
||||
# TODO: Queue for async processing if needed
|
||||
|
||||
# Parse webhook type based on POS system
|
||||
webhook_type = None
|
||||
event_id = None
|
||||
|
||||
if parsed_payload:
|
||||
if pos_system.lower() == "square":
|
||||
webhook_type = parsed_payload.get("type")
|
||||
event_id = parsed_payload.get("event_id")
|
||||
elif pos_system.lower() == "toast":
|
||||
webhook_type = parsed_payload.get("eventType")
|
||||
event_id = parsed_payload.get("guid")
|
||||
elif pos_system.lower() == "lightspeed":
|
||||
webhook_type = parsed_payload.get("action")
|
||||
event_id = parsed_payload.get("id")
|
||||
|
||||
logger.info("Webhook processed successfully",
|
||||
pos_system=pos_system,
|
||||
webhook_type=webhook_type,
|
||||
event_id=event_id)
|
||||
|
||||
# Return appropriate response based on POS system requirements
|
||||
if pos_system.lower() == "square":
|
||||
return {"status": "success"}
|
||||
elif pos_system.lower() == "toast":
|
||||
return {"success": True}
|
||||
elif pos_system.lower() == "lightspeed":
|
||||
return {"received": True}
|
||||
else:
|
||||
return {"status": "received"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Webhook processing failed",
|
||||
error=str(e),
|
||||
pos_system=pos_system)
|
||||
|
||||
# Return 500 to trigger POS system retry
|
||||
raise HTTPException(status_code=500, detail="Webhook processing failed")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_webhook_route("{pos_system}/status"),
|
||||
response_model=dict
|
||||
)
|
||||
async def get_webhook_status(pos_system: str = Path(..., description="POS system name")):
|
||||
"""Get webhook endpoint status for a POS system"""
|
||||
try:
|
||||
supported_systems = ["square", "toast", "lightspeed"]
|
||||
if pos_system.lower() not in supported_systems:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported POS system: {pos_system}")
|
||||
|
||||
return {
|
||||
"pos_system": pos_system,
|
||||
"status": "active",
|
||||
"endpoint": f"/api/v1/webhooks/{pos_system}",
|
||||
"supported_events": _get_supported_events(pos_system),
|
||||
"last_received": None,
|
||||
"total_received": 0
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get webhook status", error=str(e), pos_system=pos_system)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get webhook status: {str(e)}")
|
||||
|
||||
|
||||
def _get_supported_events(pos_system: str) -> Dict[str, Any]:
|
||||
"""Get supported webhook events for each POS system"""
|
||||
events = {
|
||||
"square": [
|
||||
"payment.created",
|
||||
"payment.updated",
|
||||
"order.created",
|
||||
"order.updated",
|
||||
"order.fulfilled",
|
||||
"inventory.count.updated"
|
||||
],
|
||||
"toast": [
|
||||
"OrderCreated",
|
||||
"OrderUpdated",
|
||||
"OrderPaid",
|
||||
"OrderCanceled",
|
||||
"OrderVoided"
|
||||
],
|
||||
"lightspeed": [
|
||||
"order.created",
|
||||
"order.updated",
|
||||
"order.paid",
|
||||
"sale.created",
|
||||
"sale.updated"
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
"events": events.get(pos_system.lower(), []),
|
||||
"format": "JSON",
|
||||
"authentication": "signature_verification"
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
# 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
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
|
||||
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)}")
|
||||
82
services/pos/app/api/transactions.py
Normal file
82
services/pos/app/api/transactions.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
POS Transactions API Endpoints
|
||||
ATOMIC layer - Basic CRUD operations for POS transactions
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
|
||||
from app.core.database import get_db
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('pos')
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("transactions"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def list_pos_transactions(
|
||||
tenant_id: UUID = Path(...),
|
||||
pos_system: Optional[str] = Query(None),
|
||||
start_date: Optional[datetime] = Query(None),
|
||||
end_date: Optional[datetime] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
is_synced: Optional[bool] = Query(None),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""List POS transactions for a tenant"""
|
||||
try:
|
||||
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 list POS transactions", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list transactions: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("transactions", "transaction_id"),
|
||||
response_model=dict
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_pos_transaction(
|
||||
tenant_id: UUID = Path(...),
|
||||
transaction_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Get a specific POS transaction"""
|
||||
try:
|
||||
return {
|
||||
"id": str(transaction_id),
|
||||
"tenant_id": str(tenant_id),
|
||||
"status": "completed",
|
||||
"is_synced": True
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get POS transaction", error=str(e),
|
||||
tenant_id=tenant_id, transaction_id=transaction_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get transaction: {str(e)}")
|
||||
@@ -1,179 +0,0 @@
|
||||
# services/pos/app/api/webhooks.py
|
||||
"""
|
||||
POS Webhook API Endpoints
|
||||
Handles incoming webhooks from POS systems
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Header, Path
|
||||
from typing import Optional, Dict, Any
|
||||
import structlog
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
|
||||
router = APIRouter(tags=["webhooks"])
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@router.post("/webhooks/{pos_system}")
|
||||
async def receive_webhook(
|
||||
request: Request,
|
||||
pos_system: str = Path(..., description="POS system name"),
|
||||
content_type: Optional[str] = Header(None),
|
||||
x_signature: Optional[str] = Header(None),
|
||||
x_webhook_signature: Optional[str] = Header(None),
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
Receive webhooks from POS systems
|
||||
Supports Square, Toast, and Lightspeed webhook formats
|
||||
"""
|
||||
try:
|
||||
# Validate POS system
|
||||
supported_systems = ["square", "toast", "lightspeed"]
|
||||
if pos_system.lower() not in supported_systems:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported POS system: {pos_system}")
|
||||
|
||||
# Get request details
|
||||
method = request.method
|
||||
url_path = str(request.url.path)
|
||||
query_params = dict(request.query_params)
|
||||
headers = dict(request.headers)
|
||||
|
||||
# Get client IP
|
||||
client_ip = None
|
||||
if hasattr(request, 'client') and request.client:
|
||||
client_ip = request.client.host
|
||||
|
||||
# Read payload
|
||||
try:
|
||||
body = await request.body()
|
||||
raw_payload = body.decode('utf-8') if body else ""
|
||||
payload_size = len(body) if body else 0
|
||||
|
||||
# Parse JSON if possible
|
||||
parsed_payload = None
|
||||
if raw_payload:
|
||||
try:
|
||||
parsed_payload = json.loads(raw_payload)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse webhook payload as JSON",
|
||||
pos_system=pos_system, payload_size=payload_size)
|
||||
except Exception as e:
|
||||
logger.error("Failed to read webhook payload", error=str(e))
|
||||
raise HTTPException(status_code=400, detail="Failed to read request payload")
|
||||
|
||||
# Determine signature from various header formats
|
||||
signature = x_signature or x_webhook_signature or authorization
|
||||
|
||||
# Log webhook receipt
|
||||
logger.info("Webhook received",
|
||||
pos_system=pos_system,
|
||||
method=method,
|
||||
url_path=url_path,
|
||||
payload_size=payload_size,
|
||||
client_ip=client_ip,
|
||||
has_signature=bool(signature),
|
||||
content_type=content_type)
|
||||
|
||||
# TODO: Store webhook log in database
|
||||
# TODO: Verify webhook signature
|
||||
# TODO: Extract tenant_id from payload
|
||||
# TODO: Process webhook based on POS system type
|
||||
# TODO: Queue for async processing if needed
|
||||
|
||||
# Parse webhook type based on POS system
|
||||
webhook_type = None
|
||||
event_id = None
|
||||
|
||||
if parsed_payload:
|
||||
if pos_system.lower() == "square":
|
||||
webhook_type = parsed_payload.get("type")
|
||||
event_id = parsed_payload.get("event_id")
|
||||
elif pos_system.lower() == "toast":
|
||||
webhook_type = parsed_payload.get("eventType")
|
||||
event_id = parsed_payload.get("guid")
|
||||
elif pos_system.lower() == "lightspeed":
|
||||
webhook_type = parsed_payload.get("action")
|
||||
event_id = parsed_payload.get("id")
|
||||
|
||||
logger.info("Webhook processed successfully",
|
||||
pos_system=pos_system,
|
||||
webhook_type=webhook_type,
|
||||
event_id=event_id)
|
||||
|
||||
# Return appropriate response based on POS system requirements
|
||||
if pos_system.lower() == "square":
|
||||
return {"status": "success"}
|
||||
elif pos_system.lower() == "toast":
|
||||
return {"success": True}
|
||||
elif pos_system.lower() == "lightspeed":
|
||||
return {"received": True}
|
||||
else:
|
||||
return {"status": "received"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Webhook processing failed",
|
||||
error=str(e),
|
||||
pos_system=pos_system)
|
||||
|
||||
# Return 500 to trigger POS system retry
|
||||
raise HTTPException(status_code=500, detail="Webhook processing failed")
|
||||
|
||||
|
||||
@router.get("/webhooks/{pos_system}/status")
|
||||
async def get_webhook_status(pos_system: str = Path(..., description="POS system name")):
|
||||
"""Get webhook endpoint status for a POS system"""
|
||||
try:
|
||||
supported_systems = ["square", "toast", "lightspeed"]
|
||||
if pos_system.lower() not in supported_systems:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported POS system: {pos_system}")
|
||||
|
||||
return {
|
||||
"pos_system": pos_system,
|
||||
"status": "active",
|
||||
"endpoint": f"/api/v1/webhooks/{pos_system}",
|
||||
"supported_events": _get_supported_events(pos_system),
|
||||
"last_received": None, # TODO: Get from database
|
||||
"total_received": 0 # TODO: Get from database
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get webhook status", error=str(e), pos_system=pos_system)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get webhook status: {str(e)}")
|
||||
|
||||
|
||||
def _get_supported_events(pos_system: str) -> Dict[str, Any]:
|
||||
"""Get supported webhook events for each POS system"""
|
||||
events = {
|
||||
"square": [
|
||||
"payment.created",
|
||||
"payment.updated",
|
||||
"order.created",
|
||||
"order.updated",
|
||||
"order.fulfilled",
|
||||
"inventory.count.updated"
|
||||
],
|
||||
"toast": [
|
||||
"OrderCreated",
|
||||
"OrderUpdated",
|
||||
"OrderPaid",
|
||||
"OrderCanceled",
|
||||
"OrderVoided"
|
||||
],
|
||||
"lightspeed": [
|
||||
"order.created",
|
||||
"order.updated",
|
||||
"order.paid",
|
||||
"sale.created",
|
||||
"sale.updated"
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
"events": events.get(pos_system.lower(), []),
|
||||
"format": "JSON",
|
||||
"authentication": "signature_verification"
|
||||
}
|
||||
Reference in New Issue
Block a user