/** * CDP Edge — Nurture Engine (Fase 7) * * Sequências de follow-up automáticas baseadas na qualificação do quiz: * comprador → contato imediato (já tratado pelo hot lead) * interessado → D+1, D+3, D+7 (WhatsApp ou email) * curioso → D+2, D+5 (conteúdo/isca) * perdido → exclusão do remarketing (cohort_label = excluded) * * Arquitetura: * 1. scheduleNurture() — chamado no QuizComplete, insere sequência no D1 * 2. runNurtureQueue() — chamado pelo Intelligence Agent (cron diário) * envia as mensagens com send_at <= now() */ import { Env, TrackPayload } from '../types.js'; // ── Tipos ───────────────────────────────────────────────────────────────────── export type NurtureQualification = 'comprador' | 'interessado' | 'curioso' | 'perdido'; export interface NurtureStep { delay_days: number; channel: 'whatsapp' | 'email'; message: string; // suporta {{name}}, {{quiz_name}}, {{qualification}} subject?: string; // apenas email } export interface NurtureResult { scheduled: number; skipped: string | null; } export interface NurtureRunResult { processed: number; sent: number; failed: number; excluded: number; } // ── Sequências por qualificação ─────────────────────────────────────────────── // Mensagens genéricas — o cliente personaliza via automation_rules no D1. // O Nurture Engine usa estas como fallback quando não há regra cadastrada. const NURTURE_SEQUENCES: Record = { comprador: [ // comprador já dispara hot lead imediato no /track — sem sequência adicional aqui ], interessado: [ { delay_days: 1, channel: 'whatsapp', message: 'Olá {{name}}! Vi que você completou nosso diagnóstico e ficou entre os mais qualificados. Posso te enviar mais detalhes sobre como podemos ajudar?', }, { delay_days: 3, channel: 'whatsapp', message: 'Oi {{name}}, tudo bem? Separei um conteúdo exclusivo baseado nas suas respostas no quiz "{{quiz_name}}". Posso compartilhar?', }, { delay_days: 7, channel: 'whatsapp', message: '{{name}}, última oportunidade esta semana! Muitos que fizeram o mesmo diagnóstico que você já estão obtendo resultados. Que tal conversarmos 15 minutos?', }, ], curioso: [ { delay_days: 2, channel: 'whatsapp', message: 'Olá {{name}}! Você completou nosso diagnóstico. Preparei um material gratuito baseado no seu perfil. Posso enviar?', }, { delay_days: 5, channel: 'whatsapp', message: 'Oi {{name}}! Vi que você está pesquisando sobre o assunto. Tenho uma aula gratuita que pode te ajudar muito. Interesse?', }, ], perdido: [ // perdido não recebe mensagens — apenas é excluído do remarketing ], }; // ── scheduleNurture — agenda sequência após QuizComplete ───────────────────── export async function scheduleNurture( env: Env, payload: TrackPayload, qualification: NurtureQualification, ): Promise { if (!env.DB) return { scheduled: 0, skipped: 'DB não disponível' }; // perdido → só atualiza cohort_label para excluir do remarketing if (qualification === 'perdido') { if (payload.userId) { try { await env.DB.prepare(` UPDATE user_profiles SET cohort_label = 'excluded', updated_at = datetime('now') WHERE user_id = ? `).bind(payload.userId).run(); } catch { /* não-crítico */ } } return { scheduled: 0, skipped: 'perdido — excluído do remarketing' }; } // comprador → contato imediato já gerenciado pelo hot lead em index.ts if (qualification === 'comprador') { return { scheduled: 0, skipped: 'comprador — contato imediato via hot lead' }; } const steps = NURTURE_SEQUENCES[qualification]; if (!steps || steps.length === 0) { return { scheduled: 0, skipped: `sem sequência para ${qualification}` }; } // Verifica se já tem sequência ativa para este usuário (evita duplicar) if (payload.userId) { try { const existing = await env.DB.prepare(` SELECT id FROM nurture_sequences WHERE user_id = ? AND status = 'pending' LIMIT 1 `).bind(payload.userId).first(); if (existing) return { scheduled: 0, skipped: 'sequência já existe para este usuário' }; } catch { /* continua */ } } let scheduled = 0; for (const step of steps) { try { const sendAt = new Date(); sendAt.setDate(sendAt.getDate() + step.delay_days); await env.DB.prepare(` INSERT INTO nurture_sequences ( user_id, qualification, delay_days, channel, message, subject, send_at, status, quiz_name, phone, email, first_name, created_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')) `).bind( payload.userId || null, qualification, step.delay_days, step.channel, step.message, step.subject || null, sendAt.toISOString().slice(0, 19).replace('T', ' '), 'pending', String((payload as any).quiz_name || ''), payload.phone || null, payload.email || null, payload.firstName || null, ).run(); scheduled++; } catch (err: any) { console.error('[Nurture] scheduleNurture insert error:', err?.message || String(err)); } } return { scheduled, skipped: null }; } // ── runNurtureQueue — processa mensagens pendentes (chamado pelo cron) ──────── export async function runNurtureQueue(env: Env): Promise { if (!env.DB) return { processed: 0, sent: 0, failed: 0, excluded: 0 }; const result: NurtureRunResult = { processed: 0, sent: 0, failed: 0, excluded: 0 }; try { // Busca até 50 mensagens pendentes com send_at <= agora const pending = await env.DB.prepare(` SELECT * FROM nurture_sequences WHERE status = 'pending' AND send_at <= datetime('now') ORDER BY send_at ASC LIMIT 50 `).all(); const rows = (pending.results || []) as any[]; if (rows.length === 0) return result; for (const row of rows) { result.processed++; // Interpola variáveis na mensagem const vars: Record = { name: String(row.first_name || 'você'), quiz_name: String(row.quiz_name || 'diagnóstico'), qualification: String(row.qualification || ''), }; const message = String(row.message || '').replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? ''); const subject = row.subject ? String(row.subject).replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '') : null; let success = false; try { if (row.channel === 'whatsapp' && row.phone && env.WHATSAPP_ACCESS_TOKEN && env.WHATSAPP_PHONE_NUMBER_ID) { const digits = String(row.phone).replace(/\D/g, ''); const e164 = digits.startsWith('55') ? `+${digits}` : `+55${digits}`; const res = await fetch( `https://graph.facebook.com/v25.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messaging_product: 'whatsapp', recipient_type: 'individual', to: e164, type: 'text', text: { body: message }, }), } ); success = res.ok; } else if (row.channel === 'email' && row.email && env.RESEND_API_KEY) { const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: env.RESEND_FROM_EMAIL || 'noreply@cdp-edge.app', to: [row.email], subject: subject || `Olá, ${vars.name}!`, html: `

${message.replace(/\n/g, '
')}

`, }), }); success = res.ok; } } catch (err: any) { console.error(`[Nurture] dispatch error (row ${row.id}):`, err?.message || String(err)); } // Atualiza status no D1 const newStatus = success ? 'sent' : 'failed'; const sentAt = success ? `datetime('now')` : 'NULL'; try { await env.DB.prepare(` UPDATE nurture_sequences SET status = ?, sent_at = ${success ? "datetime('now')" : 'NULL'}, updated_at = datetime('now') WHERE id = ? `).bind(newStatus, row.id).run(); } catch { /* não-crítico */ } if (success) result.sent++; else result.failed++; } } catch (err: any) { console.error('[Nurture] runNurtureQueue error:', err?.message || String(err)); } return result; }