Skip to content

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 /health e POST /webhook; qualquer outra rota → 404
  • withErrorBoundary traduz exceções não tratadas em response HTTP

2. Handlers

Arquivo: src/handlers/inbound-webhook.handler.ts

Ordem de execução obrigatória:

PassoAçãoResultado
1parsePayloadBody vazio → ValidationError (boundary → HTTP 400)
2mapWebhookPayloadToIncomingMessageNormaliza phone (apenas dígitos), resolve type/text/receivedAt
3Sem messageId ou phone200 {"ignored": true}
4incoming.fromMe === true200 {"ignored": "self"}
5api.checkIdempotency(messageId, phone)Se duplicado → 200 {"ignored": "duplicate"}
6api.getAccountByPhone(phone)Devolve ContrasyncAccount | null
7router.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:

EstadoDetectorComportamento
Não vinculadoaccount === nullOTP-shaped → verify. Identifier-shaped → issue. Else → help
Empresa pendenteaccount.defaultCompanyId === nullNúmero → tenta switch. Outra coisa → re-lista empresas
Vinculadoaccount.defaultCompanyId !== nullstatus, 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/

AdapterArquivosFunção
Z-APIzapi/zapi.client.ts, zapi.mapper.ts, zapi.types.tsRecebe webhook + envia mensagens
Contrasync APIcontrasync-api/contrasync-api.client.ts, contrasync-api.types.tsCliente HTTP do WhatsappModule no nest-api

ContrasyncApiClient traduz HTTP errors:

  • 4xx → ValidationError carregando a mensagem do upstream (volta pro usuário)
  • 5xx/timeout/network → BotError 502 com mensagem genérica (mascarada pelo boundary)

Detalhes do contrato em contrasync-api-integration.md.

5. Domain

Arquivos: src/domain/

  • interfaces/: IncomingMessage, OutgoingTextMessage, SentMessageReceipt
  • constants/: 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 CloudWatch
  • error-boundary.ts: BotError, ValidationError, UnauthorizedError + withErrorBoundary

Regras de dependência

  1. handler.ts → handlers, services, common, adapters
  2. handlers → services, adapters, common, domain
  3. services → adapters (qualquer), domain, common
  4. adapters → domain, common (nunca importam services ou handlers)
  5. helpers → domain apenas
  6. 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