""" 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