05/31 Phase 3
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user