# Prompt para Replit — HubSpot Custom Workflow Action (OnSMS) --- ## CONTEXTO DO SISTEMA ATUAL — leia antes de tocar em qualquer arquivo Este sistema é uma plataforma de mensageria (OnSMS) que dispara mensagens WhatsApp via Infobip para contatos gerenciados no HubSpot CRM. Os clientes autenticam via OAuth do HubSpot (botão "Conectar ao HubSpot" na plataforma → tela de autorização HubSpot → callback → token salvo). **O que JÁ EXISTE e funciona — NÃO altere:** - OAuth flow completo: `/api/hubspot/oauth/install` e `/api/hubspot/oauth/callback` - Tabela `hubspot_connections` com `portalId`, `accessToken`, `refreshToken`, `tokenExpiresAt` - CRM Cards (histórico de SMS) já registrados e funcionando no app HubSpot - Webhook legado: `POST /webhooks/hubspot` — recebe `{ portalId, phone, onsms_template, var_1, var_2 }` - Sync de templates: job que faz PATCH na Properties API do HubSpot a cada 60min - Pipeline de envio: valida sender Infobip → envia → grava em `waba_messages` - App HubSpot já registrado no Developer Portal (tem `client_id` e `client_secret` como secrets) **O que este plano adiciona — sem quebrar nada acima:** Custom Workflow Action nativa no HubSpot. O cliente, no editor de Workflow do HubSpot, verá uma ação chamada "Enviar WhatsApp (OnSMS)" com dropdown de templates e mapeamento visual de variáveis — sem nenhum JSON manual. --- ## TAREFA 1 — Adicionar escopo `automation` ao OAuth **Arquivo:** onde a URL de autorização OAuth é montada (provavelmente `server/routes/hubspot.ts` ou similar — localize pelo endpoint `/api/hubspot/oauth/install`) **O que fazer:** adicionar `automation` ao parâmetro `scope` da URL de redirect. ```typescript // ANTES (exemplo): const scope = 'crm.objects.contacts.read'; // DEPOIS: const scope = 'crm.objects.contacts.read automation'; // Separar por espaço. O HubSpot aceita espaço ou %20 na URL. ``` **Por que:** sem `automation` declarado aqui, o HubSpot recusa o registro da Custom Action mesmo que o app esteja configurado no Developer Portal. **Aviso para clientes existentes:** clientes que já conectaram não têm esse escopo no token deles. Eles precisarão reconectar. A UI (Tarefa 4) vai exibir um aviso para isso. --- ## TAREFA 2 — Endpoint de definição da Custom Action **Criar:** `GET /api/hubspot/workflow-action/definition` Este endpoint é chamado pelo HubSpot quando o admin do app registra a action, e também durante o carregamento do editor de Workflow para montar os campos. **Lógica:** 1. Extrair `portalId` da query string (HubSpot envia `?portalId=XXXXX`) 2. Buscar conexão ativa em `hubspot_connections` para esse `portalId` 3. Buscar templates aprovados (não-MARKETING ou conforme regra de negócio) para esse portal 4. Retornar o schema da action no formato exato que o HubSpot espera ```typescript // server/routes/hubspot-workflow-action.ts router.get('/api/hubspot/workflow-action/definition', async (req, res) => { const { portalId } = req.query; // Validação básica if (!portalId) { return res.status(400).json({ error: 'portalId obrigatório' }); } // Buscar templates aprovados deste portal // (reutilize a mesma função que o sync usa — nunca misture templates entre portais) const templates = await getApprovedTemplatesForPortal(String(portalId)); // Montar as opções do dropdown de template const templateOptions = templates.map(t => ({ label: `${t.name} | ${t.category} | ${t.language}`, value: String(t.id), })); // Descobrir o número máximo de variáveis entre os templates disponíveis // para gerar os campos var_1, var_2, var_3 dinamicamente const maxVars = templates.reduce((max, t) => Math.max(max, t.variableCount ?? 0), 0); // Gerar campos de variável (var_1 até var_N) const varFields = Array.from({ length: maxVars }, (_, i) => ({ typeDefinition: { name: `var_${i + 1}`, type: 'string', fieldType: 'text', // Permite que o cliente mapeie a propriedade do contato (ex: {{contact.firstname}}) }, supportedValueTypes: ['STATIC_VALUE', 'OBJECT_PROPERTY'], isRequired: false, })); // Schema completo da action — formato exato da API do HubSpot // Ref: https://developers.hubspot.com/docs/api/automation/custom-workflow-actions const definition = { // URL que o HubSpot vai chamar quando o workflow executar actionUrl: `${process.env.APP_URL}/api/hubspot/workflow-action/execute`, published: true, // false enquanto estiver em desenvolvimento/testes inputFields: [ { typeDefinition: { name: 'template_id', type: 'enumeration', fieldType: 'select', options: templateOptions, // dropdown populado dinamicamente por portalId }, supportedValueTypes: ['STATIC_VALUE'], isRequired: true, }, // Campo opcional para botão de URL dinâmico (templates com botão) { typeDefinition: { name: 'btn_url_suffix', type: 'string', fieldType: 'text', }, supportedValueTypes: ['STATIC_VALUE', 'OBJECT_PROPERTY'], isRequired: false, }, // Campos de variável gerados dinamicamente ...varFields, ], // Campos que a action vai devolver ao workflow (pode ficar vazio) outputFields: [], // Labels exibidos no editor de Workflow labels: { pt: { actionName: 'Enviar WhatsApp (OnSMS)', actionDescription: 'Envia uma mensagem WhatsApp usando um template aprovado da OnSMS.', inputFieldLabels: { template_id: 'Template', btn_url_suffix: 'Sufixo do botão URL (opcional)', // var_1, var_2... serão labelizados como "Variável 1", "Variável 2" ...Object.fromEntries( Array.from({ length: maxVars }, (_, i) => [`var_${i + 1}`, `Variável ${i + 1}`]) ), }, }, en: { actionName: 'Send WhatsApp (OnSMS)', actionDescription: 'Sends a WhatsApp message using an approved OnSMS template.', inputFieldLabels: { template_id: 'Template', btn_url_suffix: 'URL button suffix (optional)', ...Object.fromEntries( Array.from({ length: maxVars }, (_, i) => [`var_${i + 1}`, `Variable ${i + 1}`]) ), }, }, }, }; return res.json(definition); }); ``` **Importante:** o HubSpot pode chamar este endpoint sem `portalId` durante o registro inicial. Nesse caso, retorne uma definição genérica com `options: []` e `maxVars` baseado em um padrão (ex: 3 variáveis). O dropdown ficará vazio e se populará quando o cliente autenticado abrir o editor. --- ## TAREFA 3 — Endpoint de execução da Custom Action **Criar:** `POST /api/hubspot/workflow-action/execute` Este endpoint é chamado pelo HubSpot toda vez que o workflow executa para um contato. É o coração da integração — deve reutilizar exatamente o mesmo pipeline de envio que o webhook legado `/webhooks/hubspot` já usa. **Payload que o HubSpot envia (não altere a estrutura esperada):** ```json { "callbackId": "ap-abc123", "origin": { "portalId": 12345, "actionDefinitionId": 99, "actionDefinitionVersion": 1 }, "context": { "workflowId": 555, "actionExecutionIndex": 0 }, "object": { "objectId": 67890, "objectType": "CONTACT" }, "fields": { "template_id": "tpl_123", "var_1": "João", "var_2": "15/01/2025", "btn_url_suffix": "/pedido/999" } } ``` ```typescript // server/routes/hubspot-workflow-action.ts (mesmo arquivo) router.post('/api/hubspot/workflow-action/execute', async (req, res) => { const { callbackId, origin, object, fields } = req.body; const portalId = String(origin?.portalId); const contactId = String(object?.objectId); // Validações obrigatórias if (!portalId || !contactId || !fields?.template_id) { return res.status(400).json({ error: 'Payload inválido' }); } try { // 1. Buscar conexão ativa para este portal const connection = await getHubspotConnection(portalId); if (!connection) { // Responde 200 ao HubSpot (ele não deve retentar por erro de configuração) // mas loga o problema internamente console.error(`[WorkflowAction] Conexão não encontrada para portalId ${portalId}`); return res.json({ outputFields: { status: 'error', reason: 'connection_not_found' } }); } // 2. Buscar o número de telefone do contato via HubSpot CRM API // (o HubSpot não envia o phone no payload — precisamos buscar) const phone = await getContactPhone(contactId, connection.accessToken); if (!phone) { return res.json({ outputFields: { status: 'error', reason: 'phone_not_found' } }); } // 3. Montar o objeto de envio no mesmo formato que o webhook legado usa // Reutilize a função existente de envio — NÃO duplique a lógica const sendPayload = { portalId, phone, onsms_template: fields.template_id, var_1: fields.var_1 ?? '', var_2: fields.var_2 ?? '', var_3: fields.var_3 ?? '', btn_url_suffix: fields.btn_url_suffix ?? '', }; // 4. Executar o pipeline de envio (mesmo que o webhook legado) // Esta função já: valida sender Infobip, faz fallback, grava em waba_messages const result = await sendWhatsAppMessage(sendPayload); // 5. Responder ao HubSpot no formato correto // outputFields pode carregar dados para steps seguintes do workflow return res.json({ outputFields: { status: result.success ? 'sent' : 'failed', messageId: result.messageId ?? '', }, }); } catch (error) { console.error('[WorkflowAction] Erro na execução:', error); // Retornar 500 faz o HubSpot retentar — use com cuidado // Para erros de configuração, prefira 200 com status de erro no outputFields return res.status(500).json({ error: 'Erro interno' }); } }); ``` **Sobre retentativas do HubSpot:** se você retornar HTTP 5xx, o HubSpot vai retentar a execução. Para erros de negócio (template inválido, telefone não encontrado), retorne sempre 200 com o `status: 'error'` no `outputFields` para evitar spam de retentativa. --- ## TAREFA 4 — Registrar a Custom Action no HubSpot (uma vez) Após os endpoints estarem no ar, a action precisa ser registrada no app. Isso é feito via API do HubSpot e só precisa acontecer uma vez por ambiente (staging/prod). **Criar um script de setup** ou um endpoint de admin para isso: ```typescript // scripts/register-hubspot-action.ts (rodar manualmente) async function registerCustomAction() { const appId = process.env.HUBSPOT_APP_ID; // ID numérico do app no Developer Portal const developerApiKey = process.env.HUBSPOT_DEVELOPER_API_KEY; const response = await fetch( `https://api.hubapi.com/automation/v4/actions/apps/${appId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', // Atenção: registro de action usa Developer API Key, NÃO o token do cliente 'Authorization': `Bearer ${developerApiKey}`, }, body: JSON.stringify({ // aponta para o endpoint de definição que criamos actionUrl: `${process.env.APP_URL}/api/hubspot/workflow-action/definition`, published: false, // mude para true quando estiver pronto para produção }), } ); const data = await response.json(); console.log('Action registrada:', data); // Salve o `id` retornado — é o actionDefinitionId } ``` **Nota:** o `developerApiKey` é diferente dos tokens de cliente. É encontrado no HubSpot Developer Portal em "Apps" > seu app > "Auth". Adicione como secret `HUBSPOT_DEVELOPER_API_KEY` e `HUBSPOT_APP_ID`. --- ## TAREFA 5 — Buscar telefone do contato via API O payload do HubSpot não envia o telefone — só o `objectId` do contato. Precisamos buscá-lo: ```typescript async function getContactPhone(contactId: string, accessToken: string): Promise { const response = await fetch( `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}?properties=phone,mobilephone`, { headers: { Authorization: `Bearer ${accessToken}` }, } ); if (!response.ok) return null; const data = await response.json(); // Preferir celular, fallback para phone fixo return data.properties?.mobilephone || data.properties?.phone || null; } ``` --- ## TAREFA 6 — Atualizar UI da página `/admin/hubspot` **Arquivo:** `client/src/pages/hubspot-admin.tsx` **O que alterar:** ``` ANTES: - Botão principal: "Gerar payload" (abre modal com JSON para copiar) - Formulário de "Private App Access Token" em destaque DEPOIS: - Seção de destaque: instrução sobre a ação nativa "Adicione a ação 'Enviar WhatsApp (OnSMS)' diretamente no editor de Workflow do HubSpot. Não é necessário configurar webhooks manualmente." - Badge de aviso (amarelo) se o token do cliente não tiver o escopo 'automation': "Reconecte sua conta HubSpot para ativar a ação nativa de workflow." [Botão: Reconectar] - Seção "Modo avançado / Legado" (colapsável, fechada por padrão): - Gerador de payload JSON (o que existia antes) — com badge "Legado" - Campo de Private App Token manual — com badge "Legado" - Documentação inline explicando o novo fluxo (sem sair da página) ``` **Como detectar se o cliente tem o escopo `automation`:** Ao carregar a página, fazer uma chamada leve à API do HubSpot: ```typescript // Verificar escopos do token atual const scopeCheck = await fetch( `https://api.hubapi.com/oauth/v1/access-tokens/${connection.accessToken}` ); const tokenInfo = await scopeCheck.json(); const hasAutomation = tokenInfo.scopes?.includes('automation'); // Se false → mostrar banner de reconexão ``` --- ## TAREFA 7 — Atualizar `replit.md` Adicionar seção documentando: - Variáveis de ambiente novas: `HUBSPOT_APP_ID`, `HUBSPOT_DEVELOPER_API_KEY` - Endpoints novos: `GET /api/hubspot/workflow-action/definition`, `POST /api/hubspot/workflow-action/execute` - Como registrar a action (rodar o script de setup) - Fluxo completo: OAuth com escopo `automation` → registro da action → cliente usa no editor de Workflow --- ## REGRAS DE OURO — não quebre isso 1. **`/webhooks/hubspot` não pode ser tocado.** Clientes legados ainda usam esse endpoint. Todo o novo fluxo é ADICIONAL, nunca substitutivo. 2. **Nunca misture dados entre portais.** Cada chamada ao `/definition` deve filtrar templates pelo `portalId` da query. Nunca retorne templates de outro portal. 3. **Reutilize o pipeline de envio existente.** O endpoint `/execute` deve chamar a mesma função que o webhook usa — não copie e cole a lógica de envio. 4. **Tokens se renovam antes do uso.** Se `tokenExpiresAt` estiver a menos de 5 minutos, renove o `accessToken` via `refreshToken` antes de qualquer chamada à API do HubSpot. Isso já deve existir no sistema — confirme que o `/execute` passa por esse check. 5. **Responda 200 para erros de negócio.** HTTP 5xx faz o HubSpot retentar indefinidamente. Erros de telefone não encontrado, template inválido, etc. → 200 com `status: 'error'` no `outputFields`. --- ## DOCUMENTAÇÃO DE REFERÊNCIA - Custom Workflow Actions: https://developers.hubspot.com/docs/api/automation/custom-workflow-actions - Properties API: https://developers.hubspot.com/docs/api/crm/properties - CRM Contacts API (buscar telefone): https://developers.hubspot.com/docs/api/crm/contacts - Token introspection (verificar escopos): `GET https://api.hubapi.com/oauth/v1/access-tokens/{token}` --- ## ORDEM DE EXECUÇÃO SUGERIDA ``` 1. Adicionar escopo `automation` na URL OAuth (Tarefa 1) — 5 min 2. Criar /api/hubspot/workflow-action/definition (Tarefa 2) — 30 min 3. Criar /api/hubspot/workflow-action/execute (Tarefa 3) — 30 min 4. Criar getContactPhone() (Tarefa 5) — 15 min 5. Criar script de registro da action + rodar em staging (Tarefa 4) — 20 min 6. Testar no HubSpot Sandbox com um workflow real 7. Atualizar UI /admin/hubspot (Tarefa 6) — 45 min 8. Atualizar replit.md (Tarefa 7) — 10 min ``` Faça cada tarefa em sequência. Não avance para a UI (Tarefa 6) antes dos endpoints estarem testados no HubSpot Sandbox — a UI depende dos endpoints estarem no ar.