Files
DeskcommCRM/scripts/seed-e2e-nascimento-do-lead.ts
jmpoandClaude Opus 5 f691b9c8f8 merge: 198 commits da main — duas convergências e um desfecho a corrigir
O fork estava 198 commits atrás. A `main` já tinha absorvido o PR #200
(canal Zernio) e renumerado as migrations que vieram daqui, então boa
parte do conflito era o MESMO trabalho com dois números.

─── O que era duplicata ───

`0117_canal_zernio_vocabulario` e `0118_zernio_envio` diferem de `0131` e
`0132` em UMA linha: o número no comentário do cabeçalho. São as minhas,
renumeradas lá. Ficam as de lá — numeração é verdade compartilhada, e
manter as duas deixaria o mesmo schema com dois nomes.

`0119_contato_lookup_telefone` e `0122_telefone_do_lid` PARECIAM a mesma
convergência e não são: uma acrescenta `phone_lookup_at` (fila do cron de
backfill), a outra `wa_lid` (coluna gerada + dedup). Convivem.

─── O defeito que o merge criaria, e que ninguém veria ───

A `0139_kind_check_completo` reconstrói `agent_inbox_items_kind_check`, e
a minha `0120` também. Pelo timestamp a minha roda DEPOIS — e a lista
dela não conhecia `midia_nao_lida` nem `contact_proposal_expired`.
Aplicar as duas em ordem, num clone atualizado, APAGARIA dois kinds em
uso: quem reconstrói por último é quem manda, e uma lista incompleta não
é "aditiva", é uma remoção silenciosa. A `0120` passou a trazer a lista
inteira.

No `baseline.sql` continua UM bloco só para essa constraint, agora com os
kinds dos dois lados — a regra do #159, que o `update.sh` de todo clone
depende.

─── Onde discordei da main, e por quê ───

`zernio.ts` — `isConfigured()` fica `true`, e não
`zernioCredsFromEnv() !== null`.

A preocupação de lá é real: `isConfigured() === true` não pode implicar
"sem credencial", porque o handler grava `sent` quando `send()` não
lança. Só que quem fecha esse par mudou de lugar — `send()` LANÇA
`zernio_not_configured`, e o caso descrito naquele comentário ("devolve
`{externalId: null}` SEM lançar") não existe mais no arquivo.

Exigir env reintroduziria um defeito medido: `resolveZernioCreds` procura
primeiro na SESSÃO e só depois no ambiente. Uma instalação que conectou
pelo botão tem a credencial no banco e nada no `.env` — a checagem por
env diria "não configurado" para um canal conectado e funcionando, e toda
mensagem ficaria em `queued` sem tentar sair.

Mas o teste de lá estava certo no DESFECHO: sem credencial nenhuma, o
certo é `queued` com o motivo, não `failed`. Isso não estava atendido de
nenhum dos dois lados — o `catch` do handler marcava `failed`. Agora
`notConfigured` é traduzido para `queued`, ao lado do
`storage_sign_failed` que já tinha tratamento próprio: falta de
credencial não é falha DESTA mensagem, é canal ainda não conectado, e
marcar `failed` faria o follow-up desistir de algo que sai sozinho assim
que alguém conectar.

Os dez casos de `messages-handler-canal-intermediado` passam.

─── O que fica vermelho, e não é daqui ───

`lib/ai/dispatcher/rate-limit.test.ts` — 5 casos estourando o timeout de
15s. Falha também com os arquivos da main trocados um a um, então não
vem desta resolução; o CI de lá está verde, então é diferença de
ambiente. Fica registrado, não escondido.

3647 casos passam. typecheck, lint:channels e build limpos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:57:33 -03:00

111 lines
4.0 KiB
TypeScript

/**
* Seed E2E do nascimento do lead (spec 17, passo 1).
*
* Semeia o MÍNIMO para que uma mensagem possa chegar pelo caminho de produção:
* uma `channel_session` com `webhook_path_token`, que é o que a rota
* `/api/v1/webhooks/waha/[token]` usa para descobrir de qual organização o
* evento é. **Nenhum contato, nenhuma conversa, nenhum lead** — quem tem de
* criar isso é o produto, e é justamente o que a spec mede.
*
* Grava também o id do funil `is_default`, porque a prova de tela precisa abrir
* o quadro certo (`/app/pipelines/<id>`) e o funil é do tenant, não fixo.
*
* Idempotente. Depende de `.e2e-creds.json` (rode `scripts/seed-e2e-credentials.ts`
* antes).
*
* Run: npx tsx scripts/seed-e2e-nascimento-do-lead.ts
*/
import { createClient } from "@supabase/supabase-js";
import * as fs from "node:fs";
import * as path from "node:path";
import { anunciarDestino, credenciaisSupabaseDeTeste } from "./lib/env-de-teste";
const credenciais = credenciaisSupabaseDeTeste();
anunciarDestino("seed-e2e-nascimento-do-lead", credenciais);
const admin = createClient(credenciais.url, credenciais.serviceRole, {
auth: { autoRefreshToken: false, persistSession: false },
});
const CREDS_PATH = path.join(process.cwd(), ".e2e-creds.json");
const SESSION_NAME = "e2e-nascimento-session";
/** Fixo e idempotente: o spec precisa saber a URL antes de chamar. */
const WEBHOOK_TOKEN = "e2e-nascimento-token-fixo-0001";
interface Creds {
org_id: string;
nascimento?: { webhook_token: string; session_name: string; pipeline_default_id: string };
}
function lerCreds(): Creds {
if (!fs.existsSync(CREDS_PATH)) {
throw new Error(".e2e-creds.json ausente — rode scripts/seed-e2e-credentials.ts primeiro");
}
return JSON.parse(fs.readFileSync(CREDS_PATH, "utf8")) as Creds;
}
async function garantirSessao(orgId: string): Promise<void> {
const { data: existente } = await admin
.from("channel_sessions")
.select("id")
.eq("organization_id", orgId)
.eq("waha_session_name", SESSION_NAME)
.maybeSingle();
if (existente) {
// Re-afirma o token: outra rodada pode tê-lo mudado, e o spec o usa na URL.
const { error } = await admin
.from("channel_sessions")
.update({ webhook_path_token: WEBHOOK_TOKEN } as never)
.eq("id", (existente as { id: string }).id);
if (error) throw new Error(`update channel_session: ${error.message}`);
return;
}
const { error } = await admin.from("channel_sessions").insert({
organization_id: orgId,
waha_session_name: SESSION_NAME,
display_name: "Número Nascimento E2E",
// Placeholder, como todas as rotas que criam sessão gravam. Sem header de
// assinatura o webhook aceita e devolve `signatureVerified: false` — que é
// exatamente o que acontece com WAHA Core em produção (webhook-auth.ts).
webhook_secret_encrypted: "\\x00",
webhook_path_token: WEBHOOK_TOKEN,
} as never);
if (error) throw new Error(`insert channel_session: ${error.message}`);
}
async function funilPadrao(orgId: string): Promise<string> {
const { data, error } = await admin
.from("crm_pipelines")
.select("id")
.eq("organization_id", orgId)
.eq("is_default", true)
.eq("is_archived", false)
.maybeSingle();
if (error) throw new Error(`select pipeline default: ${error.message}`);
if (!data) {
throw new Error(
"a organização de e2e não tem funil is_default — sem ele o lead não nasce, e o spec mediria o produto errado",
);
}
return (data as { id: string }).id;
}
async function main(): Promise<void> {
const creds = lerCreds();
await garantirSessao(creds.org_id);
const pipelineDefaultId = await funilPadrao(creds.org_id);
creds.nascimento = {
webhook_token: WEBHOOK_TOKEN,
session_name: SESSION_NAME,
pipeline_default_id: pipelineDefaultId,
};
fs.writeFileSync(CREDS_PATH, `${JSON.stringify(creds, null, 2)}\n`);
console.info(`seed-e2e-nascimento-do-lead: ok (funil de entrada ${pipelineDefaultId})`);
}
void main();