import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; export async function POST(req: NextRequest) { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://supabase.4ventures.es'; // Prefer user JWT for tenant-scoped reads; service role only if explicitly set (server) const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim() || ''; const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const tenantId = user.app_metadata?.tenant_id || user.user_metadata?.tenant_id || null; const companyName = user.user_metadata?.company || 'Cliente'; if (!tenantId) { return NextResponse.json( { error: 'Usuario sin tenant_id en metadata. Re-provisionar cliente.' }, { status: 403 } ); } // 1. Extraer mensaje del usuario let userMessage = ''; try { const body = await req.json(); if (typeof body.message === 'string' && body.message.trim()) { userMessage = body.message.trim(); } else if (typeof body.prompt === 'string' && body.prompt.trim()) { userMessage = body.prompt.trim(); } else if (Array.isArray(body.messages) && body.messages.length > 0) { const userMsgs = body.messages.filter((m: any) => m.role === 'user'); if (userMsgs.length > 0) { userMessage = userMsgs[userMsgs.length - 1].content || ''; } } } catch (e) { console.error('Error parsing chat body:', e); } const normQuery = userMessage .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[¿?¡!.,;:_()]/g, ' ') .replace(/\s+/g, ' ') .trim(); // Use service role for RPC only after authn; still filter by tenant_id in every call const keyForRpc = serviceKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''; const headers = { apikey: keyForRpc, Authorization: `Bearer ${keyForRpc}`, 'Content-Type': 'application/json' }; // Helper para devolver respuesta uniforme const reply = (text: string) => { return NextResponse.json([{ text }]); }; // 3. Intención: Saludos, Capacidades y "¿Qué puedo preguntar?" const isHelpQuery = normQuery === '' || normQuery === 'hola' || normQuery === 'buenas' || normQuery === 'buenos dias' || normQuery === 'buenas tardes' || normQuery === 'hey' || normQuery.includes('que puedo preguntar') || normQuery.includes('que te puedo preguntar') || normQuery.includes('que preguntar') || normQuery.includes('que sabes') || normQuery.includes('que puedes hacer') || normQuery.includes('quien eres') || normQuery.includes('ayuda') || normQuery.includes('ejemplos') || normQuery.includes('comandos') || normQuery.includes('opciones') || normQuery.includes('como funciona el asistente') || normQuery.includes('instrucciones'); if (isHelpQuery) { return reply( `👋 **¡Hola! Soy tu Asistente Financiero & de Crecimiento de 4Ventures** para **${companyName}**.\n\n` + `Estoy conectado directamente a tu base de datos analítica y ejecuto cálculos matemáticos deterministas (temperatura 0.0, sin alucinaciones). Aquí tienes todo lo que puedes preguntarme:\n\n` + `1. **🔮 Simulaciones de Inversión ("¿Qué pasa si...?"):**\n` + ` - *«¿Qué pasa si invierto 1.000 € en Meta?»*\n` + ` - *«Simular 2.500 € en Google Ads»*\n` + ` - *«Proyección con 5.000 € de presupuesto»*\n` + ` *(Calcula retornos p10 conservador, p50 esperado y p90 optimista según tu histórico real).* \n\n` + `2. **🎯 Objetivos y Cumplimiento Mensual:**\n` + ` - *«¿Cómo van los objetivos de este mes?»*\n` + ` - *«¿Cuánto llevamos facturado vs la meta?»*\n` + ` - *«Progreso de captación de leads»*\n\n` + `3. **🔄 Embudo Comercial y Conversión:**\n` + ` - *«Muéstrame el embudo comercial»*\n` + ` - *«¿Cuál es nuestra tasa de cierre lead a venta?»*\n` + ` - *«Ticket medio de los clientes ganados»*\n\n` + `4. **📊 Métricas CAC y ROAS por Canal:**\n` + ` - *«¿Cuál es el CAC y ROAS de los últimos 30 días?»*\n` + ` - *«Rendimiento por canal publicitario»*\n\n` + `5. **🏆 Ranking de Campañas:**\n` + ` - *«¿Cuáles son las mejores campañas activas?»*\n` + ` - *«¿Dónde estamos invirtiendo más?»*\n\n` + `6. **📖 Metodología y Glosario 4Ventures:**\n` + ` - *«¿Qué diferencia hay entre CAC de cohorte y CAC de caja?»*\n` + ` - *«¿Cómo funciona la atribución universal a 90 días?»*\n\n` + `💡 *Puedes hacer clic en cualquiera de los botones rápidos de abajo o escribir directamente tu consulta.*` ); } // 4. Intención: Simulación Determinista "¿Qué pasa si...?" if ( normQuery.includes('simul') || normQuery.includes('invierto') || normQuery.includes('pasa si') || normQuery.includes('proyec') || normQuery.includes('presupuesto') ) { try { const matchNumber = query.match(/(\d+[\d\.,]*)/); let amount = 1000; if (matchNumber) { amount = parseFloat(matchNumber[1].replace(/\./g, '').replace(',', '.')); } let channel: string | null = null; if (normQuery.includes('meta') || normQuery.includes('facebook') || normQuery.includes('instagram')) channel = 'meta'; else if (normQuery.includes('google')) channel = 'google'; else if (normQuery.includes('tiktok')) channel = 'tiktok'; else if (normQuery.includes('linkedin')) channel = 'linkedin'; const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, { method: 'POST', headers, body: JSON.stringify({ p_tenant_id: tenantId, p_additional_spend: amount, p_target_channel: channel }) }); if (res.ok) { const sim = await res.json(); const p10 = sim.conservative_p10 || {}; const p50 = sim.expected_p50 || {}; const p90 = sim.optimistic_p90 || {}; const base = sim.baseline || {}; const channelLabel = (sim.target_channel || 'General').toUpperCase(); const text = `🔮 **Simulación de Inversión Adicional (+${amount.toLocaleString('es-ES')} € en ${channelLabel}):**\n\n` + `📈 **Métricas Base Observadas:**\n` + `- **CPL Histórico:** ${Number(base.historical_cpl || 0).toFixed(2)} €\n` + `- **Tasa de Cierre:** ${Number(base.conversion_rate_pct || 0).toFixed(1)}%\n` + `- **Ticket Medio:** ${Number(base.avg_ticket || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €\n\n` + `📊 **Proyección por Escenarios Matemáticos:**\n` + `- **Escenario Esperado (p50):** +${p50.leads} leads | +${p50.deals} clientes | **+${Number(p50.revenue || 0).toLocaleString('es-ES')} €** facturación (ROAS **${p50.roas}x**)\n` + `- **Escenario Conservador (p10):** +${p10.leads} leads | +${p10.deals} clientes | **+${Number(p10.revenue || 0).toLocaleString('es-ES')} €** facturación (ROAS **${p10.roas}x**)\n` + `- **Escenario Optimista (p90):** +${p90.leads} leads | +${p90.deals} clientes | **+${Number(p90.revenue || 0).toLocaleString('es-ES')} €** facturación (ROAS **${p90.roas}x**)\n\n` + `💡 *Cálculo determinista con la función RPC simulate_budget basada en la atribución de cohorte a 90 días.*`; return reply(text); } } catch (e) { console.error('Error in simulate_budget chat:', e); } } // 5. Intención: Objetivos vs Real / Estrategia if ( normQuery.includes('objetivo') || normQuery.includes('meta') || normQuery.includes('factura') || normQuery.includes('target') || normQuery.includes('cumplimiento') ) { try { const now = new Date(); const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`; const res = await fetch(`${supabaseUrl}/rest/v1/rpc/targets_vs_actual`, { method: 'POST', headers, body: JSON.stringify({ p_month: currentMonth }) }); if (res.ok) { const data = await res.json(); const t = data[0] || {}; const revAct = Number(t.revenue_actual || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 }); const revTar = Number(t.revenue_target || 0).toLocaleString('es-ES'); const pct = t.revenue_achievement_pct || 0; const text = `🎯 **Cumplimiento de Objetivos del Mes (${currentMonth}):**\n\n` + `- **Facturación:** **${revAct} €** logrados de una meta de **${revTar} €** (${pct}% completado).\n` + `- **Leads Captados:** **${t.leads_actual || 0}** / ${t.leads_target || 0} objetivo.\n` + `- **Clientes Ganados:** **${t.clients_actual || 0}** / ${t.clients_target || 0} objetivo.\n\n` + `📌 **Diagnóstico:** ${pct >= 100 ? '🎉 ¡Objetivo de facturación alcanzado!' : pct >= 50 ? '⚡ Buen ritmo de avance en el periodo.' : '⚠️ Requiere acelerar el volumen de leads calificados o mejorar tasa de contacto.'}\n\n` + `*Fuente: RPC targets_vs_actual sincronizado con deals ganados en CRM.*`; return reply(text); } } catch (e) { console.error('Error in targets_vs_actual:', e); } } // 6. Intención: Embudo / Funnel / Conversión if ( normQuery.includes('funnel') || normQuery.includes('embudo') || normQuery.includes('tasa') || normQuery.includes('ticket') || normQuery.includes('conversion') || normQuery.includes('cierre') ) { try { const res = await fetch(`${supabaseUrl}/rest/v1/rpc/lead_funnel`, { method: 'POST', headers, body: JSON.stringify({ p_tenant_id: tenantId, p_days: 30 }) }); if (res.ok) { const f = await res.json(); const text = `🔄 **Embudo de Conversión Comercial (Últimos 30 días):**\n\n` + `1. **Leads Inbound Captados:** ${f.total_leads || 0}\n` + `2. **Leads Contactados:** ${f.contacted_leads || 0}\n` + `3. **Oportunidades en Pipeline:** ${f.deals_in_pipeline || 0}\n` + `4. **Ventas Cerradas Ganadas:** **${f.deals_won || 0}**\n\n` + `💰 **Resultados Financieros:**\n` + `- **Facturación Cerrada:** **${Number(f.revenue_won || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €**\n` + `- **Ticket Medio:** **${Number(f.avg_ticket || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €**\n` + `- **Tasa de Cierre Lead ➔ Venta:** **${f.conversion_lead_to_deal_pct || 0}%**\n\n` + `*Fuente: Webhook deal-won n8n y tabla deals en PostgreSQL.*`; return reply(text); } } catch (e) { console.error('Error in lead_funnel:', e); } } // 7. Intención: Ranking de Campañas / Anuncios if ( normQuery.includes('campa') || normQuery.includes('anuncio') || normQuery.includes('ranking') || normQuery.includes('mejor') || normQuery.includes('ads') ) { try { const res = await fetch(`${supabaseUrl}/rest/v1/rpc/rank_campaigns`, { method: 'POST', headers, body: JSON.stringify({ p_tenant_id: tenantId, p_metric: 'spend', p_limit: 5 }) }); if (res.ok) { const campaigns = await res.json(); if (Array.isArray(campaigns) && campaigns.length > 0) { const list = campaigns.map((c: any, i: number) => `${i + 1}. **${c.campaign_name}** (${c.platform.toUpperCase()}):\n - Inversión: **${Number(c.spend || 0).toFixed(2)} €**\n - Clics: ${c.clicks || 0} (CTR: ${c.impressions > 0 ? ((c.clicks / c.impressions) * 100).toFixed(2) : 0}%)\n - Leads Captados: **${c.leads || 0}**` ).join('\n\n'); return reply(`🏆 **Top Campañas por Inversión Publicitaria:**\n\n${list}\n\n*Fuente: Datos diarios procesados vía Windsor.ai.*`); } else { return reply('📢 Actualmente no constan campañas activas con inversión registrada en los últimos 30 días para este portal.'); } } } catch (e) { console.error('Error in rank_campaigns:', e); } } // 8. Intención: RAG Documental Híbrido (Biblioteca de Activos) if ( normQuery.includes('document') || normQuery.includes('copy') || normQuery.includes('informe') || normQuery.includes('brief') || normQuery.includes('activo') || normQuery.includes('creativo') ) { try { const res = await fetch(`${supabaseUrl}/rest/v1/rpc/match_documents`, { method: 'POST', headers, body: JSON.stringify({ p_tenant_id: tenantId, p_query_text: userMessage, p_match_count: 3 }) }); if (res.ok) { const matches = await res.json(); if (Array.isArray(matches) && matches.length > 0) { const excerpts = matches.map((m: any) => `📄 **${m.document_title}** (${m.document_type.toUpperCase()}):\n> ${m.chunk_text.replace(/\n/g, '\n> ')}` ).join('\n\n'); return reply(`📚 **Fragmentos Recuperados de la Biblioteca de Activos (RAG Híbrido):**\n\n${excerpts}\n\n*Búsqueda híbrida vectorial (pgvector) y BM25 en document_chunks.*`); } } // Si no hay chunks en RAG, buscar documentos aprobados directamente const docRes = await fetch(`${supabaseUrl}/rest/v1/documents?tenant_id=eq.${tenantId}&status=eq.approved&select=id,title,type,content`, { headers }); if (docRes.ok) { const docs = await docRes.json(); if (Array.isArray(docs) && docs.length > 0) { const docList = docs.map((d: any) => `- **${d.title}** (${d.type.toUpperCase()})`).join('\n'); return reply(`📁 **Documentos Aprobados Disponibles en tu Biblioteca:**\n\n${docList}\n\nPuedes consultarlos en detalle en la pestaña **Biblioteca de Activos**.`); } } } catch (e) { console.error('Error in match_documents chat:', e); } } // 9. Intención: Metodología y Glosario 4Ventures if ( normQuery.includes('que es cac') || normQuery.includes('cohorte') || normQuery.includes('caja') || normQuery.includes('atribucion') || normQuery.includes('90 dias') || normQuery.includes('metodologia') ) { return reply( `📖 **Metodología Oficial de Crecimiento — 4Ventures:**\n\n` + `1. **CAC de Cohorte (Cohort CAC):**\n` + ` Inversión en Ads del periodo dividida entre los clientes ganados que *hicieron clic por primera vez en ese mismo periodo*, sin importar si tardaron 15 o 60 días en comprar. Es la única métrica real para saber si tus campañas son rentables.\n\n` + `2. **CAC de Caja (Cash CAC):**\n` + ` Inversión total del mes dividida entre los clientes cerrados ese mismo mes. Sirve para controlar el flujo de tesorería mensual.\n\n` + `3. **Atribución Universal a 90 Días (` + '`_4v_attr.js`' + `):**\n` + ` Captura los Click IDs (` + '`fbclid`' + `, ` + '`gclid`' + `) y UTMs en el primer contacto del visitante y los mantiene 90 días en el navegador, inyectándolos en formularios y enlaces de WhatsApp.\n\n` + `4. **Cierre de Bucle CRM (` + '`deal-won`' + `):**\n` + ` Cuando tu equipo de ventas marca un cliente como "Ganado", n8n recupera el origen original y recalcula el ROAS al instante.` ); } // 10. Consulta de KPIs Semanales / CAC & ROAS (Default Analítico) try { const res = await fetch(`${supabaseUrl}/rest/v1/kpi_weekly?tenant_id=eq.${tenantId}&select=*`, { headers }); if (res.ok) { const kpis = await res.json(); if (Array.isArray(kpis) && kpis.length > 0) { const channelsSummary = kpis.map((k: any) => `- Canal **${(k.channel || 'Directo').toUpperCase()}**:\n` + ` - Inversión: **${Number(k.total_spend || 0).toFixed(2)} €**\n` + ` - Leads Captados: **${k.total_leads || 0}**\n` + ` - Clientes Ganados: **${k.cohort_deals_won || 0}**\n` + ` - Facturación Cohorte: **${Number(k.cohort_revenue_won || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €**\n` + ` - CAC Cohorte: **${Number(k.cac_cohort || 0).toFixed(2)} €**\n` + ` - ROAS Cohorte: **${Number(k.roas_cohort || 0).toFixed(2)}x**` ).join('\n\n'); return reply( `📊 **Rendimiento Analítico de ${companyName} (Corte Horario):**\n\n` + `${channelsSummary}\n\n` + `💡 *Puedes consultarme simulaciones ("¿Qué pasa si invierto 1000€ en Meta?"), metas de facturación o el embudo de ventas.*` ); } } } catch (e) { console.error('Error fetching KPIs for assistant:', e); } return reply( `📊 **Centro de Analítica — ${companyName}:**\n\n` + `No he detectado una consulta numérica específica sobre tu pregunta, pero puedo proporcionarte métricas en tiempo real:\n\n` + `- Escribe *«Objetivos»* para ver las metas de facturación vs ventas reales.\n` + `- Escribe *«Simular 1000€ en Meta»* para proyectar nuevos clientes con intervalos p10/p50/p90.\n` + `- Escribe *«Embudo»* para ver la tasa de conversión y ticket medio.\n` + `- Escribe *«Campañas»* para ver los anuncios con mayor inversión.` ); }