feat: BI estrategico — cohort retention, revenue expansion, cross-sell, maturity + receita em USD
- Cohort retention heatmap com matriz de retencao por mes de aquisicao - Revenue expansion/contraction waterfall (new/expansion/stable/contraction/churned) - Cross-sell analysis (CambioPay only vs CambioCheckout only vs Both) - Client maturity segmentation (new/growing/mature/declining) - Nova query fetchBIStrategic + endpoint /admin/api/bi/strategic - Todas receitas de spread convertidas de BRL para USD em todas as queries - Labels e formatacao atualizados para USD em admin-bi e admin-cliente Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
14
server.js
14
server.js
@@ -11,7 +11,7 @@ const express = require('express');
|
||||
const session = require('express-session');
|
||||
const path = require('path');
|
||||
const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth');
|
||||
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchClientList, fetchClientProfile, fetchClientData } = require('./src/queries');
|
||||
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchClientList, fetchClientProfile, fetchClientData } = require('./src/queries');
|
||||
const { buildHTML } = require('./src/dashboard');
|
||||
const { buildAdminHTML } = require('./src/admin-panel');
|
||||
const { buildAdminHomeHTML } = require('./src/admin-home');
|
||||
@@ -370,6 +370,18 @@ app.get('/admin/api/bi/revenue', requireRole('admin'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/api/bi/strategic', requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
||||
const data = await fetchBIStrategic(start, end);
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error('Strategic BI API error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Admin Cliente Dashboard (admin only) ---
|
||||
app.get('/admin/cliente', requireRole('admin'), (req, res) => {
|
||||
try {
|
||||
|
||||
1458
src/admin-bi.js
1458
src/admin-bi.js
File diff suppressed because it is too large
Load Diff
1399
src/admin-cliente.js
1399
src/admin-cliente.js
File diff suppressed because it is too large
Load Diff
292
src/queries.js
292
src/queries.js
@@ -524,7 +524,7 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
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) * amount_usd), 0), 2) as spread_revenue,
|
||||
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
|
||||
@@ -537,7 +537,7 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
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) * valor), 0), 2) as spread_revenue,
|
||||
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
|
||||
@@ -571,12 +571,12 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
// 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) * amount_usd),0),2) as spread_revenue
|
||||
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) * valor),0),2) as spread_revenue
|
||||
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]);
|
||||
@@ -679,14 +679,14 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
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.amount_usd) as spread_rev
|
||||
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.valor) as spread_rev
|
||||
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) <= ?
|
||||
@@ -984,7 +984,7 @@ async function fetchClientProfile(clienteId) {
|
||||
const [brl] = 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) * amount_usd),0),2) as spread_revenue,
|
||||
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue,
|
||||
MIN(created_at) as first_op, MAX(created_at) as last_op
|
||||
FROM br_transaction_to_usa WHERE id_conta = ?
|
||||
`, [clienteId]);
|
||||
@@ -993,7 +993,7 @@ async function fetchClientProfile(clienteId) {
|
||||
const [usd] = 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) * valor),0),2) as spread_revenue,
|
||||
ROUND(COALESCE(SUM((ptax - cotacao) / ptax * valor),0),2) as spread_revenue,
|
||||
MIN(created_at) as first_op, MAX(created_at) as last_op
|
||||
FROM pagamento_br WHERE id_conta = ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0
|
||||
@@ -1011,6 +1011,20 @@ async function fetchClientProfile(clienteId) {
|
||||
|
||||
const brlQtd = Number(brlData.qtd) || 0;
|
||||
const usdQtd = Number(usdData.qtd) || 0;
|
||||
const totalOps = brlQtd + usdQtd;
|
||||
const totalVolUsd = (Number(brlData.vol_usd) || 0) + (Number(usdData.vol_usd) || 0);
|
||||
const totalSpreadRev = (Number(brlData.spread_revenue) || 0) + (Number(usdData.spread_revenue) || 0);
|
||||
|
||||
// Months active (distinct months with transactions)
|
||||
const [monthsRows] = await conn.execute(`
|
||||
SELECT COUNT(DISTINCT mes) as months_active FROM (
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes FROM br_transaction_to_usa WHERE id_conta = ?
|
||||
UNION
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes FROM pagamento_br WHERE id_conta = ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
) m
|
||||
`, [clienteId, clienteId]);
|
||||
const monthsActive = Number(monthsRows[0]?.months_active) || 0;
|
||||
|
||||
return {
|
||||
id: clienteId,
|
||||
@@ -1018,10 +1032,15 @@ async function fetchClientProfile(clienteId) {
|
||||
first_op: firstOp ? firstOp.toISOString().slice(0, 10) : null,
|
||||
last_op: lastOp ? lastOp.toISOString().slice(0, 10) : null,
|
||||
days_inactive: daysInactive,
|
||||
total_ops: brlQtd + usdQtd,
|
||||
total_vol_usd: (Number(brlData.vol_usd) || 0) + (Number(usdData.vol_usd) || 0),
|
||||
total_ops: totalOps,
|
||||
total_vol_usd: totalVolUsd,
|
||||
total_vol_brl: (Number(brlData.vol_brl) || 0) + (Number(usdData.vol_brl) || 0),
|
||||
total_spread_revenue: (Number(brlData.spread_revenue) || 0) + (Number(usdData.spread_revenue) || 0),
|
||||
total_spread_revenue: totalSpreadRev,
|
||||
months_active: monthsActive,
|
||||
avg_monthly_vol: monthsActive > 0 ? Math.round(totalVolUsd / monthsActive) : 0,
|
||||
avg_monthly_ops: monthsActive > 0 ? Math.round(totalOps / monthsActive * 10) / 10 : 0,
|
||||
avg_monthly_revenue: monthsActive > 0 ? Math.round(totalSpreadRev / monthsActive * 100) / 100 : 0,
|
||||
ltv: totalSpreadRev,
|
||||
brlUsd: { qtd: brlQtd, vol_usd: Number(brlData.vol_usd) || 0 },
|
||||
usdBrl: { qtd: usdQtd, vol_usd: Number(usdData.vol_usd) || 0 }
|
||||
};
|
||||
@@ -1047,7 +1066,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
|
||||
const [kpiBrl] = 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) * amount_usd),0),2) as spread_revenue,
|
||||
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
|
||||
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
@@ -1056,7 +1075,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
|
||||
const [kpiUsd] = 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) * valor),0),2) as spread_revenue,
|
||||
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
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
@@ -1065,12 +1084,12 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
|
||||
// Previous period
|
||||
const [prevBrl] = await conn.execute(`
|
||||
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(amount_usd),0),2) as vol_usd,
|
||||
ROUND(COALESCE(SUM((exchange_rate - ptax) * amount_usd),0),2) as spread_revenue
|
||||
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue
|
||||
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
`, [clienteId, prevStartStr, prevEndStr]);
|
||||
const [prevUsd] = await conn.execute(`
|
||||
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd,
|
||||
ROUND(COALESCE(SUM((ptax - cotacao) * valor),0),2) as spread_revenue
|
||||
ROUND(COALESCE(SUM((ptax - cotacao) / ptax * valor),0),2) as spread_revenue
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
`, [clienteId, prevStartStr, prevEndStr]);
|
||||
@@ -1151,14 +1170,18 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
|
||||
GROUP BY p.tipo_envio
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Monthly average
|
||||
// Monthly breakdown (volume + revenue + qty)
|
||||
const [monthlyBrl] = await conn.execute(`
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, ROUND(AVG(amount_usd),2) as avg_usd, COUNT(*) as qtd
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, COUNT(*) as qtd,
|
||||
ROUND(SUM(amount_usd),2) as vol_usd,
|
||||
ROUND(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),2) as spread_revenue
|
||||
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY mes
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
const [monthlyUsd] = await conn.execute(`
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, ROUND(AVG(valor),2) as avg_usd, COUNT(*) as qtd
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, COUNT(*) as qtd,
|
||||
ROUND(SUM(valor),2) as vol_usd,
|
||||
ROUND(SUM((ptax - cotacao) / ptax * valor),2) as spread_revenue
|
||||
FROM pagamento_br WHERE id_conta = ? AND 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_FORMAT(created_at, '%Y-%m') ORDER BY mes
|
||||
@@ -1193,12 +1216,13 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
|
||||
provMap[n].vol_usd += Number(r.vol_usd);
|
||||
});
|
||||
|
||||
// Merge monthly avg
|
||||
// Merge monthly data
|
||||
const monthMap = {};
|
||||
[...monthlyBrl, ...monthlyUsd].forEach(r => {
|
||||
if (!monthMap[r.mes]) monthMap[r.mes] = { mes: r.mes, total_usd: 0, total_qtd: 0 };
|
||||
monthMap[r.mes].total_usd += Number(r.avg_usd) * Number(r.qtd);
|
||||
monthMap[r.mes].total_qtd += Number(r.qtd);
|
||||
if (!monthMap[r.mes]) monthMap[r.mes] = { mes: r.mes, qtd: 0, vol_usd: 0, spread_revenue: 0 };
|
||||
monthMap[r.mes].qtd += Number(r.qtd);
|
||||
monthMap[r.mes].vol_usd += Number(r.vol_usd);
|
||||
monthMap[r.mes].spread_revenue += Number(r.spread_revenue);
|
||||
});
|
||||
|
||||
const transactions = [
|
||||
@@ -1234,13 +1258,234 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
|
||||
transactions,
|
||||
dayOfWeek: dowMap,
|
||||
providers: Object.values(provMap).sort((a, b) => b.vol_usd - a.vol_usd),
|
||||
monthlyAvg: Object.values(monthMap).map(m => ({ mes: m.mes, avg_usd: m.total_qtd > 0 ? Math.round(m.total_usd / m.total_qtd) : 0, qtd: m.total_qtd })).sort((a, b) => a.mes.localeCompare(b.mes))
|
||||
monthly: Object.values(monthMap).map(m => ({ mes: m.mes, qtd: m.qtd, vol_usd: Math.round(m.vol_usd * 100) / 100, spread_revenue: Math.round(m.spread_revenue * 100) / 100, avg_usd: m.qtd > 0 ? Math.round(m.vol_usd / m.qtd) : 0 })).sort((a, b) => a.mes.localeCompare(b.mes))
|
||||
};
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBIStrategic(dataInicio, dataFim) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
const start = new Date(dataInicio);
|
||||
const end = new Date(dataFim);
|
||||
const periodDays = Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
||||
const prevEnd = new Date(start);
|
||||
prevEnd.setDate(prevEnd.getDate() - 1);
|
||||
const prevStart = new Date(prevEnd);
|
||||
prevStart.setDate(prevStart.getDate() - periodDays + 1);
|
||||
const prevStartStr = prevStart.toISOString().slice(0, 10);
|
||||
const prevEndStr = prevEnd.toISOString().slice(0, 10);
|
||||
|
||||
// === 1. COHORT RETENTION ===
|
||||
const [cohortClients] = await 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
|
||||
`);
|
||||
const [activeMonths] = await 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);
|
||||
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);
|
||||
}
|
||||
return { month: cm, size: c.size, retention };
|
||||
});
|
||||
|
||||
// === 2. REVENUE EXPANSION / CONTRACTION ===
|
||||
const [currRevenue] = await 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]);
|
||||
const [prevRevenue] = await 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// === 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;
|
||||
} else {
|
||||
maturity.growing.count++; maturity.growing.vol += lv;
|
||||
}
|
||||
});
|
||||
|
||||
return { cohorts, expansion, crossSell, maturity };
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchTransacoes,
|
||||
fetchAllTransacoes,
|
||||
@@ -1253,6 +1498,7 @@ module.exports = {
|
||||
fetchKPIsByPeriod,
|
||||
fetchBIData,
|
||||
fetchRevenueAnalytics,
|
||||
fetchBIStrategic,
|
||||
fetchClientList,
|
||||
fetchClientProfile,
|
||||
fetchClientData
|
||||
|
||||
Reference in New Issue
Block a user