05/31 Phase 3

This commit is contained in:
2026-05-31 16:18:37 -04:00
parent 9f2a0acc38
commit 1560041cdb
13 changed files with 1152 additions and 10 deletions
+89
View File
@@ -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,
}