from sqlalchemy.orm import Session from fastapi import HTTPException, status from typing import Dict, Any, List from datetime import datetime import json import hashlib import hmac from app.models.integration import WebhookEvent, ExternalIntegration, WebhookStatus, IntegrationType from app.schemas.webhook import WebhookEventCreate from app.services.audit import AuditService import logging logger = logging.getLogger(__name__) class WebhookService: def __init__(self, db: Session): self.db = db self.audit_service = AuditService(db) def verify_webhook_signature(self, payload: bytes, signature: str, secret: str) -> bool: """Verify webhook signature using HMAC""" expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() # Remove 'sha256=' prefix if present if signature.startswith('sha256='): signature = signature[7:] return hmac.compare_digest(expected_signature, signature) def create_webhook_event(self, webhook_data: WebhookEventCreate) -> WebhookEvent: """Create a new webhook event for processing""" # Find the integration for this webhook integration = self.db.query(ExternalIntegration).filter( ExternalIntegration.organization_id == webhook_data.organization_id, ExternalIntegration.type == webhook_data.integration_type, ExternalIntegration.is_active ).first() if not integration: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No active integration found for type {webhook_data.integration_type}" ) # Check for duplicate webhook (idempotency) existing_webhook = self.db.query(WebhookEvent).filter( WebhookEvent.external_id == webhook_data.external_id, WebhookEvent.integration_id == integration.id, WebhookEvent.organization_id == webhook_data.organization_id ).first() if existing_webhook: logger.info(f"Duplicate webhook ignored: {webhook_data.external_id}") return existing_webhook # Create new webhook event webhook_event = WebhookEvent( organization_id=webhook_data.organization_id, integration_id=integration.id, external_id=webhook_data.external_id, event_type=webhook_data.event_type, payload=json.dumps(webhook_data.payload), status=WebhookStatus.PENDING ) self.db.add(webhook_event) self.db.commit() self.db.refresh(webhook_event) # Log webhook creation self.audit_service.log_action( organization_id=webhook_data.organization_id, action="create", resource_type="webhook_event", resource_id=str(webhook_event.id), details={ "event_type": webhook_data.event_type, "external_id": webhook_data.external_id, "integration_type": webhook_data.integration_type.value } ) return webhook_event def get_pending_webhooks(self, limit: int = 100) -> List[WebhookEvent]: """Get pending webhook events for processing""" return self.db.query(WebhookEvent).filter( WebhookEvent.status.in_([WebhookStatus.PENDING, WebhookStatus.RETRY]) ).order_by(WebhookEvent.created_at).limit(limit).all() def process_webhook_event(self, webhook_event: WebhookEvent) -> bool: """Process a single webhook event""" try: # Update status to processing webhook_event.status = WebhookStatus.PROCESSING self.db.commit() # Parse payload payload_data = json.loads(webhook_event.payload) # Get integration integration = self.db.query(ExternalIntegration).filter( ExternalIntegration.id == webhook_event.integration_id ).first() if not integration: raise Exception("Integration not found") # Process based on integration type success = False if integration.type == IntegrationType.USER_MANAGEMENT: success = self._process_user_webhook(webhook_event, payload_data) elif integration.type == IntegrationType.PAYMENT: success = self._process_payment_webhook(webhook_event, payload_data) elif integration.type == IntegrationType.COMMUNICATION: success = self._process_communication_webhook(webhook_event, payload_data) if success: webhook_event.status = WebhookStatus.SUCCESS webhook_event.processed_at = datetime.utcnow() webhook_event.error_message = None else: raise Exception("Webhook processing failed") self.db.commit() # Log successful processing self.audit_service.log_action( organization_id=webhook_event.organization_id, action="update", resource_type="webhook_event", resource_id=str(webhook_event.id), details={ "status": "success", "event_type": webhook_event.event_type } ) return True except Exception as e: logger.error(f"Error processing webhook {webhook_event.id}: {str(e)}") # Update retry count and status webhook_event.retry_count += 1 webhook_event.error_message = str(e) if webhook_event.retry_count >= webhook_event.max_retries: webhook_event.status = WebhookStatus.FAILED logger.error(f"Webhook {webhook_event.id} failed after {webhook_event.retry_count} retries") else: webhook_event.status = WebhookStatus.RETRY logger.info(f"Webhook {webhook_event.id} will be retried ({webhook_event.retry_count}/{webhook_event.max_retries})") self.db.commit() # Log error self.audit_service.log_action( organization_id=webhook_event.organization_id, action="update", resource_type="webhook_event", resource_id=str(webhook_event.id), details={ "status": webhook_event.status.value, "error": str(e), "retry_count": webhook_event.retry_count } ) return False def _process_user_webhook(self, webhook_event: WebhookEvent, payload: Dict[str, Any]) -> bool: """Process user management webhook events""" event_type = webhook_event.event_type logger.info(f"Processing user webhook: {event_type}") # Simulate processing different user events if event_type == "user.created": # Handle user creation from external service return True elif event_type == "user.updated": # Handle user update from external service return True elif event_type == "user.deleted": # Handle user deletion from external service return True return True # Success for demo purposes def _process_payment_webhook(self, webhook_event: WebhookEvent, payload: Dict[str, Any]) -> bool: """Process payment webhook events""" event_type = webhook_event.event_type logger.info(f"Processing payment webhook: {event_type}") # Simulate processing different payment events if event_type == "payment.succeeded": # Handle successful payment return True elif event_type == "payment.failed": # Handle failed payment return True elif event_type == "subscription.created": # Handle subscription creation return True elif event_type == "subscription.cancelled": # Handle subscription cancellation return True return True # Success for demo purposes def _process_communication_webhook(self, webhook_event: WebhookEvent, payload: Dict[str, Any]) -> bool: """Process communication webhook events""" event_type = webhook_event.event_type logger.info(f"Processing communication webhook: {event_type}") # Simulate processing different communication events if event_type == "email.delivered": # Handle email delivery confirmation return True elif event_type == "email.bounced": # Handle email bounce return True elif event_type == "sms.delivered": # Handle SMS delivery confirmation return True elif event_type == "notification.clicked": # Handle notification click tracking return True return True # Success for demo purposes def get_webhook_stats(self, organization_id: int) -> Dict[str, Any]: """Get webhook processing statistics for an organization""" from sqlalchemy import func stats = self.db.query( WebhookEvent.status, func.count(WebhookEvent.id).label('count') ).filter( WebhookEvent.organization_id == organization_id ).group_by(WebhookEvent.status).all() result = {webhook_status.value: 0 for webhook_status in WebhookStatus} for webhook_status_item, count in stats: result[webhook_status_item.value] = count # Get recent webhook events recent_webhooks = self.db.query(WebhookEvent).filter( WebhookEvent.organization_id == organization_id ).order_by(WebhookEvent.created_at.desc()).limit(10).all() return { "status_counts": result, "total_webhooks": sum(result.values()), "recent_webhooks": [ { "id": wh.id, "event_type": wh.event_type, "status": wh.status.value, "created_at": wh.created_at, "retry_count": wh.retry_count } for wh in recent_webhooks ] }