Files
Personal-Finance-Management/app/services/goal_service.py
T
2026-05-31 16:18:37 -04:00

90 lines
2.7 KiB
Python

"""
Goal Service — projected completion, emergency fund calc.
"""
from datetime import date
from dateutil.relativedelta import relativedelta
from app.extensions import db
from app.models.goal import Goal
from app.models.transaction import Transaction
from sqlalchemy import func
def get_projected_completion(goal):
"""
Estimate completion date based on average monthly contribution.
Returns date or None if can't be calculated.
"""
if goal.is_completed:
return goal.completed_at
remaining = float(goal.target_amount) - float(goal.current_amount)
if remaining <= 0:
return date.today()
contributions = goal.contributions.all()
if len(contributions) < 2:
return None
# Average monthly contribution from history
contribs_sorted = sorted(contributions, key=lambda c: c.date)
first = contribs_sorted[0].date
last = contribs_sorted[-1].date
total_contrib = sum(float(c.amount) for c in contribs_sorted)
months_elapsed = (
(last.year - first.year) * 12 + (last.month - first.month)
) or 1
avg_monthly = total_contrib / months_elapsed
if avg_monthly <= 0:
return None
months_needed = remaining / avg_monthly
projected = date.today() + relativedelta(months=int(months_needed) + 1)
return projected
def get_emergency_fund_status():
"""
Calculate emergency fund status:
- 3-month and 6-month expense targets
- Current liquid assets (checking + savings + cash)
"""
from app.models.account import Account
# Average monthly expense (last 3 months)
today = date.today()
three_months_ago = today - relativedelta(months=3)
total_expenses = db.session.query(
func.coalesce(func.sum(Transaction.amount), 0)
).filter(
Transaction.transaction_type == 'expense',
Transaction.date >= three_months_ago,
Transaction.date <= today,
).scalar()
avg_monthly = float(total_expenses) / 3
# Liquid assets
liquid = db.session.query(
func.coalesce(func.sum(Account.balance), 0)
).filter(
Account.is_active == True,
Account.account_type.in_(['checking', 'savings', 'cash']),
).scalar()
liquid = float(liquid)
months_covered = (liquid / avg_monthly) if avg_monthly > 0 else 0
return {
'avg_monthly_expense': avg_monthly,
'liquid_assets': liquid,
'target_3mo': avg_monthly * 3,
'target_6mo': avg_monthly * 6,
'months_covered': round(months_covered, 1),
'pct_3mo': min(round((liquid / (avg_monthly * 3)) * 100, 1), 100) if avg_monthly > 0 else 0,
'pct_6mo': min(round((liquid / (avg_monthly * 6)) * 100, 1), 100) if avg_monthly > 0 else 0,
}