Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
# ================================================================
# services/notification/app/schemas/notifications.py
# ================================================================
"""
Notification schemas for API validation and serialization
"""
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional, Dict, Any, List
from datetime import datetime
from enum import Enum
# Reuse enums from models
class NotificationType(str, Enum):
EMAIL = "email"
WHATSAPP = "whatsapp"
PUSH = "push"
SMS = "sms"
class NotificationStatus(str, Enum):
PENDING = "pending"
SENT = "sent"
DELIVERED = "delivered"
FAILED = "failed"
CANCELLED = "cancelled"
class NotificationPriority(str, Enum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
URGENT = "urgent"
# ================================================================
# REQUEST SCHEMAS
# ================================================================
class NotificationCreate(BaseModel):
"""Schema for creating a new notification"""
type: NotificationType
recipient_id: Optional[str] = None # For individual notifications
recipient_email: Optional[EmailStr] = None
recipient_phone: Optional[str] = None
# Content
subject: Optional[str] = None
message: str = Field(..., min_length=1, max_length=5000)
html_content: Optional[str] = None
# Template-based content
template_id: Optional[str] = None
template_data: Optional[Dict[str, Any]] = None
# Configuration
priority: NotificationPriority = NotificationPriority.NORMAL
scheduled_at: Optional[datetime] = None
broadcast: bool = False
# Internal fields (set by service)
tenant_id: Optional[str] = None
sender_id: Optional[str] = None
@validator('recipient_phone')
def validate_phone(cls, v):
"""Validate Spanish phone number format"""
if v and not v.startswith(('+34', '6', '7', '9')):
raise ValueError('Invalid Spanish phone number format')
return v
@validator('scheduled_at')
def validate_scheduled_at(cls, v):
"""Ensure scheduled time is in the future"""
if v and v <= datetime.utcnow():
raise ValueError('Scheduled time must be in the future')
return v
class NotificationUpdate(BaseModel):
"""Schema for updating notification status"""
status: Optional[NotificationStatus] = None
error_message: Optional[str] = None
delivered_at: Optional[datetime] = None
read: Optional[bool] = None
read_at: Optional[datetime] = None
class BulkNotificationCreate(BaseModel):
"""Schema for creating bulk notifications"""
type: NotificationType
recipients: List[str] = Field(..., min_items=1, max_items=1000) # User IDs or emails
# Content
subject: Optional[str] = None
message: str = Field(..., min_length=1, max_length=5000)
html_content: Optional[str] = None
# Template-based content
template_id: Optional[str] = None
template_data: Optional[Dict[str, Any]] = None
# Configuration
priority: NotificationPriority = NotificationPriority.NORMAL
scheduled_at: Optional[datetime] = None
# ================================================================
# RESPONSE SCHEMAS
# ================================================================
class NotificationResponse(BaseModel):
"""Schema for notification response"""
id: str
tenant_id: str
sender_id: str
recipient_id: Optional[str]
type: NotificationType
status: NotificationStatus
priority: NotificationPriority
subject: Optional[str]
message: str
recipient_email: Optional[str]
recipient_phone: Optional[str]
scheduled_at: Optional[datetime]
sent_at: Optional[datetime]
delivered_at: Optional[datetime]
broadcast: bool
read: bool
read_at: Optional[datetime]
retry_count: int
error_message: Optional[str]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class NotificationHistory(BaseModel):
"""Schema for notification history"""
notifications: List[NotificationResponse]
total: int
page: int
per_page: int
has_next: bool
has_prev: bool
class NotificationStats(BaseModel):
"""Schema for notification statistics"""
total_sent: int
total_delivered: int
total_failed: int
delivery_rate: float
avg_delivery_time_minutes: Optional[float]
by_type: Dict[str, int]
by_status: Dict[str, int]
recent_activity: List[Dict[str, Any]]
# ================================================================
# PREFERENCE SCHEMAS
# ================================================================
class NotificationPreferences(BaseModel):
"""Schema for user notification preferences"""
user_id: str
tenant_id: str
# Email preferences
email_enabled: bool = True
email_alerts: bool = True
email_marketing: bool = False
email_reports: bool = True
# WhatsApp preferences
whatsapp_enabled: bool = False
whatsapp_alerts: bool = False
whatsapp_reports: bool = False
# Push notification preferences
push_enabled: bool = True
push_alerts: bool = True
push_reports: bool = False
# Timing preferences
quiet_hours_start: str = Field(default="22:00", pattern=r"^([01]?[0-9]|2[0-3]):[0-5][0-9]$")
quiet_hours_end: str = Field(default="08:00", pattern=r"^([01]?[0-9]|2[0-3]):[0-5][0-9]$")
timezone: str = "Europe/Madrid"
# Frequency preferences
digest_frequency: str = Field(default="daily", pattern=r"^(none|daily|weekly)$")
max_emails_per_day: int = Field(default=10, ge=1, le=100)
# Language preference
language: str = Field(default="es", pattern=r"^(es|en)$")
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class PreferencesUpdate(BaseModel):
"""Schema for updating notification preferences"""
email_enabled: Optional[bool] = None
email_alerts: Optional[bool] = None
email_marketing: Optional[bool] = None
email_reports: Optional[bool] = None
whatsapp_enabled: Optional[bool] = None
whatsapp_alerts: Optional[bool] = None
whatsapp_reports: Optional[bool] = None
push_enabled: Optional[bool] = None
push_alerts: Optional[bool] = None
push_reports: Optional[bool] = None
quiet_hours_start: Optional[str] = Field(None, pattern=r"^([01]?[0-9]|2[0-3]):[0-5][0-9]$")
quiet_hours_end: Optional[str] = Field(None, pattern=r"^([01]?[0-9]|2[0-3]):[0-5][0-9]$")
timezone: Optional[str] = None
digest_frequency: Optional[str] = Field(None, pattern=r"^(none|daily|weekly)$")
max_emails_per_day: Optional[int] = Field(None, ge=1, le=100)
language: Optional[str] = Field(None, pattern=r"^(es|en)$")
# ================================================================
# TEMPLATE SCHEMAS
# ================================================================
class TemplateCreate(BaseModel):
"""Schema for creating notification templates"""
template_key: str = Field(..., min_length=3, max_length=100)
name: str = Field(..., min_length=3, max_length=255)
description: Optional[str] = None
category: str = Field(..., pattern=r"^(alert|marketing|transactional)$")
type: NotificationType
subject_template: Optional[str] = None
body_template: str = Field(..., min_length=10)
html_template: Optional[str] = None
language: str = Field(default="es", pattern=r"^(es|en)$")
default_priority: NotificationPriority = NotificationPriority.NORMAL
required_variables: Optional[List[str]] = None
class TemplateResponse(BaseModel):
"""Schema for template response"""
id: str
tenant_id: Optional[str]
template_key: str
name: str
description: Optional[str]
category: str
type: NotificationType
subject_template: Optional[str]
body_template: str
html_template: Optional[str]
language: str
is_active: bool
is_system: bool
default_priority: NotificationPriority
required_variables: Optional[List[str]]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# ================================================================
# WEBHOOK SCHEMAS
# ================================================================
class DeliveryWebhook(BaseModel):
"""Schema for delivery status webhooks"""
notification_id: str
status: NotificationStatus
provider: str
provider_message_id: Optional[str] = None
delivered_at: Optional[datetime] = None
error_code: Optional[str] = None
error_message: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
class ReadReceiptWebhook(BaseModel):
"""Schema for read receipt webhooks"""
notification_id: str
read_at: datetime
user_agent: Optional[str] = None
ip_address: Optional[str] = None

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"
}
}
}