/** * BI Executive Dashboard Queries * Comprehensive analytics: KPIs, revenue P&L, strategic cohort analysis */ const { pool, fmtDate, fmtTrendRows, calcPrevPeriod } = require('./helpers'); // BI Analytics - Comprehensive data for admin BI dashboard async function fetchBIData(dataInicio, dataFim, getAgenteName = null) { const conn = await pool.getConnection(); try { const { prevStartStr, prevEndStr } = calcPrevPeriod(dataInicio, dataFim); // Run all 12 queries in parallel const [ [kpiBrlUsd], [kpiUsdBrl], [kpiUsdUsd], [uniqueClients], [prevBrlUsd], [prevUsdBrl], [prevUsdUsd], [trendBrlUsd], [trendUsdBrl], [trendUsdUsd], [topClients], [retention], [clientsAtRisk], [agentRanking] ] = await Promise.all([ // 1. BRL→USD KPIs conn.execute(` SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(amount_usd), 0), 2) as vol_usd, ROUND(COALESCE(SUM(amount_brl), 0), 2) as vol_brl, ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd), 0), 2) as spread_revenue, ROUND(COALESCE(AVG((exchange_rate - ptax) / exchange_rate * 100), 0), 2) as avg_spread_pct, COUNT(DISTINCT id_conta) as clientes FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? `, [dataInicio, dataFim]), // 2. USD→BRL KPIs conn.execute(` SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor), 0), 2) as vol_usd, ROUND(COALESCE(SUM(valor_sol), 0), 2) as vol_brl, ROUND(COALESCE(SUM((ptax - cotacao) / ptax * valor), 0), 2) as spread_revenue, ROUND(COALESCE(AVG(CASE WHEN cotacao > 0 THEN (ptax - cotacao) / ptax * 100 ELSE 0 END), 0), 2) as avg_spread_pct, COUNT(DISTINCT id_conta) as clientes FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') `, [dataInicio, dataFim]), // 3. USD→USD KPIs conn.execute(` SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor), 0), 2) as vol_usd, COUNT(DISTINCT id_conta) as clientes FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND (cotacao IS NULL OR cotacao = 0 OR pgto = 'balance') `, [dataInicio, dataFim]), // 4. Unique active clients conn.execute(` SELECT COUNT(DISTINCT id_conta) as total FROM ( SELECT id_conta FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? UNION SELECT id_conta FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? ) all_clients `, [dataInicio, dataFim, dataInicio, dataFim]), // 5. Previous period conn.execute(` SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(amount_usd),0),2) as vol_usd, ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? `, [prevStartStr, prevEndStr]), conn.execute(` SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd, ROUND(COALESCE(SUM((ptax - cotacao) / ptax * valor),0),2) as spread_revenue FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') `, [prevStartStr, prevEndStr]), conn.execute(` SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND (cotacao IS NULL OR cotacao = 0 OR pgto = 'balance') `, [prevStartStr, prevEndStr]), // 6-8. Trends conn.execute(` SELECT DATE(created_at) as dia, COUNT(*) as qtd, ROUND(SUM(amount_usd), 2) as vol_usd, ROUND(AVG((exchange_rate - ptax) / exchange_rate * 100), 2) as avg_spread FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY DATE(created_at) ORDER BY dia `, [dataInicio, dataFim]), conn.execute(` SELECT DATE(created_at) as dia, COUNT(*) as qtd, ROUND(SUM(valor), 2) as vol_usd, ROUND(AVG(CASE WHEN cotacao > 0 THEN (ptax - cotacao) / ptax * 100 ELSE 0 END), 2) as avg_spread FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY DATE(created_at) ORDER BY dia `, [dataInicio, dataFim]), conn.execute(` SELECT DATE(created_at) as dia, COUNT(*) as qtd, ROUND(SUM(valor), 2) as vol_usd FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND (cotacao IS NULL OR cotacao = 0 OR pgto = 'balance') GROUP BY DATE(created_at) ORDER BY dia `, [dataInicio, dataFim]), // 9. Top clients conn.execute(` SELECT nome, SUM(vol) as total_usd, SUM(qtd) as total_qtd FROM ( SELECT c.nome, SUM(t.amount_usd) as vol, COUNT(*) as qtd FROM br_transaction_to_usa t INNER JOIN conta c ON c.id_conta = t.id_conta WHERE DATE(t.created_at) >= ? AND DATE(t.created_at) <= ? GROUP BY c.nome UNION ALL SELECT c.nome, SUM(p.valor) as vol, COUNT(*) as qtd FROM pagamento_br p INNER JOIN conta c ON c.id_conta = p.id_conta WHERE DATE(p.created_at) >= ? AND DATE(p.created_at) <= ? GROUP BY c.nome ) combined GROUP BY nome ORDER BY total_usd DESC LIMIT 10 `, [dataInicio, dataFim, dataInicio, dataFim]), // 10. Retention conn.execute(` SELECT COUNT(DISTINCT prev.id_conta) as prev_clients, COUNT(DISTINCT CASE WHEN curr.id_conta IS NOT NULL THEN prev.id_conta END) as retained FROM ( SELECT DISTINCT id_conta FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? UNION SELECT DISTINCT id_conta FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? ) prev LEFT JOIN ( SELECT DISTINCT id_conta FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? UNION SELECT DISTINCT id_conta FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? ) curr ON prev.id_conta = curr.id_conta `, [prevStartStr, prevEndStr, prevStartStr, prevEndStr, dataInicio, dataFim, dataInicio, dataFim]), // 11. Clients at risk conn.execute(` SELECT nome, MAX(last_op) as last_op, SUM(vol) as total_usd, SUM(qtd) as total_qtd, DATEDIFF(CURDATE(), MAX(last_op)) as days_inactive FROM ( SELECT c.nome, MAX(t.created_at) as last_op, SUM(t.amount_usd) as vol, COUNT(*) as qtd FROM br_transaction_to_usa t INNER JOIN conta c ON c.id_conta = t.id_conta GROUP BY c.nome UNION ALL SELECT c.nome, MAX(p.created_at) as last_op, SUM(p.valor) as vol, COUNT(*) as qtd FROM pagamento_br p INNER JOIN conta c ON c.id_conta = p.id_conta GROUP BY c.nome ) combined GROUP BY nome HAVING MAX(last_op) < CURDATE() ORDER BY total_usd DESC LIMIT 20 `), // 12. Agent ranking conn.execute(` SELECT agente_id, SUM(vol) as total_usd, SUM(qtd) as total_qtd, ROUND(SUM(spread_rev), 2) as total_spread, COUNT(DISTINCT client_id) as clientes FROM ( SELECT ac.agente_id, t.id_conta as client_id, SUM(t.amount_usd) as vol, COUNT(*) as qtd, SUM((t.exchange_rate - t.ptax) / t.exchange_rate * t.amount_usd) as spread_rev FROM br_transaction_to_usa t INNER JOIN ag_contas ac ON ac.conta_id = t.id_conta WHERE DATE(t.created_at) >= ? AND DATE(t.created_at) <= ? GROUP BY ac.agente_id, t.id_conta UNION ALL SELECT ac.agente_id, p.id_conta as client_id, SUM(p.valor) as vol, COUNT(*) as qtd, SUM((p.ptax - p.cotacao) / p.ptax * p.valor) as spread_rev FROM pagamento_br p INNER JOIN ag_contas ac ON ac.conta_id = p.id_conta WHERE DATE(p.created_at) >= ? AND DATE(p.created_at) <= ? AND p.cotacao IS NOT NULL AND p.cotacao > 0 AND (p.pgto IS NULL OR p.pgto != 'balance') GROUP BY ac.agente_id, p.id_conta ) combined GROUP BY agente_id ORDER BY total_usd DESC LIMIT 10 `, [dataInicio, dataFim, dataInicio, dataFim]) ]); // Resolve agent names const agents = agentRanking.map((r, i) => { const nome = getAgenteName ? (getAgenteName(r.agente_id) || `Agente ${r.agente_id}`) : `Agente ${r.agente_id}`; return { rank: i + 1, agente_id: r.agente_id, nome, vol_usd: Number(r.total_usd), qtd: Number(r.total_qtd), spread_revenue: Number(r.total_spread), clientes: Number(r.clientes) }; }); // Format results const fmtKpi = (r) => ({ qtd: Number(r?.qtd) || 0, vol_usd: Number(r?.vol_usd) || 0, vol_brl: Number(r?.vol_brl) || 0, spread_revenue: Number(r?.spread_revenue) || 0, avg_spread_pct: Number(r?.avg_spread_pct) || 0, clientes: Number(r?.clientes) || 0 }); const brl = fmtKpi(kpiBrlUsd[0]); const usd = fmtKpi(kpiUsdBrl[0]); const uu = { qtd: Number(kpiUsdUsd[0]?.qtd) || 0, vol_usd: Number(kpiUsdUsd[0]?.vol_usd) || 0, clientes: Number(kpiUsdUsd[0]?.clientes) || 0 }; const totalQtd = brl.qtd + usd.qtd + uu.qtd; const totalVolUsd = brl.vol_usd + usd.vol_usd + uu.vol_usd; const pBrl = Number(prevBrlUsd[0]?.qtd) || 0; const pUsd = Number(prevUsdBrl[0]?.qtd) || 0; const pUu = Number(prevUsdUsd[0]?.qtd) || 0; const prevQtd = pBrl + pUsd + pUu; const prevVolUsd = (Number(prevBrlUsd[0]?.vol_usd) || 0) + (Number(prevUsdBrl[0]?.vol_usd) || 0) + (Number(prevUsdUsd[0]?.vol_usd) || 0); const prevSpread = (Number(prevBrlUsd[0]?.spread_revenue) || 0) + (Number(prevUsdBrl[0]?.spread_revenue) || 0); const retPrev = Number(retention[0]?.prev_clients) || 0; const retCurr = Number(retention[0]?.retained) || 0; return { kpis: { brlUsd: brl, usdBrl: usd, usdUsd: uu, total: { qtd: totalQtd, vol_usd: totalVolUsd, spread_revenue: brl.spread_revenue + usd.spread_revenue, clientes: Number(uniqueClients[0]?.total) || 0, ticket_medio: totalQtd > 0 ? Math.round(totalVolUsd / totalQtd) : 0 } }, comparison: { prev_qtd: prevQtd, prev_vol_usd: prevVolUsd, prev_spread: prevSpread }, trend: { brlUsd: fmtTrendRows(trendBrlUsd), usdBrl: fmtTrendRows(trendUsdBrl), usdUsd: fmtTrendRows(trendUsdUsd) }, topClients: topClients.map(r => ({ nome: r.nome, vol_usd: Number(r.total_usd), qtd: Number(r.total_qtd) })), retention: { prev_clients: retPrev, retained: retCurr, rate: retPrev > 0 ? Math.round(retCurr / retPrev * 100) : 0 }, clientsAtRisk: clientsAtRisk.map(r => ({ nome: r.nome, vol_usd: Number(r.total_usd), qtd: Number(r.total_qtd), last_op: r.last_op instanceof Date ? r.last_op.toISOString().slice(0, 10) : String(r.last_op).slice(0, 16), days_inactive: Number(r.days_inactive) || 0 })), agentRanking: agents, netting: { saida_usd: brl.vol_usd, entrada_usd: usd.vol_usd, posicao_liquida: usd.vol_usd - brl.vol_usd, eficiencia: brl.vol_usd > 0 ? Math.min(100, Math.round(usd.vol_usd / brl.vol_usd * 100)) : 0 } }; } finally { conn.release(); } } // Revenue Analytics - Real P&L by product with dynamic granularity async function fetchRevenueAnalytics(dataInicio, dataFim, granularity = 'dia') { const conn = await pool.getConnection(); try { const validGran = ['dia', 'mes', 'ano'].includes(granularity) ? granularity : 'dia'; let periodoInicio, periodoLabel; switch (validGran) { case 'ano': periodoInicio = "MAKEDATE(YEAR(dia), 1)"; periodoLabel = "DATE_FORMAT(dia, '%Y')"; break; case 'mes': periodoInicio = "CAST(DATE_FORMAT(dia, '%Y-%m-01') AS DATE)"; periodoLabel = "DATE_FORMAT(dia, '%Y-%m')"; break; default: periodoInicio = "DATE(dia)"; periodoLabel = "DATE_FORMAT(dia, '%Y-%m-%d')"; } const [rows] = await conn.execute(` WITH limites AS ( SELECT CAST(? AS DATE) AS inicio, DATE_ADD(CAST(? AS DATE), INTERVAL 1 DAY) AS fim_exclusivo ), q1 AS ( SELECT CASE WHEN pb.tipo_envio = 'balance' THEN DATE(pb.data_cp) ELSE DATE(pb.created_at) END AS dia, CONCAT('US→BR: ', COALESCE(pb.tipo_envio, 'desconhecido')) AS produto, CASE WHEN pb.tipo_envio = 'balance' THEN COALESCE(pb.fee, 0) ELSE COALESCE( CASE WHEN pb.ptax IS NOT NULL AND pb.ptax > 0 THEN ((pb.ptax - pb.cotacao) * pb.valor) / pb.ptax ELSE 0 END, 0 ) + COALESCE(pb.fee, 0) END AS receita FROM pagamento_br pb JOIN limites l ON ( CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END ) >= l.inicio AND ( CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END ) < l.fim_exclusivo WHERE pb.valor > 0 AND pb.data_cp IS NOT NULL AND pb.data_cp <> '0000-00-00' ), q2 AS ( SELECT DATE(t.created_at) AS dia, CASE WHEN t.cobranca_id IS NOT NULL THEN 'BR→US: Checkout' ELSE 'BR→US: CambioTransfer' END AS produto, ( ( ROUND((t.amount_brl - IF(pm.provider IN ('ouribank','bs2'), 0, t.fee)) / t.ptax, 2) - COALESCE(t.pfee, 0) ) - ( t.amount_usd + COALESCE(t.bonus_valor, 0) - COALESCE(t.taxa_cr, 0) ) ) AS receita FROM br_transaction_to_usa t JOIN br_payment_methods pm ON t.payment_method_id = pm.id JOIN limites l ON t.created_at >= l.inicio AND t.created_at < l.fim_exclusivo WHERE pm.provider IN ('dlocal','bexs','braza','bs2','ouribank','msb') AND t.ptax IS NOT NULL AND t.ptax > 0 AND ( t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00' ) ), unioned AS ( SELECT dia, produto, receita FROM q1 UNION ALL SELECT dia, produto, receita FROM q2 ) SELECT ${periodoInicio} AS periodo_inicio, ${periodoLabel} AS periodo_label, produto, ROUND(SUM(receita), 2) AS receita, COUNT(*) AS qtd FROM unioned GROUP BY periodo_inicio, periodo_label, produto ORDER BY periodo_inicio, produto `, [dataInicio, dataFim]); // Also get totals by product const [totals] = await conn.execute(` WITH limites AS ( SELECT CAST(? AS DATE) AS inicio, DATE_ADD(CAST(? AS DATE), INTERVAL 1 DAY) AS fim_exclusivo ), q1 AS ( SELECT CASE WHEN pb.tipo_envio = 'balance' THEN COALESCE(pb.fee, 0) ELSE COALESCE( CASE WHEN pb.ptax IS NOT NULL AND pb.ptax > 0 THEN ((pb.ptax - pb.cotacao) * pb.valor) / pb.ptax ELSE 0 END, 0 ) + COALESCE(pb.fee, 0) END AS receita, 'US→BR' AS direcao, COALESCE(pb.tipo_envio, 'desconhecido') AS tipo FROM pagamento_br pb JOIN limites l ON ( CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END ) >= l.inicio AND ( CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END ) < l.fim_exclusivo WHERE pb.valor > 0 AND pb.data_cp IS NOT NULL AND pb.data_cp <> '0000-00-00' ), q2 AS ( SELECT ( (ROUND((t.amount_brl - IF(pm.provider IN ('ouribank','bs2'), 0, t.fee)) / t.ptax, 2) - COALESCE(t.pfee, 0)) - (t.amount_usd + COALESCE(t.bonus_valor, 0) - COALESCE(t.taxa_cr, 0)) ) AS receita, 'BR→US' AS direcao, CASE WHEN t.cobranca_id IS NOT NULL THEN 'Checkout' ELSE 'CambioTransfer' END AS tipo FROM br_transaction_to_usa t JOIN br_payment_methods pm ON t.payment_method_id = pm.id JOIN limites l ON t.created_at >= l.inicio AND t.created_at < l.fim_exclusivo WHERE pm.provider IN ('dlocal','bexs','braza','bs2','ouribank','msb') AND t.ptax IS NOT NULL AND t.ptax > 0 AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00') ) SELECT direcao, tipo, ROUND(SUM(receita), 2) AS total_receita, COUNT(*) AS total_qtd FROM (SELECT receita, direcao, tipo FROM q1 UNION ALL SELECT receita, direcao, tipo FROM q2) all_data GROUP BY direcao, tipo ORDER BY direcao, tipo `, [dataInicio, dataFim]); const timeline = rows.map(r => ({ periodo_inicio: r.periodo_inicio instanceof Date ? r.periodo_inicio.toISOString().slice(0, 10) : String(r.periodo_inicio).slice(0, 10), periodo_label: r.periodo_label, produto: r.produto, receita: Number(r.receita), qtd: Number(r.qtd) })); const totalsByProduct = totals.map(r => ({ direcao: r.direcao, tipo: r.tipo, produto: r.direcao + ': ' + r.tipo, receita: Number(r.total_receita), qtd: Number(r.total_qtd) })); const grandTotal = totalsByProduct.reduce((s, r) => s + r.receita, 0); const grandQtd = totalsByProduct.reduce((s, r) => s + r.qtd, 0); const receitaBrUs = totalsByProduct.filter(r => r.direcao === 'BR→US').reduce((s, r) => s + r.receita, 0); const receitaUsBr = totalsByProduct.filter(r => r.direcao === 'US→BR').reduce((s, r) => s + r.receita, 0); return { timeline, totals: totalsByProduct, summary: { total_receita: Math.round(grandTotal * 100) / 100, total_qtd: grandQtd, receita_br_us: Math.round(receitaBrUs * 100) / 100, receita_us_br: Math.round(receitaUsBr * 100) / 100, ticket_medio_receita: grandQtd > 0 ? Math.round(grandTotal / grandQtd * 100) / 100 : 0 }, granularity: validGran }; } finally { conn.release(); } } async function fetchBIStrategic(dataInicio, dataFim) { const conn = await pool.getConnection(); try { const { prevStartStr, prevEndStr } = calcPrevPeriod(dataInicio, dataFim); // Run all 4 sections in parallel — each is independent const [cohorts, expansion, crossSell, maturity] = await Promise.all([ _fetchCohorts(conn), _fetchExpansion(conn, dataInicio, dataFim, prevStartStr, prevEndStr), _fetchCrossSell(conn, dataInicio, dataFim), _fetchMaturity(conn, dataInicio, dataFim, prevStartStr, prevEndStr) ]); return { cohorts, expansion, crossSell, maturity }; } finally { conn.release(); } } async function _fetchCohorts(conn) { const [[cohortClients], [activeMonths]] = await Promise.all([ conn.execute(` SELECT id_conta, DATE_FORMAT(MIN(first_op), '%Y-%m') as cohort_month FROM ( SELECT id_conta, MIN(created_at) as first_op FROM br_transaction_to_usa GROUP BY id_conta UNION ALL SELECT id_conta, MIN(created_at) as first_op FROM pagamento_br WHERE cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta ) f GROUP BY id_conta `), conn.execute(` SELECT id_conta, active_month FROM ( SELECT id_conta, DATE_FORMAT(created_at, '%Y-%m') as active_month FROM br_transaction_to_usa GROUP BY id_conta, DATE_FORMAT(created_at, '%Y-%m') UNION SELECT id_conta, DATE_FORMAT(created_at, '%Y-%m') as active_month FROM pagamento_br WHERE cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta, DATE_FORMAT(created_at, '%Y-%m') ) m `) ]); const clientCohort = {}; cohortClients.forEach(r => { clientCohort[r.id_conta] = r.cohort_month; }); const clientMonths = {}; activeMonths.forEach(r => { if (!clientMonths[r.id_conta]) clientMonths[r.id_conta] = new Set(); clientMonths[r.id_conta].add(r.active_month); }); const allMonths = [...new Set([...cohortClients.map(r => r.cohort_month), ...activeMonths.map(r => r.active_month)])].sort(); const cohortMap = {}; cohortClients.forEach(r => { const cm = r.cohort_month; if (!cohortMap[cm]) cohortMap[cm] = { size: 0, months: {} }; cohortMap[cm].size++; }); Object.keys(clientCohort).forEach(clientId => { const cm = clientCohort[clientId]; const months = clientMonths[clientId] || new Set(); const cmIdx = allMonths.indexOf(cm); months.forEach(am => { const amIdx = allMonths.indexOf(am); const offset = amIdx - cmIdx; if (offset >= 0) { if (!cohortMap[cm].months[offset]) cohortMap[cm].months[offset] = 0; cohortMap[cm].months[offset]++; } }); }); const cohortKeys = Object.keys(cohortMap).sort().slice(-12); return cohortKeys.map(cm => { const c = cohortMap[cm]; const maxOff = allMonths.length - allMonths.indexOf(cm); const retention = []; for (let i = 0; i < Math.min(maxOff, 13); i++) { retention.push(c.size > 0 ? Math.round((c.months[i] || 0) / c.size * 100) : 0); } return { month: cm, size: c.size, retention }; }); } async function _fetchExpansion(conn, dataInicio, dataFim, prevStartStr, prevEndStr) { const [[currRevenue], [prevRevenue]] = await Promise.all([ conn.execute(` SELECT id_conta, ROUND(SUM(revenue), 2) as revenue, ROUND(SUM(vol_usd), 2) as vol_usd FROM ( SELECT id_conta, SUM((exchange_rate - ptax) / exchange_rate * amount_usd) as revenue, SUM(amount_usd) as vol_usd FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY id_conta UNION ALL SELECT id_conta, SUM((ptax - cotacao) / ptax * valor) as revenue, SUM(valor) as vol_usd FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta ) c GROUP BY id_conta `, [dataInicio, dataFim, dataInicio, dataFim]), conn.execute(` SELECT id_conta, ROUND(SUM(revenue), 2) as revenue, ROUND(SUM(vol_usd), 2) as vol_usd FROM ( SELECT id_conta, SUM((exchange_rate - ptax) / exchange_rate * amount_usd) as revenue, SUM(amount_usd) as vol_usd FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY id_conta UNION ALL SELECT id_conta, SUM((ptax - cotacao) / ptax * valor) as revenue, SUM(valor) as vol_usd FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta ) p GROUP BY id_conta `, [prevStartStr, prevEndStr, prevStartStr, prevEndStr]) ]); const currMap = {}; currRevenue.forEach(r => { currMap[r.id_conta] = { revenue: Number(r.revenue), vol_usd: Number(r.vol_usd) }; }); const prevMap = {}; prevRevenue.forEach(r => { prevMap[r.id_conta] = { revenue: Number(r.revenue), vol_usd: Number(r.vol_usd) }; }); const allClientIds = new Set([...Object.keys(currMap), ...Object.keys(prevMap)]); const expansion = { new_clients: { count: 0, revenue: 0 }, expansion: { count: 0, revenue: 0 }, stable: { count: 0, revenue: 0 }, contraction: { count: 0, revenue: 0 }, churned: { count: 0, revenue: 0 } }; allClientIds.forEach(id => { const curr = currMap[id]; const prev = prevMap[id]; if (curr && !prev) { expansion.new_clients.count++; expansion.new_clients.revenue += curr.revenue; } else if (!curr && prev) { expansion.churned.count++; expansion.churned.revenue -= Math.abs(prev.revenue); } else if (curr && prev) { const absP = Math.abs(prev.revenue); const change = absP > 0 ? (curr.revenue - prev.revenue) / absP : 0; if (change > 0.1) { expansion.expansion.count++; expansion.expansion.revenue += (curr.revenue - prev.revenue); } else if (change < -0.1) { expansion.contraction.count++; expansion.contraction.revenue += (curr.revenue - prev.revenue); } else { expansion.stable.count++; expansion.stable.revenue += curr.revenue; } } }); return expansion; } async function _fetchCrossSell(conn, dataInicio, dataFim) { const [crossSellData] = await conn.execute(` SELECT c.id_conta, t.vol_usd as pay_vol, p.vol_usd as checkout_vol FROM ( SELECT DISTINCT id_conta FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? UNION SELECT DISTINCT id_conta FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') ) c LEFT JOIN ( SELECT id_conta, ROUND(SUM(amount_usd), 2) as vol_usd FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY id_conta ) t ON t.id_conta = c.id_conta LEFT JOIN ( SELECT id_conta, ROUND(SUM(valor), 2) as vol_usd FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta ) p ON p.id_conta = c.id_conta `, [dataInicio, dataFim, dataInicio, dataFim, dataInicio, dataFim, dataInicio, dataFim]); const crossSell = { pay_only: { count: 0, vol: 0 }, checkout_only: { count: 0, vol: 0 }, both: { count: 0, vol: 0 } }; crossSellData.forEach(r => { const pv = Number(r.pay_vol) || 0; const cv = Number(r.checkout_vol) || 0; if (pv > 0 && cv > 0) { crossSell.both.count++; crossSell.both.vol += pv + cv; } else if (pv > 0) { crossSell.pay_only.count++; crossSell.pay_only.vol += pv; } else if (cv > 0) { crossSell.checkout_only.count++; crossSell.checkout_only.vol += cv; } }); return crossSell; } async function _fetchMaturity(conn, dataInicio, dataFim, prevStartStr, prevEndStr) { const [maturityData] = await conn.execute(` SELECT id_conta, MIN(first_op) as first_op, FLOOR(DATEDIFF(CURDATE(), MIN(first_op)) / 30.44) as months_active, SUM(vol_usd) as lifetime_vol, SUM(CASE WHEN period = 'curr' THEN vol_usd ELSE 0 END) as curr_vol, SUM(CASE WHEN period = 'prev' THEN vol_usd ELSE 0 END) as prev_vol FROM ( SELECT id_conta, MIN(created_at) as first_op, SUM(amount_usd) as vol_usd, 'all' as period FROM br_transaction_to_usa GROUP BY id_conta UNION ALL SELECT id_conta, MIN(created_at) as first_op, SUM(valor) as vol_usd, 'all' as period FROM pagamento_br WHERE cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta UNION ALL SELECT id_conta, NULL, SUM(amount_usd) as vol_usd, 'curr' as period FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY id_conta UNION ALL SELECT id_conta, NULL, SUM(valor) as vol_usd, 'curr' as period FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta UNION ALL SELECT id_conta, NULL, SUM(amount_usd) as vol_usd, 'prev' as period FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY id_conta UNION ALL SELECT id_conta, NULL, SUM(valor) as vol_usd, 'prev' as period FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta ) combined GROUP BY id_conta `, [dataInicio, dataFim, dataInicio, dataFim, prevStartStr, prevEndStr, prevStartStr, prevEndStr]); const maturity = { new_client: { count: 0, vol: 0 }, growing: { count: 0, vol: 0 }, mature: { count: 0, vol: 0 }, declining: { count: 0, vol: 0 } }; maturityData.forEach(r => { const months = Number(r.months_active) || 0; const cv = Number(r.curr_vol) || 0; const pv = Number(r.prev_vol) || 0; const lv = Number(r.lifetime_vol) || 0; if (months < 3) { maturity.new_client.count++; maturity.new_client.vol += lv; } else if (pv > 0 && cv < pv * 0.85) { maturity.declining.count++; maturity.declining.vol += lv; } else if (months >= 12) { maturity.mature.count++; maturity.mature.vol += lv; } else { maturity.growing.count++; maturity.growing.vol += lv; } }); return maturity; } module.exports = { fetchBIData, fetchRevenueAnalytics, fetchBIStrategic };