Appearance
Arquitetura em Camadas
Fluxo de dados de uma mensagem do WhatsApp até a resposta, com integração na API NestJS.
Visão geral
┌──────────────────────────────────────────────────────────────────┐
│ WHATSAPP USER │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Z-API │
│ POST {webhook-url}/webhook │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ API GATEWAY HTTP API │
│ /webhook /health │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ LAMBDA HANDLER (src/handler.ts) │
│ - Bootstrap module-level (ZapiClient, ContrasyncApiClient, │
│ BotRouterService, InboundWebhookHandler) │
│ - Roteamento /webhook /health /* │
│ - withErrorBoundary envolvendo tudo │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ INBOUND WEBHOOK HANDLER │
│ 1. parsePayload (body vazio → ValidationError → 400) │
│ 2. mapWebhookPayloadToIncomingMessage │
│ 3. Filtra fromMe / sem messageId / sem phone │
│ 4. ContrasyncApiClient.checkIdempotency → drop se duplicado │
│ 5. ContrasyncApiClient.getAccountByPhone │
│ 6. BotRouterService.route(incoming, account) │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ BOT ROUTER SERVICE │
│ Despacha baseado no estado de `account`: │
│ account=null → fluxo de connect (OTP) │
│ defaultCompanyId=null → escolha de empresa │
│ defaultCompanyId set → comandos (status / trocar / help) │
└──────┬───────────────────────────────┬───────────────────────────┘
│ │
│ (responder ao usuário) │ (consultar/mudar dados)
▼ ▼
┌──────────────────┐ ┌──────────────────────────────────────┐
│ ZAPI CLIENT │ │ CONTRASYNC API CLIENT │
│ POST /send-text │ │ /whatsapp/bot/otp/issue │
│ │ │ /whatsapp/bot/otp/verify │
│ │ │ /whatsapp/bot/companies │
│ │ │ /whatsapp/bot/switch-company │
└────────┬─────────┘ └────────────────┬─────────────────────┘
│ │
▼ ▼
USER WHATSAPP nest-api (WhatsappModule)Camadas
1. Handler (Lambda entrypoint)
Arquivo: src/handler.ts
- Bootstrap das dependências no escopo do módulo (não dentro do handler) para reaproveitar contexto entre invocations
- Roteamento HTTP:
GET /healthePOST /webhook; qualquer outra rota → 404 withErrorBoundarytraduz exceções não tratadas em response HTTP
2. Handlers
Arquivo: src/handlers/inbound-webhook.handler.ts
Ordem de execução obrigatória:
| Passo | Ação | Resultado |
|---|---|---|
| 1 | parsePayload | Body vazio → ValidationError (boundary → HTTP 400) |
| 2 | mapWebhookPayloadToIncomingMessage | Normaliza phone (apenas dígitos), resolve type/text/receivedAt |
| 3 | Sem messageId ou phone | 200 {"ignored": true} |
| 4 | incoming.fromMe === true | 200 {"ignored": "self"} |
| 5 | api.checkIdempotency(messageId, phone) | Se duplicado → 200 {"ignored": "duplicate"} |
| 6 | api.getAccountByPhone(phone) | Devolve ContrasyncAccount | null |
| 7 | router.route(incoming, account) | Dispatch vide camada Services |
3. Services
Arquivo: src/services/bot-router.service.ts
BotRouterService é uma máquina de estado stateless, derivada do que a API responde:
| Estado | Detector | Comportamento |
|---|---|---|
| Não vinculado | account === null | OTP-shaped → verify. Identifier-shaped → issue. Else → help |
| Empresa pendente | account.defaultCompanyId === null | Número → tenta switch. Outra coisa → re-lista empresas |
| Vinculado | account.defaultCompanyId !== null | status, trocar empresa [N], else → help |
Mensagens em src/domain/constants/bot-replies.const.ts.
Erros vindos da API:
ValidationError(4xx) → mensagem do upstream vira reply ao usuário via Z-API e devolve 200- Outros (5xx/network) → bubble up para o error boundary
4. Adapters
Arquivos: src/adapters/
| Adapter | Arquivos | Função |
|---|---|---|
| Z-API | zapi/zapi.client.ts, zapi.mapper.ts, zapi.types.ts | Recebe webhook + envia mensagens |
| Contrasync API | contrasync-api/contrasync-api.client.ts, contrasync-api.types.ts | Cliente HTTP do WhatsappModule no nest-api |
ContrasyncApiClient traduz HTTP errors:
- 4xx →
ValidationErrorcarregando a mensagem do upstream (volta pro usuário) - 5xx/timeout/network →
BotError 502com mensagem genérica (mascarada pelo boundary)
Detalhes do contrato em contrasync-api-integration.md.
5. Domain
Arquivos: src/domain/
interfaces/:IncomingMessage,OutgoingTextMessage,SentMessageReceiptconstants/:bot-commands.const.ts(planejado para fases futuras),bot-replies.const.ts(textos das respostas)enums/:BotState(planejado para máquina de conversa em Fase 4+)
Zero dependência de framework. Só tipos e textos.
6. Common
Arquivos: src/common/
logger.ts: JSON estruturado para CloudWatcherror-boundary.ts:BotError,ValidationError,UnauthorizedError+withErrorBoundary
Regras de dependência
- handler.ts → handlers, services, common, adapters
- handlers → services, adapters, common, domain
- services → adapters (qualquer), domain, common
- adapters → domain, common (nunca importam services ou handlers)
- helpers → domain apenas
- domain → ZERO dependências
Idempotência ponta-a-ponta
Z-API pode retransmitir o mesmo messageId em casos de timeout. O bot sempre consulta /whatsapp/bot/idempotency antes de dispatchar. A tabela whatsapp_message_idempotency (chave única em messageId) garante atomicidade, duas requisições simultâneas com o mesmo messageId resultam em uma única ação efetiva.
A resposta { duplicate: true, processedAt: <ISO> } indica que outra invocação já processou. Em vez de reexecutar, o handler devolve 200 {"ignored":"duplicate"} e o Z-API marca como entregue.
Documento atualizado em Maio 2026