179 lines
6.3 KiB
Python
179 lines
6.3 KiB
Python
|
|
# 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"
|
||
|
|
}
|