Add whatsapp feature

This commit is contained in:
Urtzi Alfaro
2025-11-13 16:01:08 +01:00
parent d7df2b0853
commit 9bc048d360
74 changed files with 9765 additions and 533 deletions

View File

@@ -0,0 +1,370 @@
"""
WhatsApp Business Cloud API Schemas
"""
from pydantic import BaseModel, Field, validator
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
# ============================================================
# Enums
# ============================================================
class WhatsAppMessageType(str, Enum):
"""WhatsApp message types"""
TEMPLATE = "template"
TEXT = "text"
IMAGE = "image"
DOCUMENT = "document"
INTERACTIVE = "interactive"
class WhatsAppMessageStatus(str, Enum):
"""WhatsApp message delivery status"""
PENDING = "pending"
SENT = "sent"
DELIVERED = "delivered"
READ = "read"
FAILED = "failed"
class TemplateCategory(str, Enum):
"""WhatsApp template categories"""
MARKETING = "MARKETING"
UTILITY = "UTILITY"
AUTHENTICATION = "AUTHENTICATION"
# ============================================================
# Template Message Schemas
# ============================================================
class TemplateParameter(BaseModel):
"""Template parameter for dynamic content"""
type: str = Field(default="text", description="Parameter type (text, currency, date_time)")
text: Optional[str] = Field(None, description="Text value for the parameter")
class Config:
json_schema_extra = {
"example": {
"type": "text",
"text": "PO-2024-001"
}
}
class TemplateComponent(BaseModel):
"""Template component (header, body, buttons)"""
type: str = Field(..., description="Component type (header, body, button)")
parameters: Optional[List[TemplateParameter]] = Field(None, description="Component parameters")
sub_type: Optional[str] = Field(None, description="Button sub_type (quick_reply, url)")
index: Optional[int] = Field(None, description="Button index")
class Config:
json_schema_extra = {
"example": {
"type": "body",
"parameters": [
{"type": "text", "text": "PO-2024-001"},
{"type": "text", "text": "100.50"}
]
}
}
class TemplateMessageRequest(BaseModel):
"""Request to send a template message"""
template_name: str = Field(..., description="WhatsApp template name")
language: str = Field(default="es", description="Template language code")
components: List[TemplateComponent] = Field(..., description="Template components with parameters")
class Config:
json_schema_extra = {
"example": {
"template_name": "po_notification",
"language": "es",
"components": [
{
"type": "body",
"parameters": [
{"type": "text", "text": "PO-2024-001"},
{"type": "text", "text": "Supplier XYZ"},
{"type": "text", "text": "€1,250.00"}
]
}
]
}
}
# ============================================================
# Send Message Schemas
# ============================================================
class SendWhatsAppMessageRequest(BaseModel):
"""Request to send a WhatsApp message"""
tenant_id: str = Field(..., description="Tenant ID")
recipient_phone: str = Field(..., description="Recipient phone number (E.164 format)")
recipient_name: Optional[str] = Field(None, description="Recipient name")
message_type: WhatsAppMessageType = Field(..., description="Message type")
template: Optional[TemplateMessageRequest] = Field(None, description="Template details (required for template messages)")
text: Optional[str] = Field(None, description="Text message body (for text messages)")
media_url: Optional[str] = Field(None, description="Media URL (for image/document messages)")
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata (PO number, order ID, etc.)")
notification_id: Optional[str] = Field(None, description="Link to existing notification")
@validator('recipient_phone')
def validate_phone(cls, v):
"""Validate E.164 phone format"""
if not v.startswith('+'):
raise ValueError('Phone number must be in E.164 format (starting with +)')
if len(v) < 10 or len(v) > 16:
raise ValueError('Phone number length must be between 10 and 16 characters')
return v
@validator('template')
def validate_template(cls, v, values):
"""Validate template is provided for template messages"""
if values.get('message_type') == WhatsAppMessageType.TEMPLATE and not v:
raise ValueError('Template details required for template messages')
return v
class Config:
json_schema_extra = {
"example": {
"tenant_id": "123e4567-e89b-12d3-a456-426614174000",
"recipient_phone": "+34612345678",
"recipient_name": "Supplier ABC",
"message_type": "template",
"template": {
"template_name": "po_notification",
"language": "es",
"components": [
{
"type": "body",
"parameters": [
{"type": "text", "text": "PO-2024-001"},
{"type": "text", "text": "€1,250.00"}
]
}
]
},
"metadata": {
"po_number": "PO-2024-001",
"po_id": "123e4567-e89b-12d3-a456-426614174111"
}
}
}
class SendWhatsAppMessageResponse(BaseModel):
"""Response after sending a WhatsApp message"""
success: bool = Field(..., description="Whether message was sent successfully")
message_id: str = Field(..., description="Internal message ID")
whatsapp_message_id: Optional[str] = Field(None, description="WhatsApp's message ID")
status: WhatsAppMessageStatus = Field(..., description="Message status")
error_message: Optional[str] = Field(None, description="Error message if failed")
class Config:
json_schema_extra = {
"example": {
"success": True,
"message_id": "123e4567-e89b-12d3-a456-426614174222",
"whatsapp_message_id": "wamid.HBgNMzQ2MTIzNDU2Nzg5FQIAERgSMzY5RTFFNTdEQzZBRkVCODdBAA==",
"status": "sent",
"error_message": None
}
}
# ============================================================
# Webhook Schemas
# ============================================================
class WebhookValue(BaseModel):
"""Webhook notification value"""
messaging_product: str
metadata: Dict[str, Any]
contacts: Optional[List[Dict[str, Any]]] = None
messages: Optional[List[Dict[str, Any]]] = None
statuses: Optional[List[Dict[str, Any]]] = None
class WebhookEntry(BaseModel):
"""Webhook entry"""
id: str
changes: List[Dict[str, Any]]
class WhatsAppWebhook(BaseModel):
"""WhatsApp webhook payload"""
object: str
entry: List[WebhookEntry]
class WebhookVerification(BaseModel):
"""Webhook verification request"""
mode: str = Field(..., alias="hub.mode")
token: str = Field(..., alias="hub.verify_token")
challenge: str = Field(..., alias="hub.challenge")
class Config:
populate_by_name = True
# ============================================================
# Message Status Schemas
# ============================================================
class MessageStatusUpdate(BaseModel):
"""Message status update"""
whatsapp_message_id: str = Field(..., description="WhatsApp message ID")
status: WhatsAppMessageStatus = Field(..., description="New status")
timestamp: datetime = Field(..., description="Status update timestamp")
error_code: Optional[str] = Field(None, description="Error code if failed")
error_message: Optional[str] = Field(None, description="Error message if failed")
# ============================================================
# Template Management Schemas
# ============================================================
class WhatsAppTemplateCreate(BaseModel):
"""Create a WhatsApp template"""
tenant_id: Optional[str] = Field(None, description="Tenant ID (null for system templates)")
template_name: str = Field(..., description="Template name in WhatsApp")
template_key: str = Field(..., description="Internal template key")
display_name: str = Field(..., description="Display name")
description: Optional[str] = Field(None, description="Template description")
category: TemplateCategory = Field(..., description="Template category")
language: str = Field(default="es", description="Template language")
header_type: Optional[str] = Field(None, description="Header type (TEXT, IMAGE, DOCUMENT, VIDEO)")
header_text: Optional[str] = Field(None, max_length=60, description="Header text (max 60 chars)")
body_text: str = Field(..., description="Body text with {{1}}, {{2}} placeholders")
footer_text: Optional[str] = Field(None, max_length=60, description="Footer text (max 60 chars)")
parameters: Optional[List[Dict[str, Any]]] = Field(None, description="Parameter definitions")
buttons: Optional[List[Dict[str, Any]]] = Field(None, description="Button definitions")
class Config:
json_schema_extra = {
"example": {
"template_name": "po_notification",
"template_key": "po_notification_v1",
"display_name": "Purchase Order Notification",
"description": "Notify supplier of new purchase order",
"category": "UTILITY",
"language": "es",
"body_text": "Hola {{1}}, has recibido una nueva orden de compra {{2}} por un total de {{3}}.",
"parameters": [
{"name": "supplier_name", "example": "Proveedor ABC"},
{"name": "po_number", "example": "PO-2024-001"},
{"name": "total_amount", "example": "€1,250.00"}
]
}
}
class WhatsAppTemplateResponse(BaseModel):
"""WhatsApp template response"""
id: str
tenant_id: Optional[str]
template_name: str
template_key: str
display_name: str
description: Optional[str]
category: str
language: str
status: str
body_text: str
parameter_count: int
is_active: bool
sent_count: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": "123e4567-e89b-12d3-a456-426614174333",
"tenant_id": None,
"template_name": "po_notification",
"template_key": "po_notification_v1",
"display_name": "Purchase Order Notification",
"description": "Notify supplier of new purchase order",
"category": "UTILITY",
"language": "es",
"status": "APPROVED",
"body_text": "Hola {{1}}, has recibido una nueva orden de compra {{2}} por un total de {{3}}.",
"parameter_count": 3,
"is_active": True,
"sent_count": 125,
"created_at": "2024-01-15T10:30:00",
"updated_at": "2024-01-15T10:30:00"
}
}
# ============================================================
# Message Query Schemas
# ============================================================
class WhatsAppMessageResponse(BaseModel):
"""WhatsApp message response"""
id: str
tenant_id: str
notification_id: Optional[str]
whatsapp_message_id: Optional[str]
recipient_phone: str
recipient_name: Optional[str]
message_type: str
status: str
template_name: Optional[str]
template_language: Optional[str]
message_body: Optional[str]
sent_at: Optional[datetime]
delivered_at: Optional[datetime]
read_at: Optional[datetime]
failed_at: Optional[datetime]
error_message: Optional[str]
metadata: Optional[Dict[str, Any]]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class WhatsAppDeliveryStats(BaseModel):
"""WhatsApp delivery statistics"""
total_messages: int
sent: int
delivered: int
read: int
failed: int
pending: int
unique_recipients: int
total_conversations: int
delivery_rate: float
period: Dict[str, str]
class Config:
json_schema_extra = {
"example": {
"total_messages": 1500,
"sent": 1480,
"delivered": 1450,
"read": 1200,
"failed": 20,
"pending": 0,
"unique_recipients": 350,
"total_conversations": 400,
"delivery_rate": 96.67,
"period": {
"start": "2024-01-01T00:00:00",
"end": "2024-01-31T23:59:59"
}
}
}