feat: integra dados de merchant/checkout no Client 360

Merchants (via br_cb_empresas) agora mostram dados de CambioCheckout:
- fetchMerchantProfile detecta merchant e retorna lifetime checkout stats
- fetchMerchantData retorna KPIs, monthly, top payers e transacoes por periodo
- fetchTopClients inclui checkout volume (merchants sobem no ranking)
- fetchClientSearch inclui merchants nos resultados de busca
- Profile/data endpoints fazem merge automatico dos dados checkout
- UI: badge MERCHANT roxo, 6 hero cards checkout, chart mensal, top 10 payers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-16 16:47:11 -05:00
parent 600a8044c2
commit a76ab30730
3 changed files with 435 additions and 20 deletions

View File

@@ -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, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData } = require('./src/queries');
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData, fetchMerchantProfile, fetchMerchantData } = require('./src/queries');
const { buildHTML } = require('./src/dashboard');
const { buildAdminHTML } = require('./src/admin-panel');
const { buildAdminHomeHTML } = require('./src/admin-home');
@@ -421,8 +421,32 @@ app.get('/admin/api/cliente/:id/profile', requireRole('admin'), async (req, res)
try {
const clienteId = parseInt(req.params.id);
if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' });
const data = await fetchClientProfile(clienteId);
res.json(data);
const [profile, merchant] = await Promise.all([
fetchClientProfile(clienteId),
fetchMerchantProfile(clienteId)
]);
if (merchant.is_merchant) {
const ck = merchant.checkout;
profile.merchant = { empresa_id: merchant.empresa_id, nome_empresa: merchant.nome_empresa };
profile.total_ops += ck.tx_count;
profile.total_vol_usd += ck.vol_usd;
profile.total_spread_revenue += ck.revenue;
profile.ltv = profile.total_spread_revenue;
// Extend date ranges
const dates = [profile.first_op, ck.first_op].filter(Boolean);
const lastDates = [profile.last_op, ck.last_op].filter(Boolean);
if (dates.length) profile.first_op = dates.sort()[0];
if (lastDates.length) {
profile.last_op = lastDates.sort().pop();
profile.days_inactive = Math.round((Date.now() - new Date(profile.last_op).getTime()) / 86400000);
}
profile.months_active = Math.max(profile.months_active, ck.months_active);
profile.avg_monthly_vol = profile.months_active > 0 ? Math.round(profile.total_vol_usd / profile.months_active) : 0;
profile.avg_monthly_ops = profile.months_active > 0 ? Math.round(profile.total_ops / profile.months_active * 10) / 10 : 0;
profile.avg_monthly_revenue = profile.months_active > 0 ? Math.round(profile.total_spread_revenue / profile.months_active * 100) / 100 : 0;
profile.checkout = ck;
}
res.json(profile);
} catch (err) {
console.error('Client profile API error:', err);
res.status(500).json({ error: err.message });
@@ -434,8 +458,39 @@ app.get('/admin/api/cliente/:id/data', requireRole('admin'), async (req, res) =>
const clienteId = parseInt(req.params.id);
const { start, end } = req.query;
if (!clienteId || !start || !end) return res.status(400).json({ error: 'client ID, start and end required' });
const data = await fetchClientData(clienteId, start, end);
res.json(data);
const merchant = await fetchMerchantProfile(clienteId);
if (merchant.is_merchant) {
const [data, mData] = await Promise.all([
fetchClientData(clienteId, start, end),
fetchMerchantData(merchant.empresa_id, start, end)
]);
// Add checkout KPIs
data.kpis.checkout = mData.kpis;
// Merge totals
data.kpis.total.qtd += mData.kpis.qtd;
data.kpis.total.vol_usd += mData.kpis.vol_usd;
data.kpis.total.spread_revenue += mData.kpis.revenue;
const totalQtd = data.kpis.total.qtd;
data.kpis.total.ticket_medio = totalQtd > 0 ? Math.round(data.kpis.total.vol_usd / totalQtd) : 0;
// Merge comparison
data.comparison.prev_qtd += mData.comparison.prev_qtd;
data.comparison.prev_vol_usd += mData.comparison.prev_vol_usd;
data.comparison.prev_spread += mData.comparison.prev_revenue;
// Merchant-specific data
data.merchant = {
monthly: mData.monthly,
topPayers: mData.topPayers,
comparison: mData.comparison
};
// Merge transactions (checkout txs get flow="Checkout")
data.transactions = data.transactions.concat(mData.transactions)
.sort((a, b) => b.date.localeCompare(a.date));
res.json(data);
} else {
const data = await fetchClientData(clienteId, start, end);
res.json(data);
}
} catch (err) {
console.error('Client data API error:', err);
res.status(500).json({ error: err.message });