Skip to main content

Analytics Dashboard

This guide covers pulling Ozzie data into a BI or analytics dashboard. You can aggregate parsed transaction data, track plan adherence, measure money move completion rates, and build reporting views β€” either for your own internal use or as a customer-facing product feature.


What data is available​

Your application stores the results of ozzie-openapi calls. The data available for analytics depends on what you've collected and stored:

Data typeSourceKey fields
Parsed transactionsPOST /v1/transactions/parse responseamount_cents, category, envelope, transaction_date
Plan allocationsPOST /v1/users/{id}/plan/generate responseenvelope allocated_pct, allocated_amount, plan_tier
Money move completionYour database (tracked by your app)status per cycle, task completion rates, amount_cents per do task
Financial intakeGET /v1/users/{id}/financial-intakemonthly_income, monthly_expenses, monthly_surplus

Key metrics you can compute​

MetricHow to compute
Monthly spending by categorySum amount_cents grouped by category for stored transactions in the current month
Savings rate(monthly_income - monthly_expenses) / monthly_income * 100 from stored intake
Goal progress %Sum of do task amount_cents completed Γ· goal target
Money move completion rateCompleted cycles Γ· total cycles Γ— 100
Average savings per cycleSum of amount_cents for completed do tasks Γ· cycle count
Plan adherenceCompare stored transaction totals per envelope vs. plan allocated_amount

Sample queries using stored data​

Monthly spending by category​

async function getMonthlySpendingByCategory(userId, year, month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of month

// Query your database for stored transaction records
const transactions = await db.transactions.findAll({
where: {
user_id: userId,
transaction_date: { gte: startDate, lte: endDate },
}
});

// Group and sum by category
const byCategory = transactions.reduce((acc, tx) => {
const category = tx.category ?? 'Uncategorized';
acc[category] = (acc[category] ?? 0) + tx.amount_cents;
return acc;
}, {});

// Convert cents to dollars and sort descending
return Object.entries(byCategory)
.map(([category, cents]) => ({ category, amount: cents / 100 }))
.sort((a, b) => b.amount - a.amount);
}

// Example output:
// [
// { category: "food", amount: 487.50 },
// { category: "transport", amount: 210.00 },
// { category: "housing", amount: 1200.00 },
// ]

Savings rate over time​

async function getSavingsRateTrend(userIds, 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 userIds) {
// Load stored intake from your database
const intake = await db.intakes.findLatest(userId);
if (!intake) continue;

// Load stored transactions for this month
const transactions = await db.transactions.findAll({
where: {
user_id: userId,
transaction_date: { gte: `${year}-${String(month).padStart(2, '0')}-01` },
}
});

const spent = transactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const income = intake.monthly_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(); // Oldest first
}

Money move completion rate​

async function getMoneyMoveCompletionRate(userId) {
// Query your stored money move records
const allMoves = await db.moneyMoves.findAll({ where: { user_id: userId } });

const terminal = allMoves.filter(m => ['completed', 'skipped'].includes(m.status));
const completed = allMoves.filter(m => m.status === 'completed');

if (terminal.length === 0) return { completion_rate: null, cycles: 0 };

return {
total_cycles: terminal.length,
completed_cycles: completed.length,
skipped_cycles: terminal.length - completed.length,
completion_rate: ((completed.length / terminal.length) * 100).toFixed(1),
};
}

Plan adherence by envelope​

async function getEnvelopeAdherence(userId, year, month) {
// Load the user's stored plan
const plan = await db.plans.findLatest(userId);
if (!plan) return null;

// Load stored transactions for this month
const transactions = await db.transactions.findAll({
where: {
user_id: userId,
transaction_date: { gte: `${year}-${String(month).padStart(2, '0')}-01` },
}
});

// Sum spending by envelope
const spentByEnvelope = transactions.reduce((acc, tx) => {
if (tx.envelope) {
acc[tx.envelope] = (acc[tx.envelope] ?? 0) + tx.amount_cents;
}
return acc;
}, {});

// Compare against plan allocations
return Object.entries(plan.envelopes).map(([envelope, allocation]) => ({
envelope,
allocated_amount: allocation.allocated_amount,
spent_amount: (spentByEnvelope[envelope] ?? 0) / 100,
remaining: allocation.allocated_amount - (spentByEnvelope[envelope] ?? 0) / 100,
adherence_pct: allocation.allocated_amount > 0
? ((1 - (spentByEnvelope[envelope] ?? 0) / 100 / allocation.allocated_amount) * 100).toFixed(1)
: null,
}));
}

Refreshing intelligence data​

When you need fresh AI-computed data for analytics (e.g., regenerating a plan or computing current envelope allocations), call ozzie-openapi and store the result:

// Refresh envelope allocations for a user
async function refreshFinancialProfile(userId) {
const user = await db.users.findById(userId);

const { data: profile } = await ozzieRequest(
'GET',
`/users/${user.ozzie_user_id}/financial-profile?personality_type=${user.personality_type}`
);

await db.financialProfiles.upsert({
user_id: userId,
data: profile,
refreshed_at: new Date(),
});

return profile;
}

Suggested charts​

Spending breakdown β€” pie or donut chart​

Use getMonthlySpendingByCategory() data:

Category | Amount | % of Total
---------------|---------|------------
food | $487 | 27.1%
housing | $1,200 | 66.7%
transport | $210 | 11.7%
entertainment | $134 | 7.5%

Chart type: Pie or donut. Color-code by category or by envelope.


Plan adherence β€” grouped bar chart​

Use getEnvelopeAdherence() data:

Envelope | Allocated | Spent | Remaining
---------------|-----------|-------|----------
daily_living | $525 | $410 | $115
lifestyle | $300 | $180 | $120
people | $375 | $90 | $285

Chart type: Grouped bar chart comparing allocated vs. spent per envelope.


Savings rate trend β€” line chart​

Use getSavingsRateTrend() data over 6–12 months.

Chart type: Line chart with monthly x-axis. Add a horizontal dashed line at the plan's implied savings target rate.


Money move completion β€” bar or gauge chart​

Use getMoneyMoveCompletionRate() data:

Completion Rate: 78% (14 completed / 18 total cycles)

Chart type: Gauge or simple stat card. For a user-facing dashboard, show a streak counter (consecutive completed cycles) to encourage consistency.