Dashboard de Analytics
Este guia explica como puxar dados do Ozzie para um dashboard de BI ou analytics. Você pode agregar dados de transações, rastrear aderência ao plano, medir taxas de conclusão de metas e construir visualizações de relatórios.
Quais dados estão disponíveis
| Tipo de dado | Endpoint | Campos principais |
|---|---|---|
| Transações | GET /users/{id}/transactions | valor, categoria, estabelecimento, data, fonte |
| Alocações do plano | POST /users/{id}/plan | % de necessidades/desejos/poupança/dívida |
| Progresso da meta | GET /users/{id}/goals | target_amount, starting_amount, next_move_date |
| Conclusão de money move | GET /users/{id}/money-moves | status por ciclo, taxas de conclusão de tarefas |
| Intake financeiro | GET /users/{id}/financial-intake | renda, despesas, snapshot de dívida |
Métricas principais que você pode calcular
| Métrica | Como calcular |
|---|---|
| Gastos mensais por categoria | Somar amount_cents agrupado por category para transações do mês atual |
| Taxa de poupança | (monthly_net_income - total_expenses) / monthly_net_income * 100 do intake |
| % de progresso da meta | (current_amount - starting_amount) / (target_amount - starting_amount) * 100 |
| Taxa de conclusão de money move | ciclos concluídos / total de ciclos * 100 por usuário ou em todos os usuários |
| Poupança média por ciclo | Soma de amount_cents para tarefas do marcadas como done, dividido pela contagem de ciclos |
| Aderência ao plano | Comparar categorias de gastos reais das transações vs. alvos de allocations do plano |
Estratégia de paginação para exportação em massa
O Ozzie usa paginação baseada em cursor em todos os endpoints de lista. Para uma exportação completa de dados, itere até has_more ser false:
export async function fetchAllPages(path, params = {}) {
const allItems = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
const queryParams = new URLSearchParams({ limit: '50', ...params });
if (cursor) queryParams.set('cursor', cursor);
const { data, pagination } = await ozzieRequest('GET', `${path}?${queryParams}`);
allItems.push(...data);
hasMore = pagination.has_more;
cursor = pagination.next_cursor;
}
return allItems;
}
Gastos mensais por categoria
async function getMonthlySpendingByCategory(ozzieUserId, year, month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = new Date(year, month, 0).toISOString().split('T')[0];
const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
start_date: startDate,
end_date: endDate,
});
const byCategory = transactions.reduce((acc, tx) => {
const category = tx.category ?? 'Sem categoria';
acc[category] = (acc[category] ?? 0) + tx.amount_cents;
return acc;
}, {});
return Object.entries(byCategory)
.map(([category, cents]) => ({ category, amount: cents / 100 }))
.sort((a, b) => b.amount - a.amount);
}
Taxa de poupança ao longo do tempo
async function getSavingsRateTrend(ozzieUserIds, monthsBack = 6) {
const results = [];
for (let i = 0; i < monthsBack; i++) {
const date = new Date();
date.setMonth(date.getMonth() - i);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const label = `${year}-${String(month).padStart(2, '0')}`;
let totalSaved = 0;
let totalIncome = 0;
for (const userId of ozzieUserIds) {
const { data: intake } = await ozzieRequest('GET', `/users/${userId}/financial-intake`)
.catch(() => ({ data: null }));
if (!intake) continue;
const monthTransactions = await fetchAllPages(`/users/${userId}/transactions`, {
start_date: `${year}-${String(month).padStart(2, '0')}-01`,
});
const spent = monthTransactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const income = intake.monthly_net_income;
const saved = Math.max(0, income - spent);
totalIncome += income;
totalSaved += saved;
}
results.push({
month: label,
savings_rate: totalIncome > 0 ? ((totalSaved / totalIncome) * 100).toFixed(1) : '0.0',
total_saved: totalSaved.toFixed(2),
});
}
return results.reverse();
}
Exportação em massa para todos os usuários
// scripts/export-all.js — executar como job noturno
async function exportAll() {
const users = await db.users.findAll({ where: { ozzie_user_id: { not: null } } });
const results = [];
const errors = [];
for (const user of users) {
try {
const data = await exportUserFinancialData(user.ozzie_user_id);
results.push({ internal_user_id: user.id, ...data });
} catch (err) {
errors.push({ user_id: user.id, error: err.message });
}
await new Promise(r => setTimeout(r, 100)); // Delay amigável com o rate limit
}
writeFileSync(`exports/ozzie-${Date.now()}.json`, JSON.stringify(results, null, 2));
}
Para bases de usuários muito grandes (10k+ usuários), execute exportações em lotes paralelos de 10–20 usuários, respeitando seu rate limit. Verifique os headers de resposta X-RateLimit-Remaining para evitar atingir o limite.
Gráficos sugeridos
Detalhamento de gastos — gráfico de pizza ou donut
Use os dados de getMonthlySpendingByCategory(). Codifique por cores por categoria. Sobreponha um anel pontilhado mostrando os alvos de allocations do plano para mostrar orçamento vs. real.
Tendência da taxa de poupança — gráfico de linhas
Use os dados de getSavingsRateTrend() ao longo de 6–12 meses. Adicione uma linha tracejada horizontal na target_savings_rate do plano.
Progresso da meta — barra de progresso horizontal
Use os dados de getGoalProgress(). Mostre porcentagem, valor restante e data projetada do plan.key_metrics.projected_goal_date.
Taxa de conclusão de money move — gauge ou barra
Use os dados de getMoneyMoveCompletionRate(). Para um dashboard voltado ao usuário, mostre um contador de sequência (ciclos consecutivos concluídos) para incentivar consistência.