From 05c446bc8207aa766a744f7d20876b5aaa1b6cf6 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 1 Aug 2025 10:09:35 -0400 Subject: [PATCH] Commit 4: Added future year forecasting logic --- backend/backend/backend/backend/hoa.db | 0 backend/backend/backend/backend/hoa_app.db | 0 backend/backend/frontend/src/Forecasting.tsx | 6 +- backend/backend/hoa.db | 0 backend/backend/hoa_app.db | Bin 143360 -> 143360 bytes .../hoa_app/__pycache__/main.cpython-311.pyc | Bin 70903 -> 72575 bytes backend/backend/hoa_app/main.py | 62 +++++++++++------- 7 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 backend/backend/backend/backend/hoa.db create mode 100644 backend/backend/backend/backend/hoa_app.db create mode 100644 backend/backend/hoa.db 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 5ba66079fb6637a114af3a08cb57cd43726e7b69..14dd5a7fb242c4775631b70f40d01d52d5c49310 100644 GIT binary patch delta 80 zcmV-W0I&am;0S==2#^~AGm#ua0W*PMwO|3EPY(~>02U332M>@BxesOyinkC@0agtJ m4@3YYk%1tW-CqGCx2;_PFaiVs001J9fgh5f1h)iW0sPP1!x&@$ delta 62 zcmV-E0Kxx&;0S==2#^~AEs-2U0WE=GwO|3EPY(b902U3d1rLx9xesO!-M0`>0agu> UkVv<#T>&owmx^5hBc@;h&|@_dtpET3 diff --git a/backend/backend/hoa_app/__pycache__/main.cpython-311.pyc b/backend/backend/hoa_app/__pycache__/main.cpython-311.pyc index df4923a3e027dff6dd19ab5526dd668ed7c32708..391fdf37db766ef9921eca7ec5c64db4a7fc3ab8 100644 GIT binary patch delta 13252 zcmb6=33yc1)$?Y{zGuirk{7a0_5dLZkT3~LKtOgdB!tPlNtk3ZN$yO5q;H5+Y{BNY z=(U1HH2S%~r?sJj7L_UpT2$0Y8YLaYV*A_0ek!#7b-~vEocm@?g4+2eXYO+D+0VWA zy?gWdapgD9C?g+_hzOOzXYAFX9sADR6`9CgJdqp44R)0`mUA*eDMXm(byYN0bX7K1 zLR}_AnyVVC*l%@XHT*`I=XWh=T)_IG%?rC~8f&_08*5oT#=NLY*QjIVSaV%hePcZ< z$C(#*EoofB%JJrIl0 zOr?v0SJ4BiSk-n&yFk5&(^IoLNhNqVsH#97BkfVn zq-WGcTpRsFouO*?P!6gBj@Vxs)UMj$8ADG5^{7l99K}Lo3#|#(t2#U_^x5D#;PZ8G zlByHhB;F+<$h#*bDZ?z6Et3gL_Q`~$#s!Zlz}sCdqm>ybfetW#rwmZkRHDC*Vrb%#UbN?ecI{Z3JmD znKX*XSk-P%J1vRyNwakdwSK^pqEEd-?r1+Q(0rGIo!b-}PvlYS7Ei2>gnGUnzYdCdDhSY0CIpeY|5C z@7Pq;ULWsRAMdUyV}I!3t*}5m{wKCyb(^P!-WAux?Q?t*cZWPy;Zo`?LW@h;)@L!e z6y2o7ar=yu!C{Lb07(pjC3JCmA=f~A)3Z`nVZ(9&uAp6dVli3TU64AheeGH`(a~R} zS8%K7N9mimR)>zatFl%j`ppO;*%*_>WNQ&xT}q2_w~fSNM=Skfc2$uMiya6$5s@=m zmGH~Mhv=~(wA<2RwdskiuZQ$H8gd5Y>Ndn~rx)@{jTR_s17vIapkg0%>IuG)mvbqFlOrrq*cQ&(Hd>8jol!54A465EB#avK%r?rG&4^~62qwmMlhp>A3^|sT{Y=5_ zqwme#vSE^h@kn()0!AM#OEePI|G_e&%IMz>)k)4)nY-If7IF{WU;YwU>ZW;D*Zv5{ z21|BmLITVVC^oqR0aFykX2v+qk)J`?$^mdYGlGC(Eq$P&wWZ5Q4%4BEwLts*igyc{ z=su!{CXEc$3tc8lOSi?`Pma(JE0@I@0AnDyq_xk`X|!1(27c(Msk%?0-iPfo?_QX! ztmet>^!kEKZZS8s0UvEU%}^xE>Y=)iCwYiRn4 z83inp1FbXF@xMY{b#(iRR9J}itVrUXaU5O|>gU&{m1*_U`Sm0Y#BC6H9EJBwj9BJX zy}{7kXR)=|`g@F4a*Fm?Hf>UD zlNW)!E7W8)Szx-h7>r~mGA+c`h&IyQWwLg+b(1c=jZMjHwkB+LU!%$yqD~=7LK@&##@4j*;b3_t^}V?p?4DJ&TAKy-XU?L%&>`H$#s&k?yo`t4m&Y zRIIyQUNfD=UcvFxS}elfVpj4d{b~aw0$sRqCU=hN>XRJXhBk5Z)lCH~KEH}_>%?2S zZG0P8Szyt6gLGkRSu9MSF6X?up_YN^5{BvGQEdz|-MaaH@S?8%8;(XbR!QmNL;@^b zyn;>2mne_G?)Dob%(qHPMvx6#bVfaLCVOY6NO!h)WF4m;G(x)2vs?d_z8DAk+>i0( z9|QEAru-&`Q;t0tU9PB>7QF?mtH*`b+S1}1?ozdxyCK#Bskjs-i;ZA79hVWDQP>`X zyo058XjAi(YS#FUBXV1B&^$KDc&)ZU%ef+8RX6tu#+E*d)#xVvJPy7c!D$3%=+nlz z+)MN$V{L;k-F>sow;Io-6ifyiOB(VrQp4Q~tmwUM@tGH(=!!AxVJ6P^C|&8Iyqp@HwhCC#GWx!II^R+mDfnLSjPn^sqUD`v-l_d42;mF+8{R0rNn8;rPg=h93`nbOoiUn#XbQTeP)F7Fw!^c1Q_s2Z%WqZ*!=~9 zVFX_y_zHoaooG5q*8CG2(L%0Z+ipt>>JL-NYLc7Grjl6qRAM7~i&byH&?B7Vr{rOd zyGXYmNOIg{+s;X|BXfh#_t%jynuUCgfXT}w$61p68vxjgF%I`5Zgp`spVI_ciskeeQIZ`oO^=C^rsA>U6}>7QPVEhOt>4(KQ|N@)^Rnk zl>!P47bs~IPE1VDV7H}0siXgW&)jsEs0(9R7K}9Z(aT~Aw}*ZyrmXP!car&TDB%d| z$L5_7w1Y3sw%HT8aJtL>0ZVR!f}Y5YcaS}e%49#*IQah%uuT3J{qqm={T8_IE5mn$ zA<}2z%3^O)JhD(^@^3`EuVoOHQcMI%CiaFU`34dGgP;e24D}6yQb6X$x--`=Oh}~T zkGFlHP%{}mr&oTg31j)d#2&tTe@>Xs?B{6N{)8-xAGu-zWef6B0Z&VAcrK zJsdN^BLzvJ+wXk9lLh}dI6sSpViJx=-3NSv`7P13`L4ui6XRRu@>`ITfxb^F`r=(0 zE|tE07w%V|zPq1;XO@zCGW}v;F>SslWgd@GWgwX1^fYYMfxxg$MyLDDQ}n5O(zq=8 z#ytmo&mPhDX6LgAn=~)ZLNd&!{(Dr~a&H=!L-*W^8`78VeT9>DRbU#MvAvu0Pxe{3 z^mH5s;t)o0lBY0Aq$g&dM&5jbhu&P2!mAO`6<|@ z;QkajmrIvD(4+C?FB@5k{LARY2ND}*V{r}ww!I8=CEG8635u{^hwL^1a0P=?jj-;y?zln@=yvl zpI&^Z8?;*Ruuh(UR{Sbz3CiH_ZhFrn+Cg|tFVjZ*S=7hMw}WO*_p=2`<1>ZV{l2LN ztN`!yWqzipfU2JXP-qRrK|F4Yyn(>XL~7}pNB`m%1=7a52I<`(3T`N~e-to*bZBiR zi#H0GDDEBgBuR9@vsg}pkKF;{&|_Kh2l5=x9t)2QmdiwiZ3R3c=wC_T(&)#p71Mt{ z^LAM}91_Sx*n^{k(CX61&I>cAZO@>e9?zy}rv#3tC!S8F!>6(;W{I;F%bJhcGsVmf z+|jVlt=v#3)KfflD9`ki+vN5vF@x@XHbIfuQ9ygEXVA+}MbHv>GF;_@Gq7>ppsEfJuz;oGgd1A<7*}<^pEPG_T%&s-b?D_QU zb2nvX+Y7`3F`Ma^CFbwstO?@Gjzo_tV!oI~OP;TafnTPv zvgZpjd!d+5N^I?(VKUkN`Zi9;*cS|_bKR(!Fb(|VjWk+zdVO3Gj42i(>?LB+L3mdL zwNiTMbgtr*J)53Aotu!|rf8Qv1}Z;+Z!FozUM7~&Z%(I#K}K4*p=hzxT8Fq5H^2o4 zXV~M#_>OcB1tF)CQ6t!zL`P4@)3;s=icyO3c8TUJv6L(lOX=DdHBrT4X$S8aX)i(Z zYIDWNjwnwX7&;H?v3_-}SPZTCVhJrfA0N)dd{!(y$XS0*-+IzS8(t{oy6Hnt!s;ulhc5A(b1s5V=+!9 z&IU_V+T)Q_XEA8*buQq?LEWn*)Jy#B1@%&>C-`Ytb~I!%{h=yxHd!ss7Sz53u$SK0 zikrX7inEy&=ZGbYf2lwJqoI?zqj^abBuXj$;JJ9(d?8dRL^0;^bnrrmW{x;VP=dr)F7Ntg(W)>7AtVVu$K=dqL5QqS4*tb79AuH zlTJHXy7@;Hfvh18c@K8UWb6e~u^|~N;jhY@<;^ce3>Mh4?bTwzf%;CSD=8GK_GZD% zQRVOUP9=6)6Q{IGX%{PI1Ade*OeB1w7OU-*?pdg6Xg=_s>+yvU+9rotNv0%o#eDkl zO9^!Dx#ZNzN-XrCgX?N&%b5gj30kjcGG478-Xa^moN<4=v(%lLk@RQJ#g~dj))&P> zA<8{rFnJ_NNU@lYWZL}Zd&v_?liBGG)Pq2AES5AOw?p(5dZI6TGH#upX!YK4W#=^^ zo5jk#7p1Vr2C)1SbI1u!Y+3?5WA zTkQYUVgyUK5|cUb`fPsvxo(Fn z6chH&7t8InVlB*{EU(ENF->ycq}@*KKm@(^+syLGimefAL4~zPRg>j7$PL{o7Vo_s zsClOCFh#C!){a>uE&{I1wxs#LG~7CI5pZ4qJqCq?GWr^aSP;|vLG_2 zv;U3AM6D6aZ&jcM7CBh68a6ChEHmi!SF>-~;5K%cSnae)}BS|C%-_kfwyU0q{HEB-~yhi`s;0G0B}Jav{<_|DbZHN6f=nZ-XXcuV-qK z$vSkn*;NbMVC$}>x4e-JkFJAnYz%673?3k!P~+o_nuDi!ciiHV+UR^b(~EapP8tByrNCD<$kcs% zfvlj-6Zzbuw0Sg>(O;T_W8mEyd!lCVP1gIfK*lSffsVYD%RNRlV|g?1t-wHJJ-k@s z@eLpxd=VaEdHvWL#vfj4FxFhBi`(gY^ba6(o_;b`;MQ->?+OZ-+)NOqdvL#t>tZJJ zJec1qo#aUJU!W&{H@g@GffM7KkTf2jy5t5yD|ZD6#-47g$p$a?@dbVx+-GP(4Wh=& z>J;^h`GF8^HNO;3>-Yf;4|ih(xa-3Ey+ySSD0#k!ACU8T0}+KhF&erp29wz+Ec}Yo zI%>yPa)aT`ZVHQy27VxwhYKNiB+8pCd{?){w!_>H2P(Ecz1dm~{F?dI-8{pb#v4>3Vw1{m76FG72~z?PNt-J;cAYVJXId{!lyJYI|Cq=ctaAmy{Sdey((Vd@x2Y?k49;VhW6Y>pm;tV~7}mNWJ4j*DNkP$sn|NLWO?u z?&2`^#_%Jg(?plOw}#tF55IROUJ$t4z?o>zWqvWTcSX8GrA0c)b436heY>%RWrGzm zv&|LbLBTHRybtF@D>{UJaCrf}dO1k32+j@p_Y=ZKpr;ZhE^BYHqwf7jgQBnS1;b&# zSTf4b9_ME}m8p)ue7Hp()U8m3U6XY?mi~1nmv<8vLbxk4`TeUt)FdA+JKQ$hdtb+> zMmw(2UX{g0Br|Z3yFw5AZRNJa?5i?$%&aSX*(JVwj4vOtp4@rLc4n<}-m(j^qx{No ze&xaC6KT0uWeUwK1`f+7Qc~})815KLDRrilj;tFmUva^3v2Cn;gR^|YL}BsaRTDEa z4lns)B95n*{?-~27h#uQRm;+Ny6B_SFT18vXl8r{0?fFkQm4g!CIj$!I6d@H-e80*Cgp0hEGFSl z_}$^oRMYUBM=Ie5z9X3@bEMMCbIuH$tvz2W)c}r0cZ^4OIHNlzlC!SLxR@QB1P|6t z#3vtGdG|_ZM%yv@lR@wU-;voTtEAEy`FVGnbJLdbO$KL13L?OFG`@X2zTL^bSK?By z%9Js7`Uc1+%17p$tb_!ZCCVjuCi8rbf8A;B_pd^EiF~~q*W1x5@0NcileNkR0(u~) zG}L=l1THeHgK`D z_v&w(t_I7B%MY)fNG}@U#?s52>17jq)v1LSik;QBIQd5Sjsx6g9_7tab(C+M;By`i ze>mJ(v~0a-$Au36mQ7B}5B%#;9^_V`M}fd3xKZ8~ z&;&+JGNDZY0xH0X_ylK4;b?rJgMM~_g9C=9e?_afR@!yFdI8&DOx-}?w6+3fnt@~j zdj?`FUSRHqb;Mc&1(@v(-CaF3zovh>o|%Sw4VMhAn6d?77zg5XPuRqNhGu;|SIy?x zo9VW%wS#N0D-6MP1h}S1ix0c)gv%1?0k)(BON=60kO;QrVjG*y|BWR!r4M7t$L}Gm z<^X{6Cht7&Qn6dKE``xDUyv5TJxEW1;3NVi0BZ;g<2@hg3W!=#o;(NrPdRk|4w18i zbnWeQ%{QAk2YvjTF9%5(jB`c0m**DP5w)4x$!rv44uVc(uo-cJd-?^v#bz>)xmZ=h z1rJ!z3A@uX4_ji|;EIdB$5c#=)*iUk1GmyDuszz>-rHxi+N`7!nn@Q@j>GlKrEWD@ zVfiQc!hu*06>(gW7VkumOW45d-->v_a4FZg6HjbVnUx;x<_=RJsg7{L()k09tpe#fx%9)hP3j3791=mU<+8+-=K zrx3i3;7tT$2*we-1Hh%~(c26=$R}7%a^J|RlQxhI5Or|!$*$_cWFjLRvctO{BoqO> z9fu3o2;vdUK#+nU6+sp!TL*!G#e4*X2;iSvWMn=9_}3NK5+K0UmARX+#K7QYq3~7k zg060%&upwAzXKHb%B(5BcFDOyd1{VRcEj()AzsDB|GG@YrA1YPxSFddwC3=x@$`98 g@zgTu*B@R}$mF}Yac<{TIUCb)=s-Nz!A_I^AHCOQJpcdz delta 12109 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 EKP>#-(f|Me 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