persona-finance-tracker-demo/app/api/analytics.py
2025-09-13 17:40:23 +05:30

202 lines
7.2 KiB
Python

"""
Analytics API Blueprint
This module contains API endpoints for financial analytics and reporting.
Provides summary statistics and spending analysis.
"""
from flask import Blueprint, jsonify, request
from sqlalchemy import func
from datetime import datetime, date
from marshmallow import ValidationError
from app.models import Transaction, Category
from app.extensions import db
# Create analytics blueprint
analytics_bp = Blueprint('analytics', __name__)
@analytics_bp.route('/summary', methods=['GET'])
def get_summary():
"""
Get financial summary analytics.
Query Parameters:
- start_date: string (YYYY-MM-DD) - Filter from date (optional)
- end_date: string (YYYY-MM-DD) - Filter to date (optional)
Returns:
- 200: Summary statistics including total income, expenses, and balance
"""
try:
# Parse optional date filters
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
# Base query
query = Transaction.query
# Apply date filters if provided
if start_date:
try:
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
query = query.filter(Transaction.transaction_date >= start_date_obj)
except ValueError:
return jsonify({
'success': False,
'error': 'validation_failed',
'message': 'Invalid start_date format. Use YYYY-MM-DD'
}), 400
if end_date:
try:
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
query = query.filter(Transaction.transaction_date <= end_date_obj)
except ValueError:
return jsonify({
'success': False,
'error': 'validation_failed',
'message': 'Invalid end_date format. Use YYYY-MM-DD'
}), 400
# Calculate summary statistics
total_income = query.filter(Transaction.type == 'income').with_entities(
func.coalesce(func.sum(Transaction.amount), 0)
).scalar() or 0
total_expenses = query.filter(Transaction.type == 'expense').with_entities(
func.coalesce(func.sum(Transaction.amount), 0)
).scalar() or 0
transaction_count = query.count()
# Calculate net balance (income - expenses)
net_balance = float(total_income) - float(total_expenses)
summary_data = {
'total_income': float(total_income),
'total_expenses': float(total_expenses),
'net_balance': net_balance,
'transaction_count': transaction_count
}
return jsonify({
'success': True,
'data': summary_data,
'message': 'Financial summary calculated successfully'
}), 200
except Exception as e:
return jsonify({
'success': False,
'error': 'internal_server_error',
'message': 'Failed to retrieve analytics summary'
}), 500
@analytics_bp.route('/spending-by-category', methods=['GET'])
def get_spending_by_category():
"""
Get spending breakdown by category.
Query Parameters:
- start_date: string (YYYY-MM-DD) - Filter from date (optional)
- end_date: string (YYYY-MM-DD) - Filter to date (optional)
- type: string (income/expense) - Filter by transaction type (default: expense)
Returns:
- 200: Spending breakdown by category
"""
try:
# Parse optional filters
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
transaction_type = request.args.get('type', 'expense')
# Validate transaction type
if transaction_type not in ['income', 'expense']:
return jsonify({
'success': False,
'error': 'validation_failed',
'message': 'Type must be either "income" or "expense"'
}), 400
# Base query - join with Category for category names
query = db.session.query(
Category.id,
Category.name,
Category.color,
func.sum(Transaction.amount).label('total_amount'),
func.count(Transaction.id).label('transaction_count')
).outerjoin(
Transaction,
(Category.id == Transaction.category_id) &
(Transaction.type == transaction_type)
).group_by(Category.id, Category.name, Category.color)
# Apply date filters if provided
if start_date:
try:
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
query = query.filter(Transaction.transaction_date >= start_date_obj)
except ValueError:
return jsonify({
'success': False,
'error': 'validation_failed',
'message': 'Invalid start_date format. Use YYYY-MM-DD'
}), 400
if end_date:
try:
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
query = query.filter(Transaction.transaction_date <= end_date_obj)
except ValueError:
return jsonify({
'success': False,
'error': 'validation_failed',
'message': 'Invalid end_date format. Use YYYY-MM-DD'
}), 400
# Execute query and format results
results = query.all()
# Calculate total for percentage calculation
total_amount = sum(float(result.total_amount or 0) for result in results)
category_data = []
for result in results:
amount = float(result.total_amount or 0)
percentage = (amount / total_amount * 100) if total_amount > 0 else 0
# Only include categories that have transactions
if amount > 0:
category_data.append({
'category_id': result.id,
'category_name': result.name,
'category_color': result.color,
'total_amount': amount,
'transaction_count': result.transaction_count,
'percentage': round(percentage, 2)
})
# Sort by total amount descending
category_data.sort(key=lambda x: x['total_amount'], reverse=True)
return jsonify({
'success': True,
'data': category_data,
'summary': {
'total_amount': total_amount,
'category_count': len(category_data),
'transaction_type': transaction_type
},
'message': f'{transaction_type.title()} breakdown by category calculated successfully'
}), 200
except Exception as e:
return jsonify({
'success': False,
'error': 'internal_server_error',
'message': 'Failed to retrieve spending by category'
}), 500