import json import os from datetime import date import math from storage import load_json, save_json DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data")) DEBTS_FILE = os.path.join(DATA_DIR, "debts.json") DEBT_LOG_FILE = os.path.join(DATA_DIR, "debt_log.json") DEBT_CATEGORIES = ["Credit Cards", "BNPL", "Medical Payment Plans"] BNPL_FREQUENCIES = { "biweekly": 14, "monthly": 30, "4-payment": 14, } def simulate_payoff(debts, extra_monthly, method="avalanche"): import copy from datetime import date from dateutil.relativedelta import relativedelta # Only credit cards and medical debts with a balance active = [d for d in debts if not d.get("is_bnpl") and d.get("active", True) and float(d.get("current_balance", 0)) > 0] if not active: return [], 0, 0, 0 # Build working copies working = [] for d in active: working.append({ "id": d["id"], "name": d["name"], "owner": d.get("owner", ""), "balance": float(d.get("current_balance", 0)), "apr": float(d.get("apr", 0)), "minimum": float(d.get("minimum", 0)), "paid_off_month": None, "total_interest": 0, "total_paid": 0, }) total_minimum = sum(w["minimum"] for w in working) extra = max(extra_monthly - total_minimum, 0) current_month = date.today().replace(day=1) total_interest = 0 months = 0 max_months = 600 while any(w["balance"] > 0 for w in working) and months < max_months: months += 1 current_month_label = current_month.strftime("%b %Y") # Sort target debt unpaid = [w for w in working if w["balance"] > 0] if method == "avalanche": unpaid.sort(key=lambda x: -x["apr"]) else: unpaid.sort(key=lambda x: x["balance"]) # Apply interest and minimums for w in unpaid: if w["balance"] <= 0: continue monthly_rate = (w["apr"] / 100) / 12 interest = round(w["balance"] * monthly_rate, 2) w["balance"] = round(w["balance"] + interest, 2) w["total_interest"] += interest total_interest += interest payment = min(w["minimum"], w["balance"]) w["balance"] = round(w["balance"] - payment, 2) w["total_paid"] += payment # Apply extra to target remaining_extra = extra for w in unpaid: if w["balance"] <= 0: continue extra_payment = min(remaining_extra, w["balance"]) w["balance"] = round(w["balance"] - extra_payment, 2) w["total_paid"] += extra_payment remaining_extra -= extra_payment if remaining_extra <= 0: break # Roll over freed minimums as extra for next debt for w in working: if w["balance"] <= 0 and w["paid_off_month"] is None: w["paid_off_month"] = current_month_label extra += w["minimum"] from datetime import timedelta current_month = (current_month.replace(day=28) + timedelta(days=4)).replace(day=1) return working, round(total_interest, 2), months, round(total_minimum + extra_monthly, 2) def bnpl_payoff_date(payments_made, total_payments, frequency_days): from datetime import date, timedelta today = date.today() remaining = total_payments - payments_made if remaining <= 0: return None, 0 days = remaining * frequency_days payoff = today + timedelta(days=days) return payoff.strftime("%b %Y"), remaining OWNER_COLORS = { "Bonna": "#D4B8C5", "Tony": "#A0B8C5", "Both": "#B8A8D4", } def load_debts(): return load_json(DEBTS_FILE, []) def save_debts(debts): save_json(DEBTS_FILE, debts) def load_debt_log(): return load_json(DEBT_LOG_FILE, {}) def save_debt_log(log): save_json(DEBT_LOG_FILE, log) def month_key(year, month): return f"{year}-{month:02d}" def monthly_interest(balance, apr): if not apr or apr <= 0: return 0 return round(balance * (apr / 100) / 12, 2) def payoff_months(balance, apr, monthly_payment, monthly_charges=0): if monthly_payment <= 0: return None if apr and apr > 0: monthly_rate = (apr / 100) / 12 else: monthly_rate = 0 b = balance months = 0 while b > 0 and months < 600: b = b + (b * monthly_rate) + monthly_charges - monthly_payment months += 1 if monthly_charges == 0 and b >= balance: return None return months if months < 600 else None def get_debt_month(debt_id, year, month): log = load_debt_log() mk = month_key(year, month) return log.get(mk, {}).get(debt_id, {}) def update_debt_month(debt_id, year, month, data): log = load_debt_log() mk = month_key(year, month) log.setdefault(mk, {})[debt_id] = data save_debt_log(log) def compute_ending_balance(debt, year, month): mk = month_key(year, month) log = load_debt_log() entry = log.get(mk, {}).get(debt["id"], {}) starting = entry.get("starting_balance", debt.get("current_balance", 0)) interest = entry.get("interest") if entry.get("interest") is not None else monthly_interest(starting, debt.get("apr", 0)) new_charges = entry.get("new_charges", 0) payment = entry.get("payment", 0) ending = round(float(starting) + float(interest) + float(new_charges) - float(payment), 2) return max(ending, 0)