UNPKG

expo-finance-kit

Version:

Native Expo module for Apple FinanceKit - Access financial data from Apple Card and other accounts

232 lines 9 kB
/** * Analytics utilities for Expo Finance Kit * Provides insights and analysis of financial data */ import { CreditDebitIndicator, } from '../ExpoFinanceKit.types'; import { formatMerchantCategory } from './formatters'; /** * Generates spending insights for a set of transactions * @param transactions - Transactions to analyze * @param startDate - Start date for the period * @param endDate - End date for the period * @returns Spending insights */ export function generateSpendingInsights(transactions, startDate, endDate) { const totalSpent = transactions .filter(t => t.creditDebitIndicator === CreditDebitIndicator.Debit) .reduce((sum, t) => sum + t.amount, 0); const totalIncome = transactions .filter(t => t.creditDebitIndicator === CreditDebitIndicator.Credit) .reduce((sum, t) => sum + t.amount, 0); const netCashFlow = totalIncome - totalSpent; const categoriesBreakdown = generateCategoryBreakdown(transactions); const merchantsBreakdown = generateMerchantBreakdown(transactions); const dailyBalances = generateDailyBalances(transactions, startDate, endDate); return { periodStart: startDate.getTime(), periodEnd: endDate.getTime(), totalSpent, totalIncome, netCashFlow, categoriesBreakdown, merchantsBreakdown, dailyBalances, }; } /** * Generates category breakdown from transactions * @param transactions - Transactions to analyze * @returns Array of category breakdowns */ function generateCategoryBreakdown(transactions) { const categoryMap = new Map(); // Only analyze debit transactions const debitTransactions = transactions.filter(t => t.creditDebitIndicator === CreditDebitIndicator.Debit); const totalSpent = debitTransactions.reduce((sum, t) => sum + t.amount, 0); debitTransactions.forEach(transaction => { const category = formatMerchantCategory(transaction.merchantCategoryCode); const existing = categoryMap.get(category) || { amount: 0, count: 0 }; categoryMap.set(category, { amount: existing.amount + transaction.amount, count: existing.count + 1, }); }); return Array.from(categoryMap.entries()) .map(([category, data]) => ({ category, amount: data.amount, percentage: totalSpent > 0 ? (data.amount / totalSpent) : 0, transactionCount: data.count, averageTransaction: data.amount / data.count, })) .sort((a, b) => b.amount - a.amount); } /** * Generates merchant breakdown from transactions * @param transactions - Transactions to analyze * @returns Array of merchant breakdowns */ function generateMerchantBreakdown(transactions) { const merchantMap = new Map(); // Only analyze debit transactions const debitTransactions = transactions.filter(t => t.creditDebitIndicator === CreditDebitIndicator.Debit); const totalSpent = debitTransactions.reduce((sum, t) => sum + t.amount, 0); debitTransactions.forEach(transaction => { const merchantName = transaction.merchantName || 'Unknown Merchant'; const existing = merchantMap.get(merchantName) || { amount: 0, count: 0, lastDate: 0 }; merchantMap.set(merchantName, { amount: existing.amount + transaction.amount, count: existing.count + 1, lastDate: Math.max(existing.lastDate, transaction.transactionDate), }); }); return Array.from(merchantMap.entries()) .map(([merchantName, data]) => ({ merchantName, amount: data.amount, percentage: totalSpent > 0 ? (data.amount / totalSpent) : 0, transactionCount: data.count, lastTransactionDate: data.lastDate, })) .sort((a, b) => b.amount - a.amount) .slice(0, 20); // Top 20 merchants } /** * Generates daily balance data * @param transactions - Transactions to analyze * @param startDate - Start date * @param endDate - End date * @returns Array of daily balances */ function generateDailyBalances(transactions, startDate, endDate) { const dailyData = new Map(); // Initialize all days in the range const currentDate = new Date(startDate); while (currentDate <= endDate) { const dateKey = currentDate.toISOString().split('T')[0]; dailyData.set(dateKey, { credits: 0, debits: 0 }); currentDate.setDate(currentDate.getDate() + 1); } // Aggregate transactions by day transactions.forEach(transaction => { const dateKey = new Date(transaction.transactionDate).toISOString().split('T')[0]; const existing = dailyData.get(dateKey); if (existing) { if (transaction.creditDebitIndicator === CreditDebitIndicator.Credit) { existing.credits += transaction.amount; } else { existing.debits += transaction.amount; } } }); // Calculate running balance let runningBalance = 0; const balances = []; Array.from(dailyData.entries()) .sort(([a], [b]) => a.localeCompare(b)) .forEach(([dateKey, data]) => { runningBalance += data.credits - data.debits; balances.push({ date: new Date(dateKey).getTime(), balance: runningBalance, credits: data.credits, debits: data.debits, }); }); return balances; } /** * Calculates spending trends * @param transactions - Transactions to analyze * @param periodDays - Number of days per period * @returns Spending trend data */ export function calculateSpendingTrends(transactions, periodDays = 30) { const periods = new Map(); transactions.forEach(transaction => { const date = new Date(transaction.transactionDate); const periodIndex = Math.floor((Date.now() - date.getTime()) / (periodDays * 24 * 60 * 60 * 1000)); const periodKey = `Period ${periodIndex + 1}`; const existing = periods.get(periodKey) || { spending: 0, income: 0, count: 0 }; if (transaction.creditDebitIndicator === CreditDebitIndicator.Credit) { existing.income += transaction.amount; } else { existing.spending += transaction.amount; } existing.count++; periods.set(periodKey, existing); }); return Array.from(periods.entries()) .map(([period, data]) => ({ period, spending: data.spending, income: data.income, transactionCount: data.count, })) .reverse(); // Most recent first } /** * Finds unusual transactions * @param transactions - Transactions to analyze * @param stdDevMultiplier - Number of standard deviations for outlier detection * @returns Array of unusual transactions */ export function findUnusualTransactions(transactions, stdDevMultiplier = 2) { if (transactions.length < 3) return []; // Calculate mean and standard deviation const amounts = transactions.map(t => t.amount); const mean = amounts.reduce((sum, amount) => sum + amount, 0) / amounts.length; const variance = amounts.reduce((sum, amount) => { const diff = amount - mean; return sum + (diff * diff); }, 0) / amounts.length; const stdDev = Math.sqrt(variance); const threshold = mean + (stdDev * stdDevMultiplier); return transactions.filter(t => t.amount > threshold); } /** * Calculates savings rate * @param income - Total income * @param expenses - Total expenses * @returns Savings rate as a percentage */ export function calculateSavingsRate(income, expenses) { if (income <= 0) return 0; const savings = income - expenses; return (savings / income) * 100; } /** * Predicts future balance based on historical data * @param transactions - Historical transactions * @param currentBalance - Current balance * @param daysToPredict - Number of days to predict * @returns Predicted balance */ export function predictFutureBalance(transactions, currentBalance, daysToPredict) { if (transactions.length === 0) return currentBalance; // Calculate average daily change const sortedTransactions = [...transactions].sort((a, b) => a.transactionDate - b.transactionDate); const firstDate = new Date(sortedTransactions[0].transactionDate); const lastDate = new Date(sortedTransactions[sortedTransactions.length - 1].transactionDate); const daysDiff = Math.max(1, (lastDate.getTime() - firstDate.getTime()) / (24 * 60 * 60 * 1000)); const totalChange = transactions.reduce((sum, t) => { if (t.creditDebitIndicator === CreditDebitIndicator.Credit) { return sum + t.amount; } else { return sum - t.amount; } }, 0); const avgDailyChange = totalChange / daysDiff; return currentBalance + (avgDailyChange * daysToPredict); } //# sourceMappingURL=analytics.js.map