@@ -127,25 +127,46 @@ export function buildCashflowProjection(
127127 }
128128 } else {
129129 // Recurring: expand occurrences within the projection window
130- const interval = getIntervalMonths ( cost ) ;
131- if ( interval <= 0 ) continue ; // safety guard
130+ const interval = Number ( getIntervalMonths ( cost ) ) ;
131+ if ( ! Number . isFinite ( interval ) || interval <= 0 ) continue ; // bad data guard
132+
133+ // Sub-monthly (e.g. weekly = 0.25): setMonth can't step by fractions,
134+ // so charge every month from the start, scaled by occurrences per month
135+ if ( interval < 1 ) {
136+ const startKey = toMonthKey ( cost . date ) ;
137+ const perMonth = 1 / interval ;
138+ for ( const key of monthKeys ) {
139+ if ( key < startKey ) continue ;
140+ const adjusted = getAdjustedCostAmount ( cost , key , settings ) * perMonth ;
141+ costBuckets . set ( key , costBuckets . get ( key ) ! + adjusted ) ;
142+ }
143+ continue ;
144+ }
145+
146+ const step = Math . round ( interval ) ; // setMonth only takes whole months
147+ const MAX_STEPS = 1000 ; // hard stop so bad data can't freeze the tab
148+ let steps = 0 ;
132149
133150 // Parse as local date to avoid UTC shift
134151 const [ cy , cm ] = cost . date . split ( "-" ) . map ( Number ) ;
135152 const cursor = new Date ( cy , cm - 1 , 1 ) ;
136153
137154 // Advance past occurrences that fall before the exact start date
138- while ( new Date ( cost . date ) < new Date ( settings . startDate ) && toMonthKey ( cursor ) < toMonthKey ( new Date ( cy , cm , 1 ) ) ) {
139- cursor . setMonth ( cursor . getMonth ( ) + interval ) ;
155+ while (
156+ new Date ( cost . date ) < new Date ( settings . startDate ) &&
157+ toMonthKey ( cursor ) < toMonthKey ( new Date ( cy , cm , 1 ) ) &&
158+ steps ++ < MAX_STEPS
159+ ) {
160+ cursor . setMonth ( cursor . getMonth ( ) + step ) ;
140161 }
141162
142- while ( toMonthKey ( cursor ) <= endKey ) {
163+ while ( toMonthKey ( cursor ) <= endKey && steps ++ < MAX_STEPS ) {
143164 const key = toMonthKey ( cursor ) ;
144165 if ( costBuckets . has ( key ) ) {
145166 const adjusted = getAdjustedCostAmount ( cost , key , settings ) ;
146167 costBuckets . set ( key , costBuckets . get ( key ) ! + adjusted ) ;
147168 }
148- cursor . setMonth ( cursor . getMonth ( ) + interval ) ;
169+ cursor . setMonth ( cursor . getMonth ( ) + step ) ;
149170 }
150171 }
151172 }
0 commit comments