90 lines
2.7 KiB
Python
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,
|
|
}
|