202 lines
7.2 KiB
Python
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
|