# BI-CCC Implementation Guide — Developer Reference > **Audience**: Engineering Team > **Date**: February 2026 > **Status**: Approved for implementation > **Current Maturity**: 45/100 → **Target**: 85/100 --- ## Table of Contents 1. [Overview & Current State](#1-overview--current-state) 2. [Phase 1 — Quick Wins (Weeks 1-4)](#2-phase-1--quick-wins-weeks-1-4) 3. [Phase 2 — Data Architecture (Weeks 5-10)](#3-phase-2--data-architecture-weeks-5-10) 4. [Phase 3 — Compliance & Predictive Analytics (Weeks 11-16)](#4-phase-3--compliance--predictive-analytics-weeks-11-16) 5. [Phase 4 — Platform Maturity (Weeks 17-24)](#5-phase-4--platform-maturity-weeks-17-24) 6. [New KPIs to Implement](#6-new-kpis-to-implement) 7. [Architecture Reference](#7-architecture-reference) 8. [Code Refactoring Guide](#8-code-refactoring-guide) 9. [Infrastructure & DevOps](#9-infrastructure--devops) 10. [Appendix — SQL Templates](#10-appendix--sql-templates) --- ## 1. Overview & Current State ### What We Have Today ``` Stack: Node.js 20 + Express 4 + MySQL RDS (read-only) + SQLite + Chart.js Deploy: Docker Compose + Netbird VPN Auth: bcrypt + express-session (3 roles: admin/corporate/agente) ``` ### Critical Problems to Solve | Problem | Risk Level | Impact | |---------|-----------|--------| | Direct queries against production RDS | **CRITICAL** | Performance degradation, no historical data | | `queries.js` = 1,786 lines monolith | **HIGH** | Unmaintainable, hard to test | | `admin-bi.js` = 2,133 lines inline HTML/CSS/JS | **HIGH** | No component reuse, can't unit test | | No alerting system | **HIGH** | Issues discovered manually, delayed response | | No compliance/AML monitoring | **CRITICAL** | Regulatory exposure for forex platform | | No ETL pipeline | **HIGH** | No snapshots, no transformation layer | | No export/scheduled reports | **MEDIUM** | Manual report generation for stakeholders | --- ## 2. Phase 1 — Quick Wins (Weeks 1-4) ### 2.1 Alerting System **Goal**: Automated notifications when KPIs breach thresholds. **Stack**: `node-cron` + Slack Webhooks + Nodemailer **File**: `src/alerts/alert-engine.js` ```js // src/alerts/alert-engine.js const cron = require('node-cron'); const { getPool } = require('../db-rds'); const { sendSlack, sendEmail } = require('./channels'); const ALERTS = [ { name: 'volume-spike', cron: '*/30 * * * *', // every 30 min query: ` SELECT COUNT(*) as current_count, (SELECT COUNT(*) / 7 FROM br_transaction_to_usa WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND created_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)) as avg_count FROM br_transaction_to_usa WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 MINUTE) `, condition: (row) => row.current_count > row.avg_count * 1.5 || row.current_count < row.avg_count * 0.5, severity: 'P0', channels: ['slack', 'email'], message: (row) => `Volume Alert: ${row.current_count} txs in last 30min (avg: ${Math.round(row.avg_count)})` }, { name: 'spread-anomaly', cron: '*/15 * * * *', query: ` SELECT AVG(exchange_rate - ptax) as current_spread, (SELECT AVG(exchange_rate - ptax) FROM br_transaction_to_usa WHERE status = 'finalizado' AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) as avg_spread, (SELECT STDDEV(exchange_rate - ptax) FROM br_transaction_to_usa WHERE status = 'finalizado' AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) as stddev_spread FROM br_transaction_to_usa WHERE status = 'finalizado' AND created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE) HAVING COUNT(*) > 0 `, condition: (row) => Math.abs(row.current_spread - row.avg_spread) > 2 * row.stddev_spread, severity: 'P0', channels: ['slack'], message: (row) => `Spread Anomaly: Current ${row.current_spread.toFixed(4)} vs avg ${row.avg_spread.toFixed(4)} (±2σ = ${(row.stddev_spread * 2).toFixed(4)})` }, { name: 'provider-down', cron: '0 */4 * * *', // every 4 hours query: ` SELECT pm.provider, MAX(t.created_at) as last_tx, TIMESTAMPDIFF(HOUR, MAX(t.created_at), NOW()) as hours_since FROM br_payment_methods pm LEFT JOIN br_transaction_to_usa t ON t.payment_method_id = pm.id AND t.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY pm.provider HAVING hours_since > 4 OR last_tx IS NULL `, condition: (rows) => rows.length > 0, severity: 'P0', channels: ['slack', 'email'], message: (rows) => `Provider Down: ${rows.map(r => `${r.provider} (${r.hours_since}h)`).join(', ')}` }, { name: 'large-transaction', cron: '*/5 * * * *', query: ` SELECT t.id_transaction_to_usa, t.amount_usd, c.nome, t.created_at FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta WHERE t.amount_usd > 50000 AND t.created_at >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) AND t.status = 'finalizado' `, condition: (rows) => rows.length > 0, severity: 'P1', channels: ['slack'], message: (rows) => `Large Tx: ${rows.map(r => `$${r.amount_usd.toLocaleString()} from ${r.nome}`).join('; ')}` }, { name: 'failed-tx-spike', cron: '*/15 * * * *', query: ` SELECT SUM(CASE WHEN status IN ('refunded','on-hold') THEN 1 ELSE 0 END) as failed, COUNT(*) as total, SUM(CASE WHEN status IN ('refunded','on-hold') THEN 1 ELSE 0 END) / COUNT(*) * 100 as fail_rate FROM br_transaction_to_usa WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR) HAVING total > 0 `, condition: (row) => row.fail_rate > 5, severity: 'P0', channels: ['slack', 'email'], message: (row) => `Failed Tx Spike: ${row.fail_rate.toFixed(1)}% failure rate (${row.failed}/${row.total}) in last hour` } ]; // Slack channel sender async function sendSlack(webhook, message, severity) { const colors = { P0: '#ef4444', P1: '#f59e0b', P2: '#3b82f6' }; await fetch(webhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attachments: [{ color: colors[severity] || '#94a3b8', title: `BI-CCC Alert [${severity}]`, text: message, ts: Math.floor(Date.now() / 1000) }] }) }); } module.exports = { ALERTS, initAlerts }; ``` **Env vars to add**: ```env SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz ALERT_EMAIL_TO=ops@cambioreal.com SMTP_HOST=smtp.gmail.com SMTP_USER=alerts@cambioreal.com SMTP_PASS=app-password ``` ### 2.2 PDF/Excel Export **Goal**: Add export buttons to all dashboards. **Stack**: Already have `puppeteer` and `exceljs` in querycambio — move to shared dependency. **File**: `src/export/report-generator.js` ```js // src/export/report-generator.js const puppeteer = require('puppeteer'); const ExcelJS = require('exceljs'); async function exportDashboardToPDF(url, sessionCookie) { const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] }); const page = await browser.newPage(); await page.setCookie({ name: 'connect.sid', value: sessionCookie, domain: 'localhost' }); await page.goto(url, { waitUntil: 'networkidle0' }); // Hide nav, expand all sections await page.evaluate(() => { document.querySelector('.console-nav')?.remove(); document.querySelector('header')?.remove(); }); const pdf = await page.pdf({ format: 'A4', landscape: true, printBackground: true, margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' } }); await browser.close(); return pdf; } async function exportToExcel(data, columns, sheetName = 'Report') { const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet(sheetName); sheet.columns = columns.map(col => ({ header: col.label, key: col.key, width: col.width || 15, style: col.style || {} })); // Header styling sheet.getRow(1).font = { bold: true, size: 11 }; sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '7600BE' } }; sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFF' } }; data.forEach(row => sheet.addRow(row)); return workbook; } module.exports = { exportDashboardToPDF, exportToExcel }; ``` **API endpoints to add in `server.js`**: ```js // Export endpoints app.get('/admin/api/export/bi-pdf', requireRole('admin'), async (req, res) => { const { start, end } = req.query; const pdf = await exportDashboardToPDF( `http://localhost:${PORT}/admin/bi?start=${start}&end=${end}`, req.cookies['connect.sid'] ); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename=BI-Report-${start}-${end}.pdf`); res.send(pdf); }); app.get('/admin/api/export/clients-excel', requireRole('admin'), async (req, res) => { const data = await queries.getTopClients(); const workbook = await exportToExcel(data, [ { key: 'nome', label: 'Client', width: 30 }, { key: 'volume_usd', label: 'Volume (USD)', width: 18 }, { key: 'transactions', label: 'Transactions', width: 15 }, { key: 'revenue', label: 'Revenue (USD)', width: 18 }, { key: 'health_score', label: 'Health Score', width: 15 } ], 'Top Clients'); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', 'attachment; filename=top-clients.xlsx'); await workbook.xlsx.write(res); }); ``` ### 2.3 Provider Performance Dashboard **Goal**: Compare all payin/payout providers in one view. **File**: `src/admin-providers.js` **Query**: `src/queries/provider-queries.js` ```sql -- Provider performance (payin) SELECT pm.provider, COUNT(*) as total_transactions, SUM(CASE WHEN t.status = 'finalizado' THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN t.status = 'refunded' THEN 1 ELSE 0 END) as refunded, ROUND(SUM(CASE WHEN t.status = 'finalizado' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as success_rate, ROUND(SUM(t.amount_usd), 2) as total_volume_usd, ROUND(AVG(t.amount_usd), 2) as avg_ticket, ROUND(AVG(TIMESTAMPDIFF(HOUR, t.created_at, t.date_received_br)), 1) as avg_settlement_hours, ROUND(AVG(t.exchange_rate - t.ptax), 4) as avg_spread FROM br_transaction_to_usa t JOIN br_payment_methods pm ON pm.id = t.payment_method_id WHERE t.created_at BETWEEN ? AND ? GROUP BY pm.provider ORDER BY total_volume_usd DESC; -- Provider performance (payout) SELECT ex.provider, COUNT(et.id) as total_transactions, SUM(CASE WHEN et.status = 'completed' THEN 1 ELSE 0 END) as completed, ROUND(SUM(CASE WHEN et.status = 'completed' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as success_rate, ROUND(SUM(et.amount_brl), 2) as total_volume_brl FROM pagamento_br_export_transactions et JOIN pagamento_br_exports ex ON ex.id = et.export_id WHERE et.created_at BETWEEN ? AND ? GROUP BY ex.provider ORDER BY total_volume_brl DESC; ``` ### 2.4 Failed Transaction Analysis **File**: `src/admin-failed-transactions.js` ```sql -- Failed transactions breakdown SELECT t.status, pm.provider, COUNT(*) as count, ROUND(SUM(t.amount_usd), 2) as volume_lost, DATE(t.created_at) as date FROM br_transaction_to_usa t JOIN br_payment_methods pm ON pm.id = t.payment_method_id WHERE t.status IN ('refunded', 'on-hold') AND t.created_at BETWEEN ? AND ? GROUP BY t.status, pm.provider, DATE(t.created_at) ORDER BY date DESC, count DESC; ``` ### 2.5 New KPIs for Corporate Dashboard Add these to `GET /corporate/api/kpis`: ```sql -- Transaction Success Rate SELECT ROUND(SUM(CASE WHEN status = 'finalizado' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as success_rate FROM br_transaction_to_usa WHERE created_at BETWEEN ? AND ?; -- Refund Rate SELECT ROUND(SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as refund_rate FROM br_transaction_to_usa WHERE created_at BETWEEN ? AND ?; -- Gross Take Rate -- (Total Revenue / Total Volume) -- Use existing revenue calculation, divide by total volume ``` --- ## 3. Phase 2 — Data Architecture (Weeks 5-10) ### 3.1 Analytics Database Setup **Recommended**: ClickHouse (best price/performance for analytical queries) **Alternative**: TimescaleDB on PostgreSQL (easier migration path) #### Option A: ClickHouse ```yaml # docker-compose.yml addition services: clickhouse: image: clickhouse/clickhouse-server:24.1 container_name: bi-clickhouse ports: - "8123:8123" # HTTP - "9000:9000" # Native volumes: - clickhouse-data:/var/lib/clickhouse environment: CLICKHOUSE_DB: bi_analytics CLICKHOUSE_USER: bi_user CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} ulimits: nofile: soft: 262144 hard: 262144 volumes: clickhouse-data: ``` #### ClickHouse Schema ```sql -- Transactions fact table (payin) CREATE TABLE bi_analytics.fact_payin ( id UInt64, created_at DateTime, date_received DateTime, id_conta UInt64, client_name String, agente_id Nullable(UInt32), amount_brl Decimal(18,2), amount_usd Decimal(18,2), exchange_rate Decimal(12,6), ptax Decimal(12,6), spread Decimal(12,6), fee Decimal(12,2), iof Decimal(12,2), pfee Decimal(12,2), bonus_valor Decimal(12,2), taxa_cr Decimal(12,2), revenue Decimal(12,2), provider String, status String, tipo_envio String, tipo_pagamento String, is_checkout UInt8, merchant_id Nullable(UInt64), merchant_name Nullable(String), -- Calculated fields spread_pct Decimal(8,4), settlement_hours Nullable(Float32) ) ENGINE = MergeTree() ORDER BY (created_at, id_conta) PARTITION BY toYYYYMM(created_at); -- Transactions fact table (payout) CREATE TABLE bi_analytics.fact_payout ( id UInt64, created_at DateTime, id_conta UInt64, sender_conta_id Nullable(UInt64), client_name String, valor_usd Decimal(18,2), valor_brl Decimal(18,2), ptax Decimal(12,6), cotacao Decimal(12,6), revenue Decimal(12,2), tipo_envio String, provider String, status String ) ENGINE = MergeTree() ORDER BY (created_at, id_conta) PARTITION BY toYYYYMM(created_at); -- Daily snapshots (materialized) CREATE TABLE bi_analytics.daily_metrics ( date Date, product String, -- 'cambioPay', 'cambioPayReverso', 'cambioCheckout' total_transactions UInt32, completed_transactions UInt32, failed_transactions UInt32, total_volume_usd Decimal(18,2), total_volume_brl Decimal(18,2), total_revenue Decimal(18,2), avg_spread Decimal(8,4), avg_ticket_usd Decimal(12,2), unique_clients UInt32, new_clients UInt32, avg_settlement_hours Nullable(Float32) ) ENGINE = MergeTree() ORDER BY (date, product); -- Client health snapshots CREATE TABLE bi_analytics.client_health_daily ( date Date, id_conta UInt64, client_name String, health_score UInt8, churn_risk String, -- 'low','medium','high','critical' lifetime_volume Decimal(18,2), lifetime_revenue Decimal(18,2), lifetime_transactions UInt32, last_transaction_date Nullable(Date), days_since_last_tx UInt16, monthly_velocity Decimal(12,2) ) ENGINE = MergeTree() ORDER BY (date, id_conta) PARTITION BY toYYYYMM(date); ``` ### 3.2 ETL Pipeline **File**: `src/etl/pipeline.js` ```js // src/etl/pipeline.js const cron = require('node-cron'); const mysql = require('../db-rds'); const clickhouse = require('../db-clickhouse'); const logger = require('../etl/logger'); const BATCH_SIZE = 5000; class ETLPipeline { // Run every hour — sync new transactions async syncPayinTransactions() { const jobId = logger.startJob('sync-payin'); try { // Get last synced ID const [lastRow] = await clickhouse.query( 'SELECT max(id) as last_id FROM bi_analytics.fact_payin' ); const lastId = lastRow?.last_id || 0; // Fetch new records from MySQL const [rows] = await mysql.query(` SELECT t.id_transaction_to_usa as id, t.created_at, t.date_received_br as date_received, t.id_conta, c.nome as client_name, ac.agente_id, t.amount_brl, t.amount_usd, t.exchange_rate, t.ptax, (t.exchange_rate - t.ptax) as spread, t.fee, t.iof, COALESCE(t.pfee, 0) as pfee, COALESCE(t.bonus_valor, 0) as bonus_valor, COALESCE(t.taxa_cr, 0) as taxa_cr, pm.provider, t.status, t.tipo_envio, t.tipo_pagamento, CASE WHEN t.cobranca_id IS NOT NULL THEN 1 ELSE 0 END as is_checkout, e.id as merchant_id, e.nome_empresa as merchant_name FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta LEFT JOIN ag_contas ac ON ac.conta_id = t.id_conta LEFT JOIN br_payment_methods pm ON pm.id = t.payment_method_id LEFT JOIN br_cb_cobranca cb ON cb.id = t.cobranca_id LEFT JOIN br_cb_empresas e ON e.id = cb.empresa_id WHERE t.id_transaction_to_usa > ? ORDER BY t.id_transaction_to_usa LIMIT ? `, [lastId, BATCH_SIZE]); if (rows.length === 0) { logger.endJob(jobId, 'success', 0); return; } // Calculate revenue for each row const enriched = rows.map(row => ({ ...row, revenue: calculatePayinRevenue(row), spread_pct: row.ptax > 0 ? ((row.exchange_rate - row.ptax) / row.exchange_rate * 100) : 0, settlement_hours: row.date_received ? (new Date(row.date_received) - new Date(row.created_at)) / 3600000 : null })); // Insert into ClickHouse await clickhouse.insert('bi_analytics.fact_payin', enriched); logger.endJob(jobId, 'success', rows.length); } catch (err) { logger.endJob(jobId, 'error', 0, err.message); throw err; } } // Run daily at 1 AM — build daily aggregates async buildDailyMetrics(date) { const jobId = logger.startJob('daily-metrics'); const targetDate = date || new Date(Date.now() - 86400000).toISOString().split('T')[0]; await clickhouse.query(` INSERT INTO bi_analytics.daily_metrics SELECT toDate(created_at) as date, CASE WHEN is_checkout = 1 THEN 'cambioCheckout' ELSE 'cambioPay' END as product, count() as total_transactions, countIf(status = 'finalizado') as completed_transactions, countIf(status IN ('refunded', 'on-hold')) as failed_transactions, sum(amount_usd) as total_volume_usd, sum(amount_brl) as total_volume_brl, sum(revenue) as total_revenue, avg(spread_pct) as avg_spread, avg(amount_usd) as avg_ticket_usd, uniq(id_conta) as unique_clients, uniqIf(id_conta, /* first tx logic */) as new_clients, avg(settlement_hours) as avg_settlement_hours FROM bi_analytics.fact_payin WHERE toDate(created_at) = '${targetDate}' GROUP BY date, product `); logger.endJob(jobId, 'success'); } } // Revenue calculation (mirrors existing logic) function calculatePayinRevenue(row) { const noFeeProviders = ['ouribank', 'bs2']; const feeAjustado = noFeeProviders.includes(row.provider) ? 0 : (row.fee || 0); if (row.ptax <= 0) return 0; const revenue = (row.amount_brl - feeAjustado) / row.ptax - (row.pfee || 0) - (row.amount_usd + (row.bonus_valor || 0) - (row.taxa_cr || 0)); return Math.round(revenue * 100) / 100; } module.exports = ETLPipeline; ``` **Cron schedule** (add to `server.js` or separate worker): ```js const ETLPipeline = require('./src/etl/pipeline'); const etl = new ETLPipeline(); // Hourly: sync new transactions cron.schedule('0 * * * *', () => etl.syncPayinTransactions()); cron.schedule('5 * * * *', () => etl.syncPayoutTransactions()); // Daily at 1 AM: build aggregates cron.schedule('0 1 * * *', () => etl.buildDailyMetrics()); // Daily at 2 AM: build client health snapshots cron.schedule('0 2 * * *', () => etl.buildClientHealthSnapshots()); ``` ### 3.3 Data Quality Monitoring **File**: `src/etl/data-quality.js` ```js // Run after every ETL job async function validateDataQuality() { const checks = []; // 1. Row count comparison const [mysqlCount] = await mysql.query( 'SELECT COUNT(*) as cnt FROM br_transaction_to_usa WHERE status = "finalizado"' ); const [chCount] = await clickhouse.query( "SELECT count() as cnt FROM bi_analytics.fact_payin WHERE status = 'finalizado'" ); checks.push({ name: 'row-count-payin', expected: mysqlCount.cnt, actual: chCount.cnt, pass: Math.abs(mysqlCount.cnt - chCount.cnt) < 100 // tolerance for in-flight }); // 2. Null checks on critical fields const [nullCheck] = await clickhouse.query(` SELECT countIf(ptax = 0 OR ptax IS NULL) as null_ptax, countIf(amount_brl = 0) as zero_amount, countIf(provider = '') as empty_provider FROM bi_analytics.fact_payin WHERE created_at >= today() - 1 `); checks.push({ name: 'null-critical-fields', pass: nullCheck.null_ptax === 0 && nullCheck.zero_amount === 0 }); // 3. Revenue reconciliation const [revCheck] = await clickhouse.query(` SELECT abs(sum(revenue) - ${expectedRevenue}) / ${expectedRevenue} * 100 as deviation_pct FROM bi_analytics.fact_payin WHERE toDate(created_at) = yesterday() `); checks.push({ name: 'revenue-reconciliation', pass: revCheck.deviation_pct < 1 // less than 1% deviation }); return checks; } ``` --- ## 4. Phase 3 — Compliance & Predictive Analytics (Weeks 11-16) ### 4.1 AML Transaction Monitoring **File**: `src/compliance/aml-monitor.js` **New route**: `GET /admin/compliance` ```sql -- Threshold monitoring: transactions > $3,000 SELECT c.nome, t.id_conta, t.amount_usd, t.created_at, pm.provider FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta JOIN br_payment_methods pm ON pm.id = t.payment_method_id WHERE t.amount_usd > 3000 AND t.status = 'finalizado' AND t.created_at BETWEEN ? AND ? ORDER BY t.amount_usd DESC; -- Structuring detection: multiple transactions just below threshold per client per day SELECT c.nome, t.id_conta, DATE(t.created_at) as tx_date, COUNT(*) as tx_count, SUM(t.amount_usd) as daily_total, AVG(t.amount_usd) as avg_amount, MIN(t.amount_usd) as min_amount, MAX(t.amount_usd) as max_amount FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta WHERE t.status = 'finalizado' AND t.amount_usd BETWEEN 2500 AND 3000 AND t.created_at BETWEEN ? AND ? GROUP BY t.id_conta, c.nome, DATE(t.created_at) HAVING tx_count >= 2 ORDER BY daily_total DESC; -- Unusual velocity: clients with transaction frequency spike SELECT c.nome, t.id_conta, COUNT(*) as recent_count, (SELECT COUNT(*) FROM br_transaction_to_usa t2 WHERE t2.id_conta = t.id_conta AND t2.created_at BETWEEN DATE_SUB(?, INTERVAL 30 DAY) AND ?) / 30 as daily_avg, SUM(t.amount_usd) as recent_volume FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta WHERE t.status = 'finalizado' AND t.created_at BETWEEN ? AND ? GROUP BY t.id_conta, c.nome HAVING recent_count > daily_avg * 3 ORDER BY recent_count DESC; ``` ### 4.2 IOF Reconciliation Dashboard ```sql -- IOF verification: compare calculated vs expected SELECT DATE(t.created_at) as date, pm.provider, COUNT(*) as transactions, SUM(t.iof) as total_iof_charged, SUM(t.amount_usd * t.exchange_rate * 0.0038) as expected_iof_standard, SUM(t.iof) - SUM(t.amount_usd * t.exchange_rate * 0.0038) as iof_delta, ROUND( ABS(SUM(t.iof) - SUM(t.amount_usd * t.exchange_rate * 0.0038)) / NULLIF(SUM(t.amount_usd * t.exchange_rate * 0.0038), 0) * 100, 2 ) as delta_pct FROM br_transaction_to_usa t JOIN br_payment_methods pm ON pm.id = t.payment_method_id WHERE t.status = 'finalizado' AND t.created_at BETWEEN ? AND ? GROUP BY DATE(t.created_at), pm.provider HAVING delta_pct > 0.5 ORDER BY date DESC; ``` ### 4.3 Volume Forecasting **Stack**: Python microservice with Prophet or Node.js with `timeseries-analysis` **File**: `src/analytics/forecast.js` (Node approach) ```js // Simple exponential smoothing for Node.js // For production, consider a Python sidecar with Prophet function exponentialSmoothing(data, alpha = 0.3) { const result = [data[0]]; for (let i = 1; i < data.length; i++) { result.push(alpha * data[i] + (1 - alpha) * result[i - 1]); } return result; } function forecast(historicalData, periodsAhead = 30) { // historicalData = [{ date, value }, ...] const values = historicalData.map(d => d.value); const smoothed = exponentialSmoothing(values); // Simple trend projection const recent = smoothed.slice(-7); const trend = (recent[recent.length - 1] - recent[0]) / recent.length; const lastValue = smoothed[smoothed.length - 1]; const predictions = []; for (let i = 1; i <= periodsAhead; i++) { const lastDate = new Date(historicalData[historicalData.length - 1].date); lastDate.setDate(lastDate.getDate() + i); predictions.push({ date: lastDate.toISOString().split('T')[0], predicted: Math.max(0, lastValue + trend * i), lower: Math.max(0, lastValue + trend * i - lastValue * 0.15), upper: lastValue + trend * i + lastValue * 0.15 }); } return predictions; } module.exports = { forecast }; ``` **API endpoint**: ```js app.get('/admin/api/bi/forecast', requireRole('admin'), async (req, res) => { const { metric = 'volume', days = 30 } = req.query; // Get 90 days of historical data from analytics DB const historical = await clickhouse.query(` SELECT date, sum(total_volume_usd) as value FROM bi_analytics.daily_metrics WHERE date >= today() - 90 GROUP BY date ORDER BY date `); const predictions = forecast(historical, parseInt(days)); res.json({ historical, predictions }); }); ``` ### 4.4 Churn Prediction Model Evolve the static Health Score into a weighted model: ```js function predictChurnRisk(client) { const weights = { recency: 0.30, // days since last transaction frequency: 0.25, // transactions per month trend monetary: 0.20, // volume trend (growing/declining) velocity: 0.15, // rate of change in transaction frequency engagement: 0.10 // product diversity, ticket variation }; // Recency score (0-100, higher = less risk) const recencyScore = Math.max(0, 100 - (client.days_since_last_tx * 2)); // Frequency score: compare last 30d vs previous 30d const freqRatio = client.tx_count_30d / Math.max(client.tx_count_prev_30d, 1); const frequencyScore = Math.min(100, freqRatio * 50); // Monetary score: volume growth const volRatio = client.volume_30d / Math.max(client.volume_prev_30d, 1); const monetaryScore = Math.min(100, volRatio * 50); // Velocity: acceleration of transaction frequency const velocityScore = client.tx_acceleration > 0 ? 70 + client.tx_acceleration * 10 : 30; // Engagement: uses multiple products? const engagementScore = client.products_used * 33; const totalScore = recencyScore * weights.recency + frequencyScore * weights.frequency + monetaryScore * weights.monetary + Math.min(100, velocityScore) * weights.velocity + Math.min(100, engagementScore) * weights.engagement; return { score: Math.round(totalScore), risk: totalScore < 25 ? 'critical' : totalScore < 45 ? 'high' : totalScore < 65 ? 'medium' : 'low', factors: { recency: { score: recencyScore, weight: weights.recency }, frequency: { score: frequencyScore, weight: weights.frequency }, monetary: { score: monetaryScore, weight: weights.monetary }, velocity: { score: Math.min(100, velocityScore), weight: weights.velocity }, engagement: { score: Math.min(100, engagementScore), weight: weights.engagement } } }; } ``` --- ## 5. Phase 4 — Platform Maturity (Weeks 17-24) ### 5.1 Scheduled Report Generation **File**: `src/reports/scheduler.js` ```js const cron = require('node-cron'); const { exportDashboardToPDF, exportToExcel } = require('../export/report-generator'); const { sendEmail } = require('../alerts/channels'); const REPORT_SCHEDULE = [ { name: 'Daily Executive Digest', cron: '0 8 * * 1-5', // 8 AM weekdays recipients: ['exec@cambioreal.com'], generator: async () => { const data = await queries.getDailyDigest(); return { subject: `BI Daily Digest — ${new Date().toLocaleDateString()}`, html: buildDigestHTML(data), attachments: [] // inline, no PDF }; } }, { name: 'Weekly Board Summary', cron: '0 9 * * 1', // Monday 9 AM recipients: ['board@cambioreal.com'], generator: async () => { const end = new Date().toISOString().split('T')[0]; const start = new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0]; const pdf = await exportDashboardToPDF( `http://localhost:3080/admin/bi?start=${start}&end=${end}` ); return { subject: `BI Weekly Report — Week of ${start}`, html: '

Weekly BI report attached.

', attachments: [{ filename: `BI-Weekly-${start}.pdf`, content: pdf }] }; } }, { name: 'Monthly Agent Commissions', cron: '0 10 1 * *', // 1st of month, 10 AM recipients: ['finance@cambioreal.com'], generator: async () => { const data = await queries.getMonthlyCommissions(); const workbook = await exportToExcel(data, commissionColumns, 'Commissions'); const buffer = await workbook.xlsx.writeBuffer(); return { subject: `Agent Commissions — ${new Date().toLocaleDateString('en', { month: 'long', year: 'numeric' })}`, html: '

Monthly commission report attached.

', attachments: [{ filename: 'commissions.xlsx', content: buffer }] }; } } ]; ``` ### 5.2 Agent Commission Integration Move commission calculation from separate scripts (`comissao-valorfx/`, `comissao-d4u/`) into BI-CCC: **New route**: `GET /admin/commissions` **Features**: - Auto-calculate commissions per agent per period - Approval workflow (admin reviews → approves → generates Excel) - Commission dispute tracking - Agent ROI: commission paid vs. revenue generated by agent's clients ```js // src/queries/commission-queries.js // ValorFx (Agent 76) — 50/50 rateio async function calculateValorFxCommission(start, end) { const [rows] = await pool.query(` SELECT t.id_transaction_to_usa, t.created_at, c.nome as client_name, t.amount_brl, t.amount_usd, t.exchange_rate, t.ptax, t.fee, t.iof, t.pfee, t.bonus_valor, t.taxa_cr, pm.provider FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta JOIN ag_contas ac ON ac.conta_id = t.id_conta JOIN br_payment_methods pm ON pm.id = t.payment_method_id WHERE ac.agente_id = 76 AND t.status = 'finalizado' AND t.created_at BETWEEN ? AND ? ORDER BY t.created_at `, [start, end]); return rows.map(row => { const noFeeProviders = ['ouribank', 'bs2']; const feeAdj = noFeeProviders.includes(row.provider) ? 0 : row.fee; const spreadLiqUsd = (row.amount_brl - feeAdj) / row.ptax - row.pfee - row.amount_usd + row.bonus_valor - row.taxa_cr; const iofEficiencia = row.provider === 'RIPPLE' ? row.iof : row.iof * 0.425; const rateio = (iofEficiencia + spreadLiqUsd) / 2; return { ...row, spreadLiqUsd, iofEficiencia, rateio }; }); } // D4U — 20% do spread async function calculateD4UCommission(start, end) { const d4uContas = [257174, 333262, 349175, 352427]; // ... similar structure, 20% of revenue } ``` ### 5.3 Frontend Migration Path **Recommended approach**: Gradual migration using htmx + Alpine.js #### Step 1: Extract UI Template Components ``` src/ ├── components/ │ ├── kpi-card.js // buildKPICard(title, value, change, color) │ ├── chart-wrapper.js // buildChartWrapper(id, type, config) │ ├── data-table.js // buildDataTable(columns, data, options) │ ├── filter-bar.js // buildFilterBar(filters) │ └── layout.js // buildPageLayout(title, nav, content) ``` #### Step 2: Add htmx for Progressive Enhancement ```html
Loading...
``` #### Step 3: Introduce Alpine.js for Client Interactivity ```html
...
...
``` ### 5.4 Multi-Language Support **File**: `src/i18n/index.js` ```js const translations = { en: { 'nav.dashboard': 'Dashboard', 'nav.bi': 'BI Executive', 'nav.clients': 'Clients', 'kpi.volume': 'Volume', 'kpi.transactions': 'Transactions', 'kpi.revenue': 'Revenue', 'kpi.avgTicket': 'Avg Ticket', 'kpi.spread': 'Spread', 'kpi.healthScore': 'Health Score', 'filter.period': 'Period', 'filter.from': 'From', 'filter.to': 'To', 'status.completed': 'Completed', 'status.refunded': 'Refunded', 'status.pending': 'Pending' }, pt: { 'nav.dashboard': 'Painel', 'nav.bi': 'BI Executivo', 'nav.clients': 'Clientes', 'kpi.volume': 'Volume', 'kpi.transactions': 'Transações', 'kpi.revenue': 'Receita', 'kpi.avgTicket': 'Ticket Médio', 'kpi.spread': 'Spread', 'kpi.healthScore': 'Score de Saúde', 'filter.period': 'Período', 'filter.from': 'De', 'filter.to': 'Até', 'status.completed': 'Finalizado', 'status.refunded': 'Estornado', 'status.pending': 'Pendente' } }; function t(key, locale = 'en') { return translations[locale]?.[key] || translations.en[key] || key; } module.exports = { t, translations }; ``` --- ## 6. New KPIs to Implement ### Financial KPIs | KPI | SQL/Formula | Priority | Add To | |-----|------------|----------|--------| | **Net Revenue Retention** | `(Start MRR + Expansion - Contraction - Churn) / Start MRR` | P0 | BI Executive | | **Revenue per Transaction** | `SUM(revenue) / COUNT(*)` | P0 | Corporate | | **Gross Take Rate** | `SUM(revenue) / SUM(volume_usd)` | P0 | BI Executive | | **Provider Cost per $1K** | `SUM(provider_fees) / (SUM(volume) / 1000)` | P1 | Provider Dashboard | | **LTV** | `AVG(monthly_revenue) × AVG(lifespan_months)` | P1 | Client 360 | | **Revenue Concentration (HHI)** | `SUM(client_share^2)` | P1 | BI Executive | | **MRR** | `SUM(revenue) WHERE client active in consecutive months` | P2 | BI Executive | ### Operational KPIs | KPI | SQL | Priority | Add To | |-----|-----|----------|--------| | **Transaction Success Rate** | `SUM(IF status='finalizado',1,0) / COUNT(*)` | P0 | Corporate | | **Avg Settlement Time** | `AVG(TIMESTAMPDIFF(HOUR, created_at, date_received_br))` | P0 | Provider Dashboard | | **Provider Uptime** | `COUNT(DISTINCT HOUR(created_at)) / 24` per provider | P1 | Provider Dashboard | | **Refund Rate** | `SUM(IF status='refunded',1,0) / COUNT(*)` | P1 | Corporate | | **PTAX Deviation** | `AVG(ABS(exchange_rate - ptax) / ptax)` | P1 | BI Executive | | **Peak Hour Analysis** | `GROUP BY HOUR(created_at)` | P2 | Corporate | ### Client KPIs | KPI | Logic | Priority | Add To | |-----|-------|----------|--------| | **Client Activation Rate** | First-tx clients / New signups | P0 | BI Executive | | **Cross-Product Adoption** | Clients using 2+ products / Total active | P1 | BI Executive | | **Time to First Transaction** | `AVG(first_tx - signup)` | P1 | BI Executive | | **Reactivation Rate** | Previously churned → active again | P2 | Client 360 | ### Compliance KPIs | KPI | Logic | Priority | Add To | |-----|-------|----------|--------| | **Threshold Tx Count** | Transactions > $3K, $10K per client per day | P0 | Compliance | | **Structuring Detection** | Multiple txs between $2.5K-$3K same day | P0 | Compliance | | **SAR Filing Rate** | SARs filed per period | P0 | Compliance | | **IOF Reconciliation Delta** | Calculated vs expected IOF | P1 | Compliance | | **Cross-Border Volume per Corridor** | Volume by origin/destination | P1 | Compliance | --- ## 7. Architecture Reference ### Current Architecture ``` Browser → Express (SSR HTML + inline JS/CSS) → MySQL RDS (direct) → SQLite (auth) ``` ### Target Architecture ``` Browser (htmx + Alpine.js + Chart.js) │ ▼ Express API Gateway │ ├── REST API layer (JSON) │ ├── /api/bi/* → BI Service │ ├── /api/clients/* → Client Service │ ├── /api/providers/* → Provider Service │ ├── /api/compliance/* → Compliance Service │ ├── /api/agents/* → Agent Service │ └── /api/export/* → Export Service │ ├── SSR HTML layer (pages) │ └── Uses components/ for reusable UI blocks │ ├── Analytics DB (ClickHouse) │ └── fact tables + daily aggregates + client snapshots │ ├── ETL Worker (node-cron) │ ├── Hourly: sync transactions │ ├── Daily: build aggregates + snapshots │ └── Data quality checks │ ├── Alert Engine (node-cron) │ ├── Volume/spread monitors │ ├── Provider health checks │ └── Channels: Slack + Email │ ├── Report Generator (Puppeteer + ExcelJS) │ ├── Scheduled: daily/weekly/monthly │ └── On-demand: PDF/Excel export │ ├── MySQL RDS (source — read-only via VPN) │ └── SQLite (auth + user preferences) ``` ### New File Structure ``` bi-agentes/ ├── server.js # Route registration + middleware only ├── src/ │ ├── routes/ │ │ ├── bi.routes.js # /admin/bi, /admin/api/bi │ │ ├── client.routes.js # /admin/cliente, /admin/api/clientes │ │ ├── corporate.routes.js # /corporate, /corporate/api │ │ ├── provider.routes.js # /admin/providers (NEW) │ │ ├── compliance.routes.js # /admin/compliance (NEW) │ │ ├── commission.routes.js # /admin/commissions (NEW) │ │ ├── export.routes.js # /admin/api/export (NEW) │ │ ├── agent.routes.js # /dashboard │ │ └── auth.routes.js # /login, /logout │ │ │ ├── services/ │ │ ├── revenue.service.js # Revenue calculation logic │ │ ├── client.service.js # Client health, churn, LTV │ │ ├── provider.service.js # Provider analytics │ │ ├── compliance.service.js # AML, threshold, structuring │ │ ├── commission.service.js # Agent commissions │ │ ├── forecast.service.js # Volume/revenue forecasting │ │ └── metrics.service.js # KPI calculations │ │ │ ├── queries/ │ │ ├── payin.queries.js # br_transaction_to_usa queries │ │ ├── payout.queries.js # pagamento_br queries │ │ ├── client.queries.js # Client-related queries │ │ ├── provider.queries.js # Provider-related queries │ │ ├── compliance.queries.js # AML/compliance queries │ │ └── commission.queries.js # Commission queries │ │ │ ├── components/ │ │ ├── kpi-card.js │ │ ├── chart-wrapper.js │ │ ├── data-table.js │ │ ├── filter-bar.js │ │ ├── alert-badge.js │ │ └── layout.js │ │ │ ├── etl/ │ │ ├── pipeline.js # Main ETL orchestrator │ │ ├── sync-payin.js # Payin transaction sync │ │ ├── sync-payout.js # Payout transaction sync │ │ ├── build-aggregates.js # Daily/weekly/monthly rollups │ │ ├── build-snapshots.js # Client health snapshots │ │ ├── data-quality.js # Validation checks │ │ └── logger.js # ETL job logging │ │ │ ├── alerts/ │ │ ├── alert-engine.js # Alert definitions + scheduler │ │ ├── channels.js # Slack, Email, SMS senders │ │ └── alert-rules.js # Alert configurations │ │ │ ├── export/ │ │ ├── report-generator.js # PDF + Excel generation │ │ └── scheduler.js # Scheduled report delivery │ │ │ ├── i18n/ │ │ ├── index.js # Translation helper │ │ ├── en.json │ │ └── pt.json │ │ │ ├── db-rds.js # MySQL connection (unchanged) │ ├── db-clickhouse.js # ClickHouse connection (NEW) │ ├── db-local.js # SQLite (unchanged) │ ├── auth.js # Auth middleware (unchanged) │ ├── cache.js # Cache layer (unchanged) │ └── ui-template.js # Base template (refactored) │ ├── public/ │ ├── chart.umd.min.js │ ├── htmx.min.js # NEW │ ├── alpine.min.js # NEW │ ├── login.html │ └── assets/ │ ├── data/ │ └── agentes.db │ ├── scripts/ │ ├── seed-admin.js │ ├── seed-agente.js │ ├── migrate-clickhouse.js # NEW: schema setup │ └── backfill-analytics.js # NEW: historical data load │ ├── Dockerfile ├── docker-compose.yml # + clickhouse service ├── docker-entrypoint.sh └── .env ``` --- ## 8. Code Refactoring Guide ### Priority 1: Break up `queries.js` (1,786 lines) Split by domain: ``` queries.js → ├── queries/payin.queries.js (~500 lines) ├── queries/payout.queries.js (~300 lines) ├── queries/client.queries.js (~400 lines) ├── queries/provider.queries.js (~200 lines — NEW) ├── queries/commission.queries.js (~200 lines — from external scripts) └── queries/compliance.queries.js (~200 lines — NEW) ``` **Migration strategy**: Keep `queries.js` as a facade that re-exports from sub-modules. This avoids breaking existing imports. ```js // queries.js (facade — no breaking changes) const payin = require('./queries/payin.queries'); const payout = require('./queries/payout.queries'); const client = require('./queries/client.queries'); module.exports = { ...payin, ...payout, ...client }; ``` ### Priority 2: Extract UI Components from `admin-bi.js` (2,133 lines) Identify repeating patterns and extract to `src/components/`: ```js // BEFORE (admin-bi.js, inline) html += `
${label}
${value}
${change}%
`; // AFTER (components/kpi-card.js) function buildKPICard({ label, value, change, color = 'green' }) { const direction = change > 0 ? 'up' : 'down'; return `
${label}
${value}
${change > 0 ? '+' : ''}${change}%
`; } module.exports = { buildKPICard }; ``` ### Priority 3: Separate CSS into Files Extract inline styles from `ui-template.js` into CSS files: ``` public/css/ ├── variables.css # CSS custom properties (dark/light theme) ├── layout.css # Grid, nav, page structure ├── components.css # Cards, tables, buttons ├── charts.css # Chart containers, legends └── responsive.css # Media queries ``` --- ## 9. Infrastructure & DevOps ### Docker Compose Update ```yaml version: '3.8' services: bi-ccc: build: . container_name: bi-ccc ports: - "3080:3080" environment: - MYSQL_URL=${MYSQL_URL} - USER_MYSQL=${USER_MYSQL} - PW_MYSQL=${PW_MYSQL} - SESSION_SECRET=${SESSION_SECRET} - CLICKHOUSE_URL=http://clickhouse:8123 - CLICKHOUSE_DB=bi_analytics - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL} - ALERT_EMAIL_TO=${ALERT_EMAIL_TO} depends_on: - clickhouse volumes: - ./data:/app/data cap_add: - NET_ADMIN - SYS_RESOURCE clickhouse: image: clickhouse/clickhouse-server:24.1 container_name: bi-clickhouse ports: - "8123:8123" volumes: - clickhouse-data:/var/lib/clickhouse environment: - CLICKHOUSE_DB=bi_analytics - CLICKHOUSE_USER=${CLICKHOUSE_USER} - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} volumes: clickhouse-data: ``` ### New Dependencies ```json { "dependencies": { "node-cron": "^3.0.3", "nodemailer": "^6.9.8", "puppeteer": "^22.0.0", "exceljs": "^4.4.0", "@clickhouse/client": "^0.2.10" } } ``` ### Monitoring Add basic health check endpoint: ```js app.get('/health', (req, res) => { const checks = { mysql: pool ? 'connected' : 'disconnected', clickhouse: chPool ? 'connected' : 'disconnected', uptime: process.uptime(), memory: process.memoryUsage(), cache: cache.stats() }; const healthy = checks.mysql === 'connected'; res.status(healthy ? 200 : 503).json(checks); }); ``` --- ## 10. Appendix — SQL Templates ### A. Net Revenue Retention (NRR) ```sql WITH monthly_revenue AS ( SELECT id_conta, DATE_FORMAT(created_at, '%Y-%m') as month, SUM(revenue) as mrr FROM bi_analytics.fact_payin WHERE status = 'finalizado' GROUP BY id_conta, DATE_FORMAT(created_at, '%Y-%m') ), retention AS ( SELECT a.month as current_month, SUM(a.mrr) as current_mrr, SUM(CASE WHEN b.mrr IS NOT NULL THEN LEAST(a.mrr, b.mrr) ELSE 0 END) as retained, SUM(CASE WHEN b.mrr IS NOT NULL AND a.mrr > b.mrr THEN a.mrr - b.mrr ELSE 0 END) as expansion, SUM(CASE WHEN b.mrr IS NOT NULL AND a.mrr < b.mrr THEN b.mrr - a.mrr ELSE 0 END) as contraction, SUM(CASE WHEN b.mrr IS NULL AND a.mrr IS NOT NULL THEN 0 ELSE b.mrr END) as churned FROM monthly_revenue a LEFT JOIN monthly_revenue b ON a.id_conta = b.id_conta AND b.month = DATE_FORMAT(DATE_SUB(STR_TO_DATE(CONCAT(a.month, '-01'), '%Y-%m-%d'), INTERVAL 1 MONTH), '%Y-%m') GROUP BY a.month ) SELECT current_month, ROUND((retained + expansion - contraction) / NULLIF(retained + contraction, 0) * 100, 1) as nrr_pct FROM retention ORDER BY current_month; ``` ### B. Revenue Concentration (HHI) ```sql SELECT ROUND(SUM(POW(client_share, 2)) * 10000) as hhi_index FROM ( SELECT id_conta, SUM(revenue) / (SELECT SUM(revenue) FROM bi_analytics.fact_payin WHERE created_at BETWEEN ? AND ?) as client_share FROM bi_analytics.fact_payin WHERE created_at BETWEEN ? AND ? AND status = 'finalizado' GROUP BY id_conta ) sub; -- HHI < 1500 = competitive, 1500-2500 = moderate concentration, > 2500 = high concentration ``` ### C. Cross-Product Adoption ```sql SELECT products_used, COUNT(*) as client_count, ROUND(COUNT(*) / SUM(COUNT(*)) OVER() * 100, 1) as pct FROM ( SELECT id_conta, COUNT(DISTINCT product) as products_used FROM ( SELECT id_conta, CASE WHEN is_checkout = 1 THEN 'checkout' ELSE 'cambioPay' END as product FROM bi_analytics.fact_payin WHERE created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY) UNION ALL SELECT id_conta, 'cambioPayReverso' as product FROM bi_analytics.fact_payout WHERE created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY) ) all_products GROUP BY id_conta ) sub GROUP BY products_used ORDER BY products_used; ``` ### D. Structuring Detection ```sql SELECT c.nome as client_name, t.id_conta, DATE(t.created_at) as tx_date, COUNT(*) as tx_count, ROUND(SUM(t.amount_usd), 2) as daily_total, ROUND(AVG(t.amount_usd), 2) as avg_amount, ROUND(STDDEV(t.amount_usd), 2) as stddev_amount, GROUP_CONCAT(ROUND(t.amount_usd, 2) ORDER BY t.created_at) as amounts FROM br_transaction_to_usa t JOIN conta c ON c.id_conta = t.id_conta WHERE t.status = 'finalizado' AND t.amount_usd BETWEEN 2000 AND 3000 AND t.created_at BETWEEN ? AND ? GROUP BY t.id_conta, c.nome, DATE(t.created_at) HAVING tx_count >= 2 AND daily_total > 3000 ORDER BY daily_total DESC; ``` --- ## Checklist de Entregas ### Phase 1 (Weeks 1-4) - [ ] Alert engine (`src/alerts/`) com 5+ regras configuradas - [ ] Slack webhook integrado e testado - [ ] Export PDF/Excel nos dashboards existentes - [ ] Provider Performance dashboard funcional - [ ] Failed Transaction view - [ ] Success Rate + Refund Rate no Corporate Dashboard - [ ] `queries.js` dividido em módulos (facade mantida) ### Phase 2 (Weeks 5-10) - [ ] ClickHouse rodando no Docker Compose - [ ] ETL pipeline sincronizando payin + payout - [ ] Daily aggregates sendo gerados automaticamente - [ ] Client health snapshots diários - [ ] Data quality checks passando - [ ] BI-CCC lendo de ClickHouse (não mais direto do RDS) ### Phase 3 (Weeks 11-16) - [ ] Compliance dashboard com threshold monitoring - [ ] Structuring detection funcional - [ ] IOF reconciliation view - [ ] Volume forecast aparecendo no BI Executive - [ ] Churn prediction substituindo Health Score estático - [ ] NRR, LTV, Take Rate no BI Executive ### Phase 4 (Weeks 17-24) - [ ] Scheduled reports (daily/weekly/monthly) sendo enviados - [ ] Commission module integrado ao BI-CCC - [ ] Revenue forecast para board reporting - [ ] Frontend migrado para components + htmx - [ ] i18n EN/PT-BR funcional - [ ] API documentada - [ ] Audit logging de acesso a dados --- > **Nota**: Este guia assume que o time manterá a stack Node.js/Express. Se houver decisão de migrar para Python (FastAPI) ou Go para o backend de dados, as queries e lógica de negócio se mantêm — muda apenas a camada de serviço.