diff --git a/backend/backend/backend/backend/hoa.db b/backend/backend/backend/backend/hoa.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/backend/backend/backend/hoa_app.db b/backend/backend/backend/backend/hoa_app.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/backend/frontend/src/Forecasting.tsx b/backend/backend/frontend/src/Forecasting.tsx index d089151..4d1a4b0 100644 --- a/backend/backend/frontend/src/Forecasting.tsx +++ b/backend/backend/frontend/src/Forecasting.tsx @@ -378,9 +378,9 @@ const Forecasting: React.FC = () => { smooth: true, }; // Add a markLine to show the boundary between actual and projected data - // Only add the markLine if we're viewing the current year and have data beyond the current month - console.log('Debug markLine:', { year, currentYear, chartDataLength: chartData.length, currentMonth, shouldShow: year === currentYear && chartData.length > currentMonth + 1 }); - const markLine = (year === currentYear && chartData.length > currentMonth + 1) ? { + // Only add the markLine if we're viewing the current year and the current month is within the data range + console.log('Debug markLine:', { year, currentYear, chartDataLength: chartData.length, currentMonth, shouldShow: year === currentYear && currentMonth < chartData.length }); + const markLine = (year === currentYear && currentMonth < chartData.length) ? { symbol: 'none', lineStyle: { color: '#666', diff --git a/backend/backend/hoa.db b/backend/backend/hoa.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/backend/hoa_app.db b/backend/backend/hoa_app.db index 5ba6607..14dd5a7 100644 Binary files a/backend/backend/hoa_app.db and b/backend/backend/hoa_app.db differ diff --git a/backend/backend/hoa_app/__pycache__/main.cpython-311.pyc b/backend/backend/hoa_app/__pycache__/main.cpython-311.pyc index df4923a..391fdf3 100644 Binary files a/backend/backend/hoa_app/__pycache__/main.cpython-311.pyc and b/backend/backend/hoa_app/__pycache__/main.cpython-311.pyc differ diff --git a/backend/backend/hoa_app/main.py b/backend/backend/hoa_app/main.py index 9b9ca68..d8d98dc 100644 --- a/backend/backend/hoa_app/main.py +++ b/backend/backend/hoa_app/main.py @@ -8,7 +8,7 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.exc import IntegrityError from .schemas import BucketCreate, BucketRead, AccountCreate, AccountRead, TransactionCreate, TransactionRead, CashFlowCreate, CashFlowRead, ForecastRequest, ForecastResponse, ForecastPoint, ReportRequest, ReportResponse, ReportEntry, CashFlowCategoryCreate, CashFlowCategoryRead, CashFlowEntryCreate, CashFlowEntryRead from datetime import datetime, timedelta, date as date_cls -from sqlalchemy import and_, func +from sqlalchemy import and_, func, or_ from .logging_config import setup_logging, DEBUG_LOGGING import logging import os @@ -669,6 +669,8 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge # --- Cash Flow Projections: Only for Operating bucket --- cashflow_by_year_month = defaultdict(float) + # --- CD Maturity Tracking: For Reserve bucket --- + cd_maturity_by_year_month = defaultdict(float) if bucket_name.lower() == "operating" and primary_account_id is not None: # Get all cash flow categories for Operating op_categories = db.query(models.CashFlowCategory).filter(models.CashFlowCategory.funding_type == models.CashFlowFundingTypeEnum.operating).all() @@ -719,31 +721,46 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge cd_funding_tx = None # Look for funding transactions in the forecast year first for m in range(1, months + 1): + # Check both direct deposits and transfers TO this CD month_txs = txs_by_year_month.get((acc.id, year, m), []) for tx in month_txs: if tx.type in ('deposit', 'transfer'): cd_funding_tx = tx break + if not cd_funding_tx: + # Also check for transfers TO this CD (where related_account_id == acc.id) + month_txs = txs_by_year_month.get((acc.id, year, m), []) + for tx in month_txs: + if tx.type == 'transfer' and tx.related_account_id == acc.id: + cd_funding_tx = tx + break if cd_funding_tx: funding_month = cd_funding_tx.date.month funding_amount = cd_funding_tx.amount logger.debug(f"CD funding for {acc.name} (id={acc.id}) in month {funding_month}: amount={funding_amount}") break - # If no funding found in forecast year, look for historical funding + # If no funding found in forecast year, look for historical funding (including planned transactions) if not cd_funding_tx: - # Get all historical transactions for this CD + # Get all transactions for this CD (both reconciled and planned) + # Look for both direct deposits and transfers TO this CD all_cd_txs = db.query(models.Transaction).filter( - models.Transaction.account_id == acc.id, models.Transaction.type.in_(('deposit', 'transfer')), - models.Transaction.reconciled == True + or_( + models.Transaction.account_id == acc.id, # Direct deposit to CD + models.Transaction.related_account_id == acc.id # Transfer TO CD + ) ).order_by(models.Transaction.date.asc()).all() + logger.debug(f"CD {acc.name} (id={acc.id}): Found {len(all_cd_txs)} funding transactions") + for tx in all_cd_txs: + logger.debug(f" - {tx.date}: {tx.amount} ({tx.type}, reconciled={tx.reconciled})") + if all_cd_txs: cd_funding_tx = all_cd_txs[0] # First funding transaction funding_month = cd_funding_tx.date.month funding_amount = cd_funding_tx.amount - logger.debug(f"CD funding for {acc.name} (id={acc.id}) found in historical data: month {funding_month}, amount={funding_amount}") + logger.debug(f"CD funding for {acc.name} (id={acc.id}) found in historical data: month {funding_month}, amount={funding_amount}, reconciled={cd_funding_tx.reconciled}") # For CDs, determine starting balance based on whether they should exist if cd_funding_tx: @@ -751,7 +768,7 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge cd_start_age_months = (year - cd_funding_tx.date.year) * 12 + (1 - cd_funding_tx.date.month) if cd_start_age_months >= 0: - # CD should exist, use previous year's ending balance or validated balance + # CD should exist, prioritize previous year's ending balance (which includes projected Dec balance) if prev_dec_balance is not None: balance = prev_dec_balance logger.debug(f"[{acc.name}] CD account, using Dec {year-1} ending balance: {balance}") @@ -787,6 +804,9 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge prev_dec_balance = None if (year-1, 12) in monthly_actuals: prev_dec_balance = monthly_actuals[(year-1, 12)] + logger.debug(f"[{acc.name}] Found Dec {year-1} balance in monthly_actuals: {prev_dec_balance}") + else: + logger.debug(f"[{acc.name}] No Dec {year-1} balance found in monthly_actuals. Available keys: {list(monthly_actuals.keys())}") # Determine starting balance for forecast (skip for CDs since we already set them to $0) if not is_cd: if earliest_actual_month == 1 and (year, 1) in monthly_actuals: @@ -841,21 +861,13 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge if (maturity_date.year == forecast_year and maturity_date.month == forecast_month and primary_account_id is not None): - # Find the CD balance just before maturity - prev_balance = balance - if forecast_month > 1: - prev_month_date = datetime(forecast_year, forecast_month - 1, 1) - prev_key = (forecast_year, forecast_month - 1) - if prev_key in monthly_actuals: - prev_balance = monthly_actuals[prev_key] - else: - # Calculate previous month's balance - prev_balance = balance # This will be calculated in the loop - - # Add matured amount to primary account's cash flow - if prev_balance > 0: - cashflow_by_year_month[(forecast_year, forecast_month)] += prev_balance - logger.debug(f"CD {acc.name} matured: adding ${prev_balance} to primary account cash flow") + # For CD maturity, we need to calculate the balance just before maturity + # This will be the balance from the previous month's calculation + # We'll store this in a variable that gets updated as we process each account + matured_amount = balance # This is the CD balance before maturity + if matured_amount > 0: + cd_maturity_by_year_month[(forecast_year, forecast_month)] += matured_amount + logger.debug(f"CD {acc.name} matured: adding ${matured_amount} to CD maturity tracking") continue @@ -900,6 +912,12 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge if cf != 0.0: logger.debug(f"Applying cash flow projection to primary account {acc.name} (id={acc.id}) {forecast_year}-{forecast_month:02d}: {cf}") bal += cf + # --- Apply matured CD funds to primary account (Reserve only) --- + elif bucket_name.lower() == "reserve" and acc.id == primary_account_id: + matured_cd_amount = cd_maturity_by_year_month.get((forecast_year, forecast_month), 0.0) + if matured_cd_amount != 0.0: + logger.debug(f"Applying matured CD funds to primary account {acc.name} (id={acc.id}) {forecast_year}-{forecast_month:02d}: {matured_cd_amount}") + bal += matured_cd_amount # --- Apply interest if applicable --- if acc.interest_rate and acc.interest_rate > 0: interest = bal * (acc.interest_rate / 100.0) / 12.0