Pular para o conteúdo
  • Por Autor desconhecido
  • /

Twenty CRM: testar a normalização de uma oportunidade ganha

Execute o normalizador público Twenty com dados sintéticos, descubra seus limites e separe mudança de estágio, autenticação, persistência e entrega.

Para transformar uma oportunidade ganha do Twenty em um evento verificável, primeiro teste o mapeamento do objeto comercial. Depois comprove a transição de estágio, a identidade e o armazenamento. O normalizador público examinado aqui traduz um objeto com estágio won; ele não autentica webhooks, detecta transições ou envia conversões ao Google.

Resumo: O exemplo Twenty da ClickTrail converte um objeto sintético ganho em um payload com ID estável, valor, moeda e contexto publicitário. O teste abaixo executa o código público e revela limites importantes: repetição gera novamente o mesmo payload, ID ausente não é rejeitado e ganho não comprova pagamento. Uma integração real ainda precisa validar o esquema da instância, autenticar a origem e persistir o resultado sem duplicação.

Por FunnelSheet. Revisão técnica: 15 de setembro de 2026. Conflito de interesse: a FunnelSheet desenvolve e divulga ClickTrail. Esta é uma execução local de código público com dados sintéticos; nenhuma conta Twenty ou Google Ads foi utilizada.

Qual objeto este normalizador realmente recebe?

O normalizador recebe um objeto plano de oportunidade, não uma requisição HTTP completa. Na versão examinada, ele lê stage ou status, id, amount, currency, campos personalizados e uma referência de contato. Essa forma é o contrato do exemplo; precisa ser confrontada com o esquema da sua instância Twenty.

A fonte é twenty-crm-google-ads/src/index.mjs, no commit 0c792bf386fb04971ee1b347a5cfbc4acaab748e. Fixar o commit permite repetir a leitura mesmo que o repositório mude. Não foi verificada uma versão implantada do Twenty, nem publicação de pacote npm.

Entrada do exemplo Transformação observada Limite
stage ou status Converte para minúsculas e compara com won Não consulta estágio anterior
id Compõe twenty_<id>_purchase Não rejeita ausência
amount Converte com Number Pode produzir NaN
currency Usa maiúsculas; ausência vira USD Não comprova moeda do negócio
customFields Lê GCLID, GBRAID e WBRAID Não valida origem ou permissão

O artigo de atribuição conjunta entre Twenty e Chatwoot apresenta o papel dos sistemas. Este tutorial se concentra no comportamento executável de uma transformação, incluindo entradas que ela aceita sem validação suficiente.

Estágio ganho significa uma nova venda?

Um objeto atualmente ganho não demonstra que acabou de mudar para ganho. Duas atualizações da mesma oportunidade podem apresentar stage: 'won', mesmo que a segunda altere apenas uma anotação. O normalizador retorna um payload nas duas chamadas porque não mantém histórico.

Imagine a oportunidade sintética demo-op-17: primeiro está em negociação, depois ganha, depois continua ganha com uma descrição corrigida. O resultado esperado de uma política de primeiro ganho é registrar a ocorrência uma vez. A função isolada apenas distingue o objeto não ganho do objeto ganho; identificar a transição é responsabilidade de outra camada.

Defina também o significado comercial. O nome Purchase, produzido pelo exemplo, não confirma liquidação financeira. Se sua oportunidade ganha representa contrato assinado e o dinheiro chega semanas depois, documente essa diferença antes de configurar o evento no destino.

Para implementar a transição, proponha uma comparação com estado anterior confiável ou um registro persistente da ocorrência já processada. Não aceite um campo previousStage fornecido pelo navegador como prova. O servidor precisa obter esse estado da sua própria persistência ou de um mecanismo documentado da origem.

Como reproduzir o comportamento com dados sintéticos?

O bloco abaixo executa a função pública sem alterar seu comportamento e acrescenta assertions. Ele cobre estágio não ganho, ganho, repetição e campos ausentes. O resultado esperado é uma caracterização precisa do exemplo, incluindo comportamentos permissivos que não devem virar critérios de produção.

Salve como twenty-local.mjs e execute node twenty-local.mjs. O teste foi executado com Node.js v24.15.0 e não precisa de contas, pacotes adicionais ou rede. A saída é B10: normalizador caracterizado; entrega desconhecida.

import assert from 'node:assert/strict';

// Fonte ClickTrail Examples, commit 0c792bf386fb04971ee1b347a5cfbc4acaab748e.
function opportunityToConversion(opportunity = {}) {
  const won = String(opportunity.stage || opportunity.status || '').toLowerCase() === 'won';
  if (!won) return null;
  const fields = opportunity.customFields || opportunity.custom_fields || {};
  return { eventId: `twenty_${opportunity.id}_purchase`, eventName: 'Purchase', value: Number(opportunity.amount || 0), currency: String(opportunity.currency || 'USD').toUpperCase(), gclid: fields.gclid, gbraid: fields.gbraid, wbraid: fields.wbraid, customerId: opportunity.contactId || opportunity.contact_id };
}

const won = {
  id: 'demo-op-17', stage: 'won', amount: '8000.00', currency: 'BRL',
  contactId: 'demo-person-3', customFields: { gclid: 'synthetic-only' }
};
assert.equal(opportunityToConversion({ ...won, stage: 'negotiation' }), null);
const first = opportunityToConversion(won);
assert.equal(first.eventId, 'twenty_demo-op-17_purchase');
assert.equal(first.value, 8000);
assert.equal(first.currency, 'BRL');
assert.equal(first.customerId, 'demo-person-3');
assert.equal(first.gclid, 'synthetic-only');
assert.deepEqual(opportunityToConversion(won), first);
assert.equal(opportunityToConversion({ stage: 'won' }).eventId,
  'twenty_undefined_purchase');
assert(Number.isNaN(opportunityToConversion({ ...won, amount: 'inválido' }).value));

// Proposta local: validar o objeto antes de chamar o normalizador.
function validate(o) {
  assert.equal(o.id, 'demo-op-17');
  assert.equal(o.contactId, 'demo-person-3');
  assert.equal(o.currency, 'BRL');
  assert.equal(typeof o.amount, 'string');
  assert(/^\d+\.\d{2}$/.test(o.amount));
  assert(Number.isFinite(Number(o.amount)));
  return opportunityToConversion(o);
}
assert.equal(validate(won).value, 8000);
assert.throws(() => validate({ ...won, id: undefined }));
assert.throws(() => validate({ ...won, amount: 'inválido' }));
// ponytail: memória de uma execução; persistência transacional para concorrência/reinício.
const seen = new Set();
for (const o of [won, won]) seen.add(validate(o).eventId);
assert.equal(seen.size, 1);
console.log('B10: normalizador caracterizado; entrega desconhecida');

As comparações fixas de oportunidade e contato representam um vínculo sintético conhecido. Não são uma lista adequada para clientes reais. A validação monetária também é deliberadamente restrita: aceita a forma decimal usada no exercício, sem estabelecer uma biblioteca financeira ou um esquema completo de Twenty.

O que fazer com campos ausentes e valores inesperados?

Rejeite a entrada antes da transformação quando faltar um campo necessário ao resultado. O teste mostrou que a função original cria twenty_undefined_purchase sem ID e converte texto inválido em NaN. Esses resultados caracterizam o código examinado; não são eventos comerciais aceitáveis.

Uma falha de moeda merece atenção especial. O padrão USD pode ocultar que a origem não transportou a moeda. Em um negócio denominado em BRL, substituir ausência por dólares muda o significado financeiro. O guard ilustrativo exige BRL justamente para tornar essa ausência visível.

Para adaptar uma instância real, siga esta sequência:

  1. Capture um exemplo autorizado e sem dados pessoais desnecessários do objeto recebido.
  2. Identifique o caminho e a unidade de cada campo no esquema da instância.
  3. Defina quais ausências bloqueiam o processamento e quais são legítimas.
  4. Verifique oportunidade, pessoa e organização com registros confiáveis.
  5. Só então chame a transformação e compare a saída campo por campo.

O guia de transporte entre formulário e CRM ajuda quando o campo já chega ausente à oportunidade. Um valor perdido antes do CRM não será recuperado apenas porque o normalizador conhece seu nome.

Como impedir que uma repetição vire outro resultado?

A identidade determinística permite reconhecer repetição, mas não executa deduplicação persistente. No teste, duas chamadas produzem o mesmo ID e um Set termina com um elemento. Essa observação vale apenas para a memória daquele processo; reiniciar o programa apaga o conjunto.

Uma implementação durável precisa gravar a ocorrência com uma chave única que inclua o escopo da organização. A gravação do resultado e a decisão de enfileirar transporte devem respeitar a mesma fronteira transacional. Caso contrário, duas requisições simultâneas podem observar ausência e tentar processar o mesmo fato.

Também defina a política de reabertura. Se a oportunidade volta para negociação e ganha novamente, o ID atual do exemplo continua igual. Isso pode ser correto para uma visão de valor final por oportunidade, mas insuficiente para um histórico de ciclos comerciais. Escolha a unidade antes de acrescentar um contador ou timestamp ao identificador.

O exercício não simula concorrência, reinício ou banco de dados. Esses cenários permanecem na lista de aceitação da integração. Ter um identificador estável é uma condição útil; não é prova de processamento exatamente uma vez.

Como autenticação e transporte mudam o nível de evidência?

Autenticação prova a origem da mensagem; normalização prova uma transformação; persistência prova que o resultado ficou registrado. Cada etapa responde a uma pergunta diferente. Um teste com objeto JavaScript não valida automaticamente nenhuma requisição HTTP.

A documentação oficial de webhooks do Twenty, consultada em 15 de setembro de 2026, descreve notificações como opportunity.updated, corpo com event, data e timestamp, e verificação por assinatura HMAC SHA256. A integração deve conferir esse contrato contra a versão em uso. A assinatura não está implementada no exemplo executado.

No transporte publicitário, o nome do diretório twenty-crm-google-ads também não prova envio. Não há chamada ao Google na função. A documentação atual de conversões offline do Google exige configuração e diagnóstico próprios e apresenta restrições para novas integrações de upload, com orientação para Data Manager API nos casos afetados.

Por isso, não publique um comando de instalação inferido do nome do projeto nem marque o status como aceito. O resultado demonstrado aqui é um payload local. Seu destino ainda precisa ser escolhido, configurado e verificado.

Que evidência falta antes de conectar uma conta real?

Antes de ativar uma conta real, faça um ensaio controlado que preserve a distinção entre objeto válido, mensagem autêntica, resultado armazenado e entrega aceita. Use registros identificados como teste e um caminho que não dispare contato comercial ou conversões sintéticas em produção.

O checklist operacional deve incluir uma mensagem válida, uma assinatura inválida, um objeto de outra organização, um campo obrigatório ausente e uma repetição. Para cada caso, registre a expectativa, o resultado observado e a referência da evidência. Inclua a recusa da finalidade publicitária: um negócio válido não precisa se transformar em exportação autorizada.

Se usar ferramentas auxiliares, mantenha o mesmo rigor. O MCP local de diagnóstico ClickTrail ajuda a inspecionar dados, mas não substitui observação do CRM ou do provedor. No código consultado, ferramentas de envio são construtoras de payload e status externo permanece desconhecido.

O próximo passo concreto é mapear o objeto da sua instância para a entrada testada e registrar o estado anterior da oportunidade. Esse mapeamento, seguido de validação persistente, transforma um exemplo compreensível em uma implementação que pode ser avaliada sem promessas implícitas.

CategoriasUncategorized