Initial commit
This commit is contained in:
commit
983cee0320
322 changed files with 57174 additions and 0 deletions
198
app/routes/budget.py
Normal file
198
app/routes/budget.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"""
|
||||
Budget Alerts API
|
||||
Provides budget status, alerts, and notification management
|
||||
Security: All queries filtered by user_id
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from app.models import Category, Expense
|
||||
from app import db
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func
|
||||
|
||||
bp = Blueprint('budget', __name__, url_prefix='/api/budget')
|
||||
|
||||
|
||||
@bp.route('/status', methods=['GET'])
|
||||
@login_required
|
||||
def get_budget_status():
|
||||
"""
|
||||
Get budget status for all user categories and overall monthly budget
|
||||
Security: Only returns current user's data
|
||||
|
||||
Returns:
|
||||
- overall: Total spending vs monthly budget
|
||||
- categories: Per-category budget status
|
||||
- alerts: Active budget alerts
|
||||
"""
|
||||
# Get current month date range
|
||||
now = datetime.utcnow()
|
||||
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Calculate overall monthly spending - Security: filter by user_id
|
||||
total_spent = db.session.query(func.sum(Expense.amount)).filter(
|
||||
Expense.user_id == current_user.id,
|
||||
Expense.date >= start_of_month
|
||||
).scalar() or 0.0
|
||||
|
||||
overall_status = {
|
||||
'spent': float(total_spent),
|
||||
'budget': current_user.monthly_budget or 0,
|
||||
'remaining': (current_user.monthly_budget or 0) - float(total_spent),
|
||||
'percentage': 0 if not current_user.monthly_budget else round((float(total_spent) / current_user.monthly_budget) * 100, 1),
|
||||
'alert_level': 'none'
|
||||
}
|
||||
|
||||
# Determine overall alert level
|
||||
if current_user.monthly_budget and current_user.monthly_budget > 0:
|
||||
if overall_status['percentage'] >= 100:
|
||||
overall_status['alert_level'] = 'exceeded'
|
||||
elif overall_status['percentage'] >= 90:
|
||||
overall_status['alert_level'] = 'danger'
|
||||
elif overall_status['percentage'] >= 80:
|
||||
overall_status['alert_level'] = 'warning'
|
||||
|
||||
# Get category budgets - Security: filter by user_id
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
category_statuses = []
|
||||
active_alerts = []
|
||||
|
||||
for category in categories:
|
||||
if category.monthly_budget and category.monthly_budget > 0:
|
||||
status = category.get_budget_status()
|
||||
category_statuses.append({
|
||||
'category_id': category.id,
|
||||
'category_name': category.name,
|
||||
'category_color': category.color,
|
||||
'category_icon': category.icon,
|
||||
**status
|
||||
})
|
||||
|
||||
# Add to alerts if over threshold
|
||||
if status['alert_level'] in ['warning', 'danger', 'exceeded']:
|
||||
active_alerts.append({
|
||||
'category_id': category.id,
|
||||
'category_name': category.name,
|
||||
'category_color': category.color,
|
||||
'alert_level': status['alert_level'],
|
||||
'percentage': status['percentage'],
|
||||
'spent': status['spent'],
|
||||
'budget': status['budget'],
|
||||
'remaining': status['remaining']
|
||||
})
|
||||
|
||||
# Sort alerts by severity
|
||||
alert_order = {'exceeded': 0, 'danger': 1, 'warning': 2}
|
||||
active_alerts.sort(key=lambda x: (alert_order[x['alert_level']], -x['percentage']))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'overall': overall_status,
|
||||
'categories': category_statuses,
|
||||
'alerts': active_alerts,
|
||||
'alert_count': len(active_alerts)
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/weekly-summary', methods=['GET'])
|
||||
@login_required
|
||||
def get_weekly_summary():
|
||||
"""
|
||||
Get weekly spending summary for notification
|
||||
Security: Only returns current user's data
|
||||
|
||||
Returns:
|
||||
- week_total: Total spent this week
|
||||
- daily_average: Average per day
|
||||
- top_category: Highest spending category
|
||||
- comparison: vs previous week
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
week_start = now - timedelta(days=now.weekday()) # Monday
|
||||
week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
prev_week_start = week_start - timedelta(days=7)
|
||||
|
||||
# Current week spending - Security: filter by user_id
|
||||
current_week_expenses = Expense.query.filter(
|
||||
Expense.user_id == current_user.id,
|
||||
Expense.date >= week_start
|
||||
).all()
|
||||
|
||||
week_total = sum(e.amount for e in current_week_expenses)
|
||||
daily_average = week_total / max(1, (now - week_start).days + 1)
|
||||
|
||||
# Previous week for comparison
|
||||
prev_week_expenses = Expense.query.filter(
|
||||
Expense.user_id == current_user.id,
|
||||
Expense.date >= prev_week_start,
|
||||
Expense.date < week_start
|
||||
).all()
|
||||
|
||||
prev_week_total = sum(e.amount for e in prev_week_expenses)
|
||||
change_percent = 0
|
||||
if prev_week_total > 0:
|
||||
change_percent = ((week_total - prev_week_total) / prev_week_total) * 100
|
||||
|
||||
# Find top category
|
||||
category_totals = {}
|
||||
for expense in current_week_expenses:
|
||||
if expense.category:
|
||||
category_totals[expense.category.name] = category_totals.get(expense.category.name, 0) + expense.amount
|
||||
|
||||
top_category = max(category_totals.items(), key=lambda x: x[1]) if category_totals else (None, 0)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'week_total': float(week_total),
|
||||
'daily_average': float(daily_average),
|
||||
'previous_week_total': float(prev_week_total),
|
||||
'change_percent': round(change_percent, 1),
|
||||
'top_category': top_category[0] if top_category[0] else 'None',
|
||||
'top_category_amount': float(top_category[1]),
|
||||
'expense_count': len(current_week_expenses),
|
||||
'week_start': week_start.isoformat(),
|
||||
'currency': current_user.currency
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/category/<int:category_id>/budget', methods=['PUT'])
|
||||
@login_required
|
||||
def update_category_budget(category_id):
|
||||
"""
|
||||
Update budget settings for a category
|
||||
Security: Verify category belongs to current user
|
||||
"""
|
||||
# Security check: ensure category belongs to current user
|
||||
category = Category.query.filter_by(id=category_id, user_id=current_user.id).first()
|
||||
|
||||
if not category:
|
||||
return jsonify({'success': False, 'message': 'Category not found'}), 404
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
try:
|
||||
if 'monthly_budget' in data:
|
||||
budget = float(data['monthly_budget']) if data['monthly_budget'] else None
|
||||
if budget is not None and budget < 0:
|
||||
return jsonify({'success': False, 'message': 'Budget cannot be negative'}), 400
|
||||
category.monthly_budget = budget
|
||||
|
||||
if 'budget_alert_threshold' in data:
|
||||
threshold = float(data['budget_alert_threshold'])
|
||||
if threshold < 0.5 or threshold > 2.0:
|
||||
return jsonify({'success': False, 'message': 'Threshold must be between 0.5 (50%) and 2.0 (200%)'}), 400
|
||||
category.budget_alert_threshold = threshold
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Budget updated successfully',
|
||||
'category': category.to_dict()
|
||||
})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'message': f'Invalid data: {str(e)}'}), 400
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'Error updating budget: {str(e)}'}), 500
|
||||
Loading…
Add table
Add a link
Reference in a new issue