# ================================================================ # services/notification/app/services/whatsapp_service.py # ================================================================ """ WhatsApp service for sending notifications Integrates with WhatsApp Business Cloud API (Meta/Facebook) This is a backward-compatible wrapper around the new WhatsAppBusinessService """ import structlog import httpx from typing import Optional, Dict, Any, List import asyncio from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.services.whatsapp_business_service import WhatsAppBusinessService from app.schemas.whatsapp import ( SendWhatsAppMessageRequest, TemplateMessageRequest, TemplateComponent, TemplateParameter, WhatsAppMessageType ) from shared.monitoring.metrics import MetricsCollector logger = structlog.get_logger() metrics = MetricsCollector("notification-service") class WhatsAppService: """ WhatsApp service for sending notifications via WhatsApp Business Cloud API Backward-compatible wrapper for existing code """ def __init__(self, session: Optional[AsyncSession] = None, tenant_client=None): self.enabled = settings.ENABLE_WHATSAPP_NOTIFICATIONS self.business_service = WhatsAppBusinessService(session, tenant_client) async def send_message( self, to_phone: str, message: str, template_name: Optional[str] = None, template_params: Optional[List[str]] = None, tenant_id: Optional[str] = None ) -> bool: """ Send WhatsApp message (backward-compatible wrapper) Args: to_phone: Recipient phone number (with country code) message: Message text template_name: WhatsApp template name (optional) template_params: Template parameters (optional) tenant_id: Tenant ID (optional, defaults to system tenant) Returns: bool: True if message was sent successfully """ try: if not self.enabled: logger.info("WhatsApp notifications disabled") return True # Return success to avoid blocking workflow # Format phone number phone = self._format_phone_number(to_phone) if not phone: logger.error("Invalid phone number", phone=to_phone) return False # Use default tenant if not provided if not tenant_id: tenant_id = "00000000-0000-0000-0000-000000000000" # System tenant # Build request if template_name: # Template message components = [] if template_params: # Build body component with parameters parameters = [ TemplateParameter(type="text", text=param) for param in template_params ] components.append( TemplateComponent(type="body", parameters=parameters) ) template_request = TemplateMessageRequest( template_name=template_name, language="es", components=components ) request = SendWhatsAppMessageRequest( tenant_id=tenant_id, recipient_phone=phone, message_type=WhatsAppMessageType.TEMPLATE, template=template_request ) else: # Text message request = SendWhatsAppMessageRequest( tenant_id=tenant_id, recipient_phone=phone, message_type=WhatsAppMessageType.TEXT, text=message ) # Send via business service response = await self.business_service.send_message(request) if response.success: logger.info( "WhatsApp message sent successfully", to=phone, template=template_name ) return response.success except Exception as e: logger.error( "Failed to send WhatsApp message", to=to_phone, error=str(e) ) metrics.increment_counter("whatsapp_sent_total", labels={"status": "failed"}) return False async def send_bulk_messages( self, recipients: List[str], message: str, template_name: Optional[str] = None, template_params: Optional[List[str]] = None, batch_size: int = 20, tenant_id: Optional[str] = None ) -> Dict[str, Any]: """ Send bulk WhatsApp messages with rate limiting (backward-compatible wrapper) Args: recipients: List of recipient phone numbers message: Message text template_name: WhatsApp template name (optional) template_params: Template parameters (optional) batch_size: Number of messages to send per batch tenant_id: Tenant ID (optional) Returns: Dict containing success/failure counts """ results = { "total": len(recipients), "sent": 0, "failed": 0, "errors": [] } try: # Use default tenant if not provided if not tenant_id: tenant_id = "00000000-0000-0000-0000-000000000000" # Build requests for all recipients requests = [] for phone in recipients: formatted_phone = self._format_phone_number(phone) if not formatted_phone: results["failed"] += 1 results["errors"].append({"phone": phone, "error": "Invalid phone format"}) continue if template_name: # Template message components = [] if template_params: parameters = [ TemplateParameter(type="text", text=param) for param in template_params ] components.append( TemplateComponent(type="body", parameters=parameters) ) template_request = TemplateMessageRequest( template_name=template_name, language="es", components=components ) request = SendWhatsAppMessageRequest( tenant_id=tenant_id, recipient_phone=formatted_phone, message_type=WhatsAppMessageType.TEMPLATE, template=template_request ) else: # Text message request = SendWhatsAppMessageRequest( tenant_id=tenant_id, recipient_phone=formatted_phone, message_type=WhatsAppMessageType.TEXT, text=message ) requests.append(request) # Send via business service bulk_result = await self.business_service.send_bulk_messages( requests, batch_size=batch_size ) # Update results results["sent"] = bulk_result.get("sent", 0) results["failed"] += bulk_result.get("failed", 0) results["errors"].extend(bulk_result.get("errors", [])) logger.info( "Bulk WhatsApp completed", total=results["total"], sent=results["sent"], failed=results["failed"] ) return results except Exception as e: logger.error("Bulk WhatsApp failed", error=str(e)) results["errors"].append({"error": str(e)}) return results async def health_check(self) -> bool: """ Check if WhatsApp service is healthy Returns: bool: True if service is healthy """ return await self.business_service.health_check() def _format_phone_number(self, phone: str) -> Optional[str]: """ Format phone number for WhatsApp (E.164 format) Args: phone: Input phone number Returns: Formatted phone number or None if invalid """ return self.business_service._format_phone_number(phone)