import json import os from datetime import date from dateutil.relativedelta import relativedelta import math DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data")) MORTGAGE_FILE = os.path.join(DATA_DIR, "mortgage.json") def load_mortgage(): if os.path.exists(MORTGAGE_FILE): with open(MORTGAGE_FILE) as f: return json.load(f) return {} def save_mortgage(data): with open(MORTGAGE_FILE, "w") as f: json.dump(data, f, indent=2) def monthly_payment(principal, annual_rate, term_years): if annual_rate == 0: return round(principal / (term_years * 12), 2) r = (annual_rate / 100) / 12 n = term_years * 12 payment = principal * (r * (1 + r) ** n) / ((1 + r) ** n - 1) return round(payment, 2) def amortization_schedule(principal, annual_rate, term_years, start_date_str, extra_monthly=0): r = (annual_rate / 100) / 12 if annual_rate > 0 else 0 n = term_years * 12 base_payment = monthly_payment(principal, annual_rate, term_years) total_payment = base_payment + extra_monthly try: current = date.fromisoformat(start_date_str) except Exception: current = date.today().replace(day=1) schedule = [] balance = principal total_interest = 0 total_principal = 0 for i in range(1, n + 1): if balance <= 0: break interest = round(balance * r, 2) principal_paid = min(round(total_payment - interest, 2), balance) if principal_paid < 0: principal_paid = 0 balance = round(balance - principal_paid, 2) total_interest += interest total_principal += principal_paid schedule.append({ "month": i, "date": current.strftime("%b %Y"), "payment": round(interest + principal_paid, 2), "principal": principal_paid, "interest": interest, "balance": max(balance, 0), }) if balance <= 0: break current = current + relativedelta(months=1) return schedule, round(total_interest, 2), len(schedule) def biweekly_schedule(principal, annual_rate, term_years, start_date_str): base = monthly_payment(principal, annual_rate, term_years) biweekly_payment = round(base / 2, 2) r_biweekly = (annual_rate / 100) / 26 if annual_rate > 0 else 0 try: current = date.fromisoformat(start_date_str) except Exception: current = date.today().replace(day=1) balance = principal total_interest = 0 payments = 0 max_payments = term_years * 26 + 52 while balance > 0 and payments < max_payments: interest = round(balance * r_biweekly, 2) principal_paid = min(round(biweekly_payment - interest, 2), balance) if principal_paid < 0: principal_paid = 0 balance = round(balance - principal_paid, 2) total_interest += interest payments += 1 if balance <= 0: break years = payments / 26 months_total = round(payments / 26 * 12) return round(total_interest, 2), payments, round(years, 1), months_total def current_balance_estimate(principal, annual_rate, term_years, start_date_str, extra_monthly=0): schedule, _, _ = amortization_schedule( principal, annual_rate, term_years, start_date_str, extra_monthly) today = date.today() for row in schedule: try: row_date = date.strptime(row["date"], "%b %Y") if False else None except Exception: pass # Count months elapsed try: start = date.fromisoformat(start_date_str) months_elapsed = (today.year - start.year) * 12 + (today.month - start.month) months_elapsed = max(0, months_elapsed) if months_elapsed < len(schedule): return schedule[months_elapsed]["balance"] return 0 except Exception: return principal