256 lines
8.6 KiB
Python
256 lines
8.6 KiB
Python
# ================================================================
|
|
# 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) |