Integración bidireccional profesional entre WuzAPI (WhatsApp) y Chatwoot usando Arquitectura Hexagonal, principios SOLID y mejores prácticas de ingeniería de software.
- Características
- Arquitectura
- Estructura del Proyecto
- Instalación
- Configuración
- Uso
- Flujos de Datos
- Desarrollo
- Troubleshooting
- ✅ Arquitectura Hexagonal (Ports & Adapters) - Separación clara de responsabilidades
- ✅ Principios SOLID - Código mantenible y escalable
- ✅ Domain-Driven Design (DDD) - Lógica de negocio en el dominio
- ✅ Type Hints Completos - Type safety con Python 3.12+
- ✅ Async/Await - Operaciones I/O no bloqueantes
- ✅ Dependency Injection - Vía parámetros de constructores
- ✅ Integración Bidireccional WhatsApp ↔ Chatwoot
- ✅ Multi-instancia Ready - 1 contenedor = 1 token = 1 inbox
- ✅ Validación por TOKEN - Seguridad y aislamiento
- ✅ Caché Inteligente - Redis con fallback en memoria
- ✅ Logs Estructurados - Debugging fácil con contexto
- ✅ Docker Compose - Deploy rápido y reproducible
- ✅ Nginx + SSL/TLS - Producción con Let's Encrypt
- ✅ Systemd Service - Autostart y supervisión
- ✅ Health Checks - Monitoreo del estado
┌─────────────────────────────────────────────────────────────────────┐
│ CAPA EXTERNA - ENTRADA │
│ 📥 Adaptadores Primarios (Infraestructura) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ src/infrastructure/api/webhook.py │
│ ├─ POST /webhook/wuzapi ← Recibe eventos de WuzAPI │
│ ├─ POST /webhook/chatwoot ← Recibe eventos de Chatwoot │
│ ├─ GET /health ← Health check │
│ └─ GET / ← Info del servicio │
│ │
│ Responsabilidad: Transformar HTTP requests → llamadas al dominio │
│ │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ CAPA DE APLICACIÓN │
│ 🎯 Casos de Uso (Application) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ src/application/use_cases/ │
│ ├─ sync_message_to_chatwoot.py │
│ │ Orquesta: WhatsApp → Chatwoot │
│ │ 1. Validar mensaje │
│ │ 2. Crear/buscar contacto │
│ │ 3. Crear/buscar conversación │
│ │ 4. Enviar mensaje │
│ │ │
│ └─ send_message_to_whatsapp.py │
│ Orquesta: Chatwoot → WhatsApp │
│ 1. Validar evento │
│ 2. Extraer datos │
│ 3. Enviar a WuzAPI │
│ │
│ Responsabilidad: Coordinar el flujo de negocio │
│ NO conoce HTTP, Redis, ni detalles técnicos │
│ │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ CAPA DE DOMINIO (CORE) │
│ ❤️ Núcleo del Negocio (Domain) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ src/domain/ │
│ │ │
│ ├─ entities/ │
│ │ └─ whatsapp_message.py │
│ │ Entidad rica con lógica de negocio │
│ │ - Representa un mensaje de WhatsApp │
│ │ - Métodos: is_incoming(), extract_text_content() │
│ │ - Factory: from_wuzapi_event() │
│ │ │
│ ├─ value_objects/ │
│ │ └─ phone_number.py │
│ │ Objeto de valor inmutable │
│ │ - Validación automática │
│ │ - Normalización de formatos │
│ │ - Métodos: from_whatsapp_jid(), is_group │
│ │ │
│ └─ ports/ (INTERFACES - Contratos) │
│ ├─ chatwoot_repository.py │
│ │ Define QUÉ hacer con Chatwoot (no CÓMO) │
│ │ - create_or_get_contact() │
│ │ - create_or_get_conversation() │
│ │ - send_message() │
│ │ │
│ ├─ wuzapi_repository.py │
│ │ Define QUÉ hacer con WuzAPI (no CÓMO) │
│ │ - send_text_message() │
│ │ │
│ └─ cache_repository.py │
│ Define QUÉ hacer con el caché (no CÓMO) │
│ - get_conversation_id() │
│ - set_conversation_id() │
│ │
│ Responsabilidad: Reglas de negocio puras │
│ Define interfaces (ports) sin conocer implementaciones │
│ CERO dependencias externas │
│ │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ CAPA EXTERNA - SALIDA │
│ 📤 Adaptadores Secundarios (Infraestructura) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ src/infrastructure/ │
│ │ │
│ ├─ chatwoot/client.py │
│ │ Implementa ChatwootRepository │
│ │ - Comunicación HTTP con API Chatwoot │
│ │ - Maneja autenticación │
│ │ - Transforma respuestas HTTP → entidades del dominio │
│ │ │
│ ├─ wuzapi/client.py │
│ │ Implementa WuzAPIRepository │
│ │ - Comunicación HTTP con API WuzAPI │
│ │ - Normaliza números de teléfono │
│ │ - Maneja formato de mensajes │
│ │ │
│ ├─ persistence/ │
│ │ ├─ redis_cache.py │
│ │ │ Implementa CacheRepository con Redis │
│ │ │ - Conexión a Redis │
│ │ │ - Serialización/deserialización │
│ │ │ │
│ │ └─ memory_cache.py │
│ │ Implementa CacheRepository en memoria │
│ │ - Fallback cuando Redis no disponible │
│ │ - Dict Python simple │
│ │ │
│ └─ logging/setup.py │
│ Configuración de logging │
│ │
│ Responsabilidad: Detalles técnicos de implementación │
│ Pueden cambiar sin afectar el dominio │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 🛠️ SHARED - Utilidades Compartidas │
├─────────────────────────────────────────────────────────────────────┤
│ src/shared/config.py │
│ - Configuración centralizada con Pydantic │
│ - Validación de variables de entorno │
│ - Type-safe settings │
└─────────────────────────────────────────────────────────────────────┘
| Principio | Implementación en el Proyecto |
|---|---|
| Single Responsibility | - WhatsAppMessage: solo representa mensajes - ChatwootClient: solo se comunica con Chatwoot - SyncMessageToChatwootUseCase: solo sincroniza a Chatwoot |
| Open/Closed | - Puedes agregar nuevos adaptadores (ej: TwilioClient) sin modificar el dominio - Extensible vía implementación de ports |
| Liskov Substitution | - RedisCache y InMemoryCache son intercambiables - Ambos implementan CacheRepository |
| Interface Segregation | - Interfaces pequeñas y específicas - ChatwootRepository solo métodos de Chatwoot - No hay una interfaz gigante "Repository" |
| Dependency Inversion | - UseCase depende de interfaces (ports), no de implementaciones - ChatwootClient implementa la interfaz del dominio - El dominio no conoce a FastAPI, Redis, ni HTTP |
wuzapi-consumer/
│
├── 📂 src/ # Código fuente principal
│ │
│ ├── 📂 domain/ # ❤️ NÚCLEO DEL NEGOCIO
│ │ │ # Reglas de negocio puras
│ │ │ # CERO dependencias externas
│ │ │
│ │ ├── 📂 entities/ # Entidades con identidad
│ │ │ └── whatsapp_message.py # Representa un mensaje de WhatsApp
│ │ │ # - ID único (message_id)
│ │ │ # - Lógica de negocio
│ │ │ # - Factory methods
│ │ │
│ │ ├── 📂 value_objects/ # Objetos de valor inmutables
│ │ │ └── phone_number.py # Representa un número de teléfono
│ │ │ # - Inmutable (frozen dataclass)
│ │ │ # - Auto-validación
│ │ │ # - Sin identidad propia
│ │ │
│ │ └── 📂 ports/ # Interfaces (contratos)
│ │ ├── chatwoot_repository.py # QUÉ hacer con Chatwoot
│ │ ├── wuzapi_repository.py # QUÉ hacer con WuzAPI
│ │ └── cache_repository.py # QUÉ hacer con caché
│ │ # IMPORTANTE: Solo definen métodos
│ │ # NO implementan nada
│ │
│ ├── 📂 application/ # 🎯 CASOS DE USO
│ │ │ # Orquestación de lógica de negocio
│ │ │ # Coordinan múltiples operaciones
│ │ │
│ │ └── 📂 use_cases/
│ │ ├── sync_message_to_chatwoot.py
│ │ │ # Flujo: WhatsApp → Chatwoot
│ │ │ # 1. Recibe WhatsAppMessage
│ │ │ # 2. Crea/busca contacto
│ │ │ # 3. Crea/busca conversación
│ │ │ # 4. Envía mensaje
│ │ │
│ │ └── send_message_to_whatsapp.py
│ │ # Flujo: Chatwoot → WhatsApp
│ │ # 1. Recibe evento de Chatwoot
│ │ # 2. Extrae datos
│ │ # 3. Envía a WuzAPI
│ │
│ ├── 📂 infrastructure/ # 🔌 ADAPTADORES
│ │ │ # Detalles técnicos de implementación
│ │ │ # Pueden cambiar sin afectar dominio
│ │ │
│ │ ├── 📂 api/ # Entrada HTTP (FastAPI)
│ │ │ │ # Arquitectura Router-Handler
│ │ │ ├── app.py # Application Factory
│ │ │ │ # - Crea instancia FastAPI
│ │ │ │ # - Gestiona lifecycle (startup/shutdown)
│ │ │ │ # - Registra routers
│ │ │ │ # - Configura CORS y middleware
│ │ │ │
│ │ │ ├── dependencies.py # Dependency Injection Container
│ │ │ │ # - Crea singletons de clientes
│ │ │ │ # - Inyecta dependencias en handlers
│ │ │ │ # - Gestiona ciclo de vida de recursos
│ │ │ │
│ │ │ ├── 📂 routers/ # Definición de rutas HTTP
│ │ │ │ ├── wuzapi_router.py # POST /webhook/wuzapi
│ │ │ │ └── chatwoot_router.py # POST /webhook/chatwoot
│ │ │ │ # Responsabilidad: Solo recibir HTTP
│ │ │ │ # y delegar a handlers
│ │ │ │
│ │ │ └── 📂 handlers/ # Lógica de procesamiento
│ │ │ ├── base_handler.py # Template Method Pattern
│ │ │ │ # Clase base abstracta
│ │ │ ├── wuzapi_handler.py # Procesa eventos WuzAPI
│ │ │ │ # - Valida userID
│ │ │ │ # - Parsea mensajes
│ │ │ │ # - Ejecuta sincronización
│ │ │ └── chatwoot_handler.py # Procesa eventos Chatwoot
│ │ │ # - Valida eventos
│ │ │ # - Ejecuta envío a WhatsApp
│ │ │
│ │ ├── 📂 chatwoot/ # Salida a Chatwoot
│ │ │ └── client.py # Implementa ChatwootRepository
│ │ │ # - HTTP client con httpx
│ │ │ # - Autenticación con API key
│ │ │ # - Transforma JSON ↔ Entities
│ │ │
│ │ ├── 📂 wuzapi/ # Salida a WuzAPI
│ │ │ └── client.py # Implementa WuzAPIRepository
│ │ │ # - HTTP client con httpx
│ │ │ # - Autenticación con token
│ │ │ # - Normaliza teléfonos
│ │ │
│ │ ├── 📂 persistence/ # Salida a base de datos/caché
│ │ │ ├── redis_cache.py # Implementación con Redis
│ │ │ │ # - Conexión async
│ │ │ │ # - Serialización JSON
│ │ │ │
│ │ │ └── memory_cache.py # Implementación en memoria
│ │ │ # - Fallback sin Redis
│ │ │ # - Dict Python simple
│ │ │
│ │ ├── 📂 media/ # Descarga de multimedia
│ │ │ └── media_downloader.py # MediaDownloader
│ │ │ # - Descarga desde WuzAPI
│ │ │ # - Usa endpoints oficiales
│ │ │ # - Convierte base64 a bytes
│ │ │ # - Genera filenames únicos
│ │ │
│ │ └── 📂 logging/ # Configuración de logs
│ │ └── setup.py # Configuración de logging
│ │
│ └── 📂 shared/ # 🛠️ UTILIDADES COMPARTIDAS
│ └── config.py # Configuración con Pydantic
│ # - Lee variables de entorno
│ # - Validación automática
│ # - Type-safe settings
│
├── 📄 main.py # 🚀 ENTRY POINT
│ # - Configura e inicia FastAPI
│ # - Inicializa dependencias
│ # - Maneja ciclo de vida
│
├── 📄 .env # 🔐 Variables de entorno (NO commitear)
├── 📄 .env.example # 📝 Template de configuración
│
├── 📄 pyproject.toml # 📦 Dependencias del proyecto
│ # - Usa uv para gestión
│ # - Python 3.12+
│
├── 📄 docker-compose.yml # 🐳 Orquestación de contenedores
├── 📄 Dockerfile # 🐳 Imagen Docker
│
├── 📄 README.md # 📖 Este archivo
├── 📄 ARCHITECTURE.md # 🏗️ Detalles de arquitectura
├── 📄 CLAUDE.md # 🤖 Contexto para Claude AI
└── 📄 llm.txt # 🤖 Contexto para LLMs
Regla de Oro: NUNCA debe depender de infrastructure/ o application/
- entities/: Objetos con identidad única (ej: WhatsAppMessage tiene un
message_id) - value_objects/: Objetos inmutables sin identidad (ej: PhoneNumber, Email)
- ports/: Interfaces que definen QUÉ hacer, no CÓMO hacerlo
Regla de Oro: Depende de domain/, NO de infrastructure/
- Coordina múltiples operaciones del dominio
- Usa los ports (interfaces) para comunicarse con el exterior
- NO sabe de HTTP, Redis, SQL, etc.
Regla de Oro: Implementa los ports del dominio
- Puede cambiar completamente sin afectar
domain/oapplication/ - Ejemplo: cambiar de Redis a Memcached solo requiere crear nuevo adaptador
Separación de responsabilidades (clean separation of concerns):
-
app.py: Application Factory Pattern
- Crea instancia de FastAPI con configuración
- Gestiona lifecycle (startup/shutdown)
- Registra todos los routers
-
dependencies.py: Dependency Injection Container
- Patrón Singleton para clientes HTTP
- Inyecta dependencias en handlers y use cases
- Facilita testing (fácil mockear)
-
routers/: Capa HTTP pura
- Solo define rutas y recibe requests
- NO tiene lógica de negocio
- Delega procesamiento a handlers
-
handlers/: Lógica de procesamiento
- Template Method Pattern (base_handler.py)
- Valida payloads
- Ejecuta use cases
- Maneja respuestas HTTP
- Código usado por todas las capas
- Configuración, constantes, helpers
- Python: 3.12+
- Redis: 6+ (opcional, tiene fallback en memoria)
- Nginx: Con SSL/TLS (producción)
- WuzAPI: Instancia activa con token
- Chatwoot: Cuenta con API key
cd /home/wuzapi-consumer
# Instalar uv (gestor de dependencias moderno)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Instalar dependencias del proyecto
uv sync# Copiar template
cp .env.example .env
# Editar con tus credenciales
nano .envContenido del .env:
# =============================================================================
# CONFIGURACIÓN WUZAPI ↔ CHATWOOT (1:1)
# =============================================================================
# Servidor
PORT=8789
HOST=0.0.0.0
LOG_LEVEL=INFO
BACKEND_URL=integracion.wuzapi.torneofututel.com
# ============= CHATWOOT =============
CHATWOOT_URL=https://chatwoot.fututel.com
CHATWOOT_API_KEY=tu_api_key_aqui
CHATWOOT_ACCOUNT_ID=2
CHATWOOT_INBOX_ID=29
# ============= WUZAPI =============
WUZAPI_URL=https://wuzapi.torneofututel.com
WUZAPI_USER_TOKEN=tu_user_token_aqui
WUZAPI_INSTANCE_TOKEN=tu_instance_token_aqui
# ============= REDIS =============
REDIS_URL=redis://localhost:6379/0
# ============= CELERY =============
USE_CELERY=false# Instalar certbot si no está instalado
apt install certbot python3-certbot-nginx -y
# Obtener certificado SSL automático
certbot --nginx -d integracion.wuzapi.torneofututel.com --non-interactive --agree-tos --email [email protected]
# Nginx configurará SSL automáticamente
systemctl reload nginxcat > /etc/systemd/system/wuzapi-chatwoot-integration.service << 'EOF'
[Unit]
Description=WuzAPI Chatwoot Integration Webhook
After=network.target redis.service nginx.service
[Service]
Type=simple
User=root
WorkingDirectory=/home/wuzapi-consumer
Environment="PATH=/root/.local/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/root/.local/bin/uv run python main.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
# Recargar systemd
systemctl daemon-reload
# Habilitar autostart
systemctl enable wuzapi-chatwoot-integration
# Iniciar servicio
systemctl start wuzapi-chatwoot-integration
# Ver estado
systemctl status wuzapi-chatwoot-integration- Abre la interfaz de WuzAPI
- Ve a la configuración de tu instancia
- Configura:
- Webhook URL:
https://integracion.wuzapi.torneofututel.com/webhook/wuzapi - Events:
Message(todos los mensajes) - IMPORTANTE: NO incluir el puerto
:8789en la URL
- Webhook URL:
Opción A: Via Interfaz Web
- Abre Chatwoot:
https://chatwoot.fututel.com - Ve a Settings → Integrations → Webhooks
- Click "Add Webhook"
- Completa:
- URL:
https://integracion.wuzapi.torneofututel.com/webhook/chatwoot - Events: Marca solo
message_created
- URL:
- Click "Create"
Opción B: Via API
curl -X POST https://chatwoot.fututel.com/api/v1/accounts/2/webhooks \
-H "api_access_token: TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://integracion.wuzapi.torneofututel.com/webhook/chatwoot",
"subscriptions": ["message_created"]
}'# Iniciar servicio
systemctl start wuzapi-chatwoot-integration
# Detener servicio
systemctl stop wuzapi-chatwoot-integration
# Reiniciar servicio
systemctl restart wuzapi-chatwoot-integration
# Ver estado
systemctl status wuzapi-chatwoot-integration
# Ver logs en tiempo real
journalctl -u wuzapi-chatwoot-integration -f
# Ver últimas 100 líneas de logs
journalctl -u wuzapi-chatwoot-integration -n 100
# Ver solo errores
journalctl -u wuzapi-chatwoot-integration -p err
# Ver logs desde hace 2 horas
journalctl -u wuzapi-chatwoot-integration --since "2 hours ago"# Local
curl http://localhost:8789/health | jq
# Público (HTTPS)
curl https://integracion.wuzapi.torneofututel.com/health | jqRespuesta esperada:
{
"status": "healthy",
"mapping": {
"wuzapi_token": "CCE6198C6E2D-43A0-A4A9-598F53FE5C38",
"chatwoot_inbox": "29"
},
"integrations": {
"wuzapi": {
"url": "https://wuzapi.torneofututel.com",
"configured": true
},
"chatwoot": {
"url": "https://chatwoot.fututel.com",
"configured": true
}
},
"cache": {
"type": "redis"
}
}┌─────────────┐
│ 📱 Cliente │ Envía mensaje "Hola"
│ WhatsApp │
└──────┬──────┘
↓
┌──────────────────┐
│ 💚 WuzAPI │ Recibe mensaje
│ (Tu Instancia) │
└──────┬───────────┘
↓ webhook POST
┌───────────────────────────────────────────────────────────┐
│ 🌐 Nginx (SSL) │
│ https://integracion.wuzapi.torneofututel.com │
└──────┬────────────────────────────────────────────────────┘
↓ proxy_pass
┌───────────────────────────────────────────────────────────┐
│ 🐍 FastAPI Backend (localhost:8789) │
│ src/infrastructure/api/webhook.py │
│ │
│ 1. Valida TOKEN (¿Es mi instancia?) │
│ 2. Parsea JSON → WhatsAppMessage entity │
└──────┬────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ 🎯 SyncMessageToChatwootUseCase │
│ src/application/use_cases/sync_message_to_chatwoot.py │
│ │
│ Paso A: Buscar/crear contacto │
│ → ChatwootClient.create_or_get_contact() │
│ → Busca por teléfono en Chatwoot │
│ → Si no existe, lo crea │
│ │
│ Paso B: Buscar/crear conversación │
│ → ChatwootClient.create_or_get_conversation() │
│ → Busca conversación existente │
│ → Si no existe, la crea en el inbox correcto │
│ │
│ Paso C: Enviar mensaje │
│ → ChatwootClient.send_message() │
│ → Envía contenido del mensaje │
└──────┬────────────────────────────────────────────────────┘
↓ HTTP API calls
┌───────────────────────────────────────────────────────────┐
│ 💬 Chatwoot │
│ https://chatwoot.fututel.com │
│ │
│ ✅ Mensaje aparece en Inbox 29 │
└───────────────────────────────────────────────────────────┘
↓
┌──────────────┐
│ 👤 Agente │ Ve el mensaje
│ Chatwoot │
└──────────────┘
┌──────────────┐
│ 👤 Agente │ Responde "¿Cómo te ayudo?"
│ Chatwoot │
└──────┬───────┘
↓
┌───────────────────────────────────────────────────────────┐
│ 💬 Chatwoot │
│ Detecta mensaje outgoing │
└──────┬────────────────────────────────────────────────────┘
↓ webhook POST
┌───────────────────────────────────────────────────────────┐
│ 🌐 Nginx (SSL) │
│ https://integracion.wuzapi.torneofututel.com │
└──────┬────────────────────────────────────────────────────┘
↓ proxy_pass
┌───────────────────────────────────────────────────────────┐
│ 🐍 FastAPI Backend (localhost:8789) │
│ src/infrastructure/api/webhook.py │
│ │
│ 1. Parsea evento de Chatwoot │
│ 2. Extrae datos del mensaje │
└──────┬────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ 🎯 SendMessageToWhatsAppUseCase │
│ src/application/use_cases/send_message_to_whatsapp.py │
│ │
│ 1. Valida que sea mensaje outgoing │
│ 2. Extrae número de teléfono del contacto │
│ 3. Extrae contenido del mensaje │
│ 4. Llama a WuzAPIClient.send_text_message() │
└──────┬────────────────────────────────────────────────────┘
↓ HTTP API call
┌───────────────────────────────────────────────────────────┐
│ 💚 WuzAPI │
│ https://wuzapi.torneofututel.com │
│ │
│ Envía mensaje por WhatsApp │
└──────┬────────────────────────────────────────────────────┘
↓
┌─────────────┐
│ 📱 Cliente │ Recibe "¿Cómo te ayudo?"
│ WhatsApp │
└─────────────┘
❌ MAL: "Voy a refactorizar todo el código"
✅ BIEN: "Voy a agregar funcionalidad nueva sin tocar lo existente"
❌ MAL: Eliminar funcionalidad antigua
✅ BIEN: Deprecar y crear nueva versión
❌ MAL: domain/ importa de infrastructure/
✅ BIEN: domain/ solo conoce sus propias interfaces (ports)
❌ MAL: use_case = UseCase() # Crea sus propias dependencias
✅ BIEN: use_case = UseCase(repo, cache) # Recibe dependencias
PASO 1: Extender el Dominio (si es necesario)
# src/domain/entities/whatsapp_message.py
# ✅ AGREGAR nuevo método, NO modificar existentes
def extract_image_url(self) -> Optional[str]:
"""Extrae URL de imagen si el mensaje la contiene"""
if self.message_type == MessageType.IMAGE:
return self.metadata.get('url')
return NonePASO 2: Extender el Port (Interfaz)
# src/domain/ports/wuzapi_repository.py
# ✅ AGREGAR nuevo método a la interfaz
from abc import ABC, abstractmethod
class WuzAPIRepository(ABC):
@abstractmethod
async def send_text_message(self, phone: str, message: str) -> bool:
"""Método existente - NO TOCAR"""
pass
@abstractmethod
async def send_image_message(
self,
phone: str,
image_url: str,
caption: str = ""
) -> bool:
"""NUEVO método para enviar imágenes"""
passPASO 3: Implementar en el Adaptador
# src/infrastructure/wuzapi/client.py
# ✅ AGREGAR nuevo método, NO modificar send_text_message
class WuzAPIClient(WuzAPIRepository):
# Método existente - NO TOCAR
async def send_text_message(self, phone: str, message: str) -> bool:
# ... código existente ...
pass
# NUEVO método
async def send_image_message(
self,
phone: str,
image_url: str,
caption: str = ""
) -> bool:
"""Implementación para enviar imagen"""
try:
phone_clean = phone.replace('+', '').replace('@s.whatsapp.net', '')
recipient = f"{phone_clean}@s.whatsapp.net"
url = "/message/image"
data = {
'phone': recipient,
'image': image_url,
'caption': caption
}
response = await self.client.post(url, json=data)
return response.status_code in [200, 201]
except Exception as e:
logger.error(f"Error enviando imagen: {e}")
return FalsePASO 4: Usar en el Caso de Uso (si aplica)
# src/application/use_cases/send_message_to_whatsapp.py
# ✅ AGREGAR lógica para detectar y enviar imágenes
async def execute(self, event_data: Dict[str, Any]) -> bool:
# ... validaciones existentes ...
# NUEVO: detectar si hay imagen
attachments = message_data.get('attachments', [])
if attachments:
for attachment in attachments:
if attachment.get('file_type') == 'image':
image_url = attachment.get('data_url')
await self.wuzapi_repo.send_image_message(
phone=phone,
image_url=image_url,
caption=content
)
return True
# Flujo existente para texto - NO TOCAR
return await self.wuzapi_repo.send_text_message(phone, content)# Ejecutar en modo desarrollo
uv run python main.py
# Ver logs en tiempo real
journalctl -u wuzapi-chatwoot-integration -f
# Test manual
# 1. Envía mensaje de WhatsApp
# 2. Verifica logs
# 3. Verifica que llegó a Chatwoot
# 4. Responde en Chatwoot
# 5. Verifica que llegó a WhatsApp# Ver logs del servicio
journalctl -u wuzapi-chatwoot-integration -n 50
# Ver errores específicos
journalctl -u wuzapi-chatwoot-integration -p err
# Verificar que el puerto no esté ocupado
lsof -i :8789
# Verificar configuración
cat .env
# Probar arranque manual
cd /home/wuzapi-consumer
uv run python main.py# 1. Verificar que el webhook esté configurado en WuzAPI
# Ve a la interfaz de WuzAPI y verifica la URL del webhook
# 2. Ver logs cuando envías un mensaje
journalctl -u wuzapi-chatwoot-integration -f
# 3. Verificar que el TOKEN sea correcto
grep WUZAPI_INSTANCE_TOKEN .env
# 4. Test manual del webhook
curl -X POST https://integracion.wuzapi.torneofututel.com/webhook/wuzapi \
-H "Content-Type: application/json" \
-d '{"type":"Message","token":"TU_TOKEN"}'# 1. Verificar que el webhook esté configurado en Chatwoot
# Settings → Integrations → Webhooks
# 2. Ver logs cuando respondes en Chatwoot
journalctl -u wuzapi-chatwoot-integration -f
# 3. Verificar conectividad con WuzAPI
curl https://wuzapi.torneofututel.com/health# Verificar que Redis esté corriendo
systemctl status redis
# Iniciar Redis si está detenido
systemctl start redis
# Ver logs de Redis
journalctl -u redis -f
# Test de conexión
redis-cli ping
# Debe responder: PONG
# NOTA: Si Redis no funciona, el sistema usa caché en memoria automáticamente# Verificar certificados
certbot certificates
# Renovar certificados
certbot renew
# Ver logs de Nginx
tail -f /var/log/nginx/integracion_wuzapi_error.log
# Test de SSL
curl -v https://integracion.wuzapi.torneofututel.com/health- ARCHITECTURE.md: Detalles profundos de la arquitectura hexagonal
- CLAUDE.md: Contexto y reglas para Claude AI
- llm.txt: Formato condensado para LLMs
Proyecto propietario de Fututel.
Fututel - Equipo de Ingeniería
- Sistema desarrollado para integración empresarial
- Arquitectura diseñada para escalabilidad y mantenibilidad
- Contacto: [email protected]
- ✅ Arquitectura hexagonal completa
- ✅ Soporte para nuevo formato de WuzAPI (con token)
- ✅ Validación por TOKEN en lugar de instanceId
- ✅ Logs estructurados con JSON completo
- ✅ Documentación exhaustiva
- Versión inicial con formato antiguo de WuzAPI