mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
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>
111 lines
4.0 KiB
TypeScript
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();
|