Documentação / Guia Definitivo e Detalhado: Migração para Arquitetura Unificada Evolution GO (Multi-Instâncias)

Guia Definitivo e Detalhado: Migração para Arquitetura Unificada Evolution GO (Multi-Instâncias)

Entrar

Guia Definitivo e Detalhado: Migração para Arquitetura Unificada Evolution GO (Multi-Instâncias)

1. Introdução e Visão Geral da Arquitetura

O sistema atualmente utiliza um modelo acoplado onde uma WhatsAppInstance atua como base tanto para o provedor primário (Evolution V2) quanto para o provedor de contingência (Evolution GO em Dual-Link).

A modernização prevê a transição para multi-instâncias no Evolution GO (onde 1 Número Lógico possui N conexões), GARANTINDO RETROCOMPATIBILIDADE absoluta com as instâncias V2 já conectadas produtoras de receita.

1.1 Diagrama de Arquitetura (Fase de Transição Híbrida)

graph TD
    subgraph "Camada Lógica (Pilot DB)"
        WI(WhatsAppInstance<br/>+v2InstanceName<br/>+v2State)
        WC1(WhatsAppConnection 1<br/>+goInstanceToken)
        WC2(WhatsAppConnection 2<br/>+goInstanceToken)
    end

    subgraph "Camada de Provedores"
        V2[Evolution V2 Server <br/>_Legado / Mantido_]
        GOS[Evolution GO Server Cluster]
    end

    WI -->|Legado se OPEN| V2
    WI --> WC1
    WI --> WC2
    
    WC1 --> GOS
    WC2 --> GOS

2. Refatoração do Banco de Dados (schema.prisma)

Para manter a retrocompatibilidade, nenhuma coluna do V2 será removida na Fase 1. A tabela WhatsAppConnection passa a ser injetada de forma complementar, absorvendo as sessões GO.

2.1 Prisma Estrutural

model WhatsAppInstance {
  id               String    @id @default(cuid())
  tenantId         String    @map("tenant_id")
  instanceName     String    @unique @map("instance_name") 
  number           String    @map("number")
  displayName      String    @default("") @map("display_name")
  
  // connectionMode ("SINGLE", "CASCADE_FALLBACK", "ROUND_ROBIN")
  connectionMode   String    @default("SINGLE") @map("connection_mode")

  // === MANTIDOS PARA RETROCOMPATIBILIDADE (LEGADO V2) ===
  v2InstanceName     String?   @unique @map("v2_instance_name")
  v2State            String?   @default("CLOSE") @map("v2_state")
  v2InstanceApiKey   String?   @map("v2_instance_api_key") @db.Text
  
  // Remoção apenas das colunas planas do GO, que migram para a nova tabela
  // REMOVIDO: goInstanceId, goInstanceToken, goState
  
  connections      WhatsAppConnection[]
  messages         Message[]
}

// NOVA TABELA
model WhatsAppConnection {
  id                  String    @id @default(cuid())
  whatsappInstanceId  String    @map("whatsapp_instance_id")
  
  // Base do GO
  goInstanceId        String    @unique @map("go_instance_id")
  goInstanceName      String?   @map("go_instance_name")
  goInstanceToken     String    @map("go_instance_token") @db.Text
  
  state               String    @default("CLOSE") // OPEN, CONNECTING, CLOSE
  priority            Int       @default(0) // 0 para master
  isActive            Boolean   @default(true) @map("is_active")

  whatsappInstance    WhatsAppInstance @relation(fields: [whatsappInstanceId], references: [id], onDelete: Cascade)
  
  @@index([whatsappInstanceId])
  @@map("whatsapp_connections")
}

2.2 Migração Apenas do Go Existente

As conexões do Evo V2 não sofrerão migração. Apenas quem estiver no Dual-Link (e portanto tiver dados em goInstanceId) sofrerá o script:

INSERT INTO whatsapp_connections (id, whatsapp_instance_id, go_instance_id, go_instance_name, go_instance_token, state, is_active)
SELECT 
    gen_random_uuid(), id, go_instance_id, go_instance_name, go_instance_token, COALESCE(go_state, 'CLOSE'), true 
FROM whatsapp_instances WHERE go_instance_id IS NOT NULL;

3. O Sistema de Roteamento de Mensagens com Backwards Compatibility

No worker (apps/worker/src/send-message.ts), o roteamento precisa conferir tanto as conexões N do Go quanto o Estado Legado do V2, aplicando fallback se necessário.

// Refatoração em apps/worker/src/send-message.ts
async function processMessage(job) {
    const message = await prisma.message.findUnique({
        where: { id: messageId },
        include: { whatsappInstance: true } // Traz a master com campos v2
    });

    const instance = message.whatsappInstance;
    
    // Busca N Conexões Multi-EvoGO
    const activeGoConnections = await prisma.whatsAppConnection.findMany({
         where: { whatsappInstanceId: instance.id, state: 'OPEN', isActive: true },
         orderBy: { priority: "asc" } 
    });

    let sent = false;

    // TENTATIVA 1: Roteamento nas Novas Conexões GO (Cluster Primário)
    for(const conn of activeGoConnections) {
         try {
             const providerClient = getGoConnectionClient(conn.goInstanceToken);
             await providerClient.messages.sendText({ ...payload });
             
             await prisma.message.update({
                 where: { id: message.id },
                 data: { whatsappConnectionId: conn.id } // Usou rede nova
             });
             sent = true;
             break;
         } catch(e) {
             logger.warn(`Falha no nó GO ${conn.id}. Tentando próxima rota.`);
         }
    }

    // TENTATIVA 2: Retrocompatibilidade (Fallback pro Legado V2)
    if (!sent && instance.v2State === "OPEN") {
         try {
             logger.info(`Go falhou ou não existe. Acionando Fallback Legacy V2 para a instância ${instance.v2InstanceName}`);
             
             // O código antigo ainda vive e é invocado
             const v2Client = getEvolutionV2Client(); 
             await v2Client.messages.sendText(instance.v2InstanceName, { ...payload });
             
             await prisma.message.update({
                 where: { id: message.id },
                 data: { sentViaProvider: "v2" } // Usou rede antiga
             });
             sent = true;
         } catch (err) {
             logger.error("Falha no fallback legacy V2.");
         }
    }

    if (!sent) {
        throw new EvolutionSendError("All instances disconnected or failed (Go and V2)");
    }
}

Essa lógica de TENTATIVA 1 e 2 cumpre 100% da retrocompatibilidade: Clientes novos ou atualizados rodam no GO Cluster. Clientes velhos que não re-parearam continuam passando liso no Fallback do V2.


4. Webhooks Híbridos (apps/fullstack/src/app/api/internal/webhook)

Como há instâncias operando em V2 e novas operando em GO, o webhook ingress (api/internal/webhook/handlers/connection.ts e afins) precisa decifrar de onde veio.

// apps/fullstack/src/app/api/internal/webhook/utils.ts
export async function getRootInstanceFromWebhookData(payload: any) {
     
     // 1. O Payload é do provedor EVO V2 (Legado)? 
     if (payload.instance && !payload.instanceId) {
          const mainInstance = await prisma.whatsAppInstance.findFirst({
               where: { v2InstanceName: payload.instance }
          });
          return mainInstance ? { rootInstance: mainInstance, connection: null /* legacy */ } : null;
     }

     // 2. O Payload é do provedor EVO GO (Multi-link)?
     const goUuid = payload.instanceId;
     if (goUuid) {
          const conn = await prisma.whatsAppConnection.findUnique({
               where: { goInstanceId: goUuid },
               include: { whatsappInstance: true }
          });
          return conn ? { rootInstance: conn.whatsappInstance, connection: conn } : null;
     }
}

Isso mantém seus webhooks agnósticos de forma inquebrável.


5. UI (Frontend) / Abordagem de Migração Visível Sugerida

Em apps/fullstack/src/spa/pages/Numbers.tsx:

  1. Clientes com apenas V2 ativo:

    • Exiba um banner na UI instruindo e incentivando a pessoa: "Recomendamos conectar na Nova Adicionadora Padrão do Pilot. Clique para re-parear."
    • O card visual mostrará "Conexão Antiga (V2)" e as opções de gerenciar/descadastrar.
  2. Ao Criar Uma Nova Conexão:

    • Uma chamada de submissão para POST /api/whatsapp-instances/[id]/connections provisionará e pareará exclusivamente Evolution GO para eles a partir de agora.

Assim que a vasta maioria transacionar naturalmente para as Placas GO de conexão através do encorajamento visual da SPA, você poderá começar a agendar o desligamento paulatino do servidor V2 sem ansiedade operacional.


6. Atualização de Testes (Unitários e de Integração)

A substituição e divisão das funções impactará o ecossistema de testes do projeto. As suítes deverão agora mockar coleções de instâncias em vez do provider monolítico.

6.1 Redirecionando os Testes Unitários (apps/worker/src/test/unit/)

A lógica de negócio unitária (particularmente no send-message.test.ts e whatsapp-instance-validate.test.ts) precisará de mocks independentes do comportamento 1:N.

  1. Testes Base do Roteador (Worker): Como extrairemos a obrigatoriedade do Provider V2, no Jest você mockará o retorno do Prisma para emular uma situação Multi-Node com fallback:

    // Mock de estado (Jest) para Multi-Link
    prismaMock.whatsAppConnection.findMany.mockResolvedValue([
        { id: "conn_1", goInstanceToken: "token_master", state: "OPEN", priority: 0 },
        { id: "conn_2", goInstanceToken: "token_slave", state: "OPEN", priority: 1 }
    ]);
    
  2. Afirmação do Disparo Sequencial: Teste um caso de "Fallback Node" forçando o Node Base (conn_1) a falhar (mock de erro na API do GO) e verificando com expect(mockGoClient.sendText).toHaveBeenCalledTimes(2) para certificar que o script acionou a próxima rota (conn_2), sem que a mensagem tenha morrido antes do loop.

  3. Afirmação Condicional Legada (Retrocompatibilidade): Teste o pior cenário sem nós do Evolution GO:

    prismaMock.whatsAppConnection.findMany.mockResolvedValue([]); // Nada na malha Go
    
    // Simula que a instância mestra tem V2 configurado e logado
    const result = await processMessage(mockJob);
    expect(mockEvolutionV2Client).toHaveBeenCalled(); // Garante o Fallback legado
    

6.2 Redirecionando Testes de Integração (apps/worker/src/test/integration/)

As interações end-to-end com bancos e Redis devem focar no modelo real.

  1. Infraestrutura e Factory de Seeds (tests/integration/...): A função que fabrica instâncias para o banco isolado de teste (seed()) terá que criar primeiramente o pai WhatsAppInstance e, independentemente, inserir os filhos na WhatsAppConnection para viabilizar as chaves descritas no seu .env.test. Se o teste requerer cenários híbridos, plante apenas as flags de V2.

  2. Teste dos Casos Limite do Webhook (Contract E2E): Em testes como send-message-webhook-contract.integration.test.ts, valide injetando cargas simuladas de callback via request interno.

    • Inject 1: Payload mock imitando V2 (só contém .instance).
    • Inject 2: Payload mock imitando GO (contém .instanceId UUID).
    • Asserção: Ambos os requests precisam alcançar com clareza seu messageTrace gerando os logs apropriados sem Undefined Null Pointer Exception, validando que o utils.ts novo suporta o Híbrido perfeitamente.