diff --git a/src/queries/bi.queries.js b/src/queries/bi.queries.js index 82473e6..501f60a 100644 --- a/src/queries/bi.queries.js +++ b/src/queries/bi.queries.js @@ -10,184 +10,151 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) { try { const { prevStartStr, prevEndStr } = calcPrevPeriod(dataInicio, dataFim); - // 1. BRL→USD KPIs - const [kpiBrlUsd] = await 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 - const [kpiUsdBrl] = await 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 - const [kpiUsdUsd] = await 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 across all flows - const [uniqueClients] = await conn.execute(` - SELECT COUNT(DISTINCT id_conta) as total FROM ( - SELECT id_conta FROM br_transaction_to_usa + // 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) <= ? - UNION - SELECT id_conta FROM pagamento_br + `, [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) <= ? - ) all_clients - `, [dataInicio, dataFim, dataInicio, dataFim]); - - // 5. Previous period totals for comparison - const [prevBrlUsd] = await 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]); - const [prevUsdBrl] = await 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]); - const [prevUsdUsd] = await 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. BRL→USD daily trend with spread - const [trendBrlUsd] = await 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]); - - // 7. USD→BRL daily trend with spread - const [trendUsdBrl] = await 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]); - - // 8. USD→USD daily trend - const [trendUsdUsd] = await 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 10 clients by volume - const [topClients] = await 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. Client retention - const [retention] = await 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 - const [clientsAtRisk] = await 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 with spread revenue - const [agentRanking] = await 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]); + 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) => { @@ -444,8 +411,23 @@ async function fetchBIStrategic(dataInicio, dataFim) { try { const { prevStartStr, prevEndStr } = calcPrevPeriod(dataInicio, dataFim); - // === 1. COHORT RETENTION === - const [cohortClients] = await conn.execute(` + // 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 @@ -453,8 +435,8 @@ async function fetchBIStrategic(dataInicio, dataFim) { WHERE cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance') GROUP BY id_conta ) f GROUP BY id_conta - `); - const [activeMonths] = await conn.execute(` + `), + 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') @@ -463,50 +445,53 @@ async function fetchBIStrategic(dataInicio, dataFim) { 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 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); - const cohorts = 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); + 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]++; } - return { month: cm, size: c.size, retention }; }); + }); - // === 2. REVENUE EXPANSION / CONTRACTION === - const [currRevenue] = await conn.execute(` + 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) <= ? @@ -517,8 +502,8 @@ async function fetchBIStrategic(dataInicio, dataFim) { 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]); - const [prevRevenue] = await conn.execute(` + `, [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) <= ? @@ -529,127 +514,128 @@ async function fetchBIStrategic(dataInicio, dataFim) { 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]); + `, [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 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; - } - } - }); - - // === 3. CROSS-SELL === - 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; } - }); - - // === 4. CLIENT MATURITY SEGMENTS === - 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; + 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 { - maturity.growing.count++; maturity.growing.vol += lv; + expansion.stable.count++; expansion.stable.revenue += curr.revenue; } - }); + } + }); + return expansion; +} - return { cohorts, expansion, crossSell, maturity }; - } finally { - conn.release(); - } +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 = {