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:
-
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.
- Exiba um banner na UI instruindo e incentivando a pessoa:
-
Ao Criar Uma Nova Conexão:
- Uma chamada de submissão para
POST /api/whatsapp-instances/[id]/connectionsprovisionará e pareará exclusivamente Evolution GO para eles a partir de agora.
- Uma chamada de submissão para
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.
-
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 } ]); -
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 comexpect(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. -
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.
-
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 paiWhatsAppInstancee, independentemente, inserir os filhos naWhatsAppConnectionpara viabilizar as chaves descritas no seu.env.test. Se o teste requerer cenários híbridos, plante apenas as flags de V2. -
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
.instanceIdUUID). - Asserção: Ambos os requests precisam alcançar com clareza seu
messageTracegerando os logs apropriados sem Undefined Null Pointer Exception, validando que outils.tsnovo suporta o Híbrido perfeitamente.
- Inject 1: Payload mock imitando V2 (só contém