REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View 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)}")

View File

@@ -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"]
}
]
}
}

View 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"
}

View File

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

View 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)}")

View File

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

View File

@@ -7,7 +7,10 @@ import time
from fastapi import FastAPI, Request
from sqlalchemy import text
from app.core.config import settings
from app.api import pos_config, webhooks, sync
from app.api.configurations import router as configurations_router
from app.api.transactions import router as transactions_router
from app.api.pos_operations import router as pos_operations_router
from app.api.analytics import router as analytics_router
from app.core.database import database_manager
from shared.service_base import StandardFastAPIService
@@ -76,7 +79,7 @@ class POSService(StandardFastAPIService):
description="Handles integration with external POS systems",
version="1.0.0",
cors_origins=settings.CORS_ORIGINS,
api_prefix="/api/v1",
api_prefix="", # Empty because RouteBuilder already includes /api/v1
database_manager=database_manager,
expected_tables=pos_expected_tables,
custom_metrics=pos_custom_metrics
@@ -166,9 +169,10 @@ service.setup_custom_middleware()
service.setup_custom_endpoints()
# Include routers
service.add_router(pos_config.router, tags=["pos-config"])
service.add_router(webhooks.router, tags=["webhooks"])
service.add_router(sync.router, tags=["sync"])
service.add_router(configurations_router, tags=["pos-configurations"])
service.add_router(transactions_router, tags=["pos-transactions"])
service.add_router(pos_operations_router, tags=["pos-operations"])
service.add_router(analytics_router, tags=["pos-analytics"])
if __name__ == "__main__":

View File

@@ -1,8 +1,8 @@
"""initial_schema_20251001_1118
"""initial_schema_20251006_1515
Revision ID: 36bd79501798
Revision ID: 31fcdb636d6e
Revises:
Create Date: 2025-10-01 11:18:18.854624+02:00
Create Date: 2025-10-06 15:15:44.162404+02:00
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '36bd79501798'
revision: str = '31fcdb636d6e'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None