streams() ->where('is_active', true) ->get(); // Process each day in the range $currentDate = $startDate->copy(); while ($currentDate <= $endDate) { // Process all streams that fire on this date foreach ($activeStreams as $stream) { if ($this->streamFiresOnDate($stream, $currentDate)) { if ($stream->type === StreamTypeEnum::INCOME) { // Create and save inflow $inflow = Inflow::create([ 'stream_id' => $stream->id, 'amount' => $stream->amount, 'date' => $currentDate->copy(), 'description' => "Projected income from {$stream->name}", 'is_projected' => true, ]); $inflows->push($inflow); // Immediately allocate this income to buckets $dailyDraws = $this->pipelineAllocationService->allocateInflow( $scenario, $inflow->amount ); // Set date and description for each draw and save foreach ($dailyDraws as $draw) { $draw->date = $currentDate->copy(); $draw->description = "Allocation from {$stream->name}"; $draw->is_projected = true; $draw->save(); } $draws = $draws->merge($dailyDraws); } else { // Create and save outflow $outflow = Outflow::create([ 'stream_id' => $stream->id, 'bucket_id' => $stream->bucket_id, 'amount' => $stream->amount, 'date' => $currentDate->copy(), 'description' => "Projected expense from {$stream->name}", 'is_projected' => true, ]); $outflows->push($outflow); } } } // Move to next day $currentDate->addDay(); } // Calculate summary statistics $summary = [ 'total_inflow' => $inflows->sum('amount'), 'total_outflow' => $outflows->sum('amount'), 'total_allocated' => $draws->sum('amount'), 'net_cashflow' => $inflows->sum('amount') - $outflows->sum('amount'), ]; return [ 'inflows' => $inflows->sortBy('date')->values(), 'outflows' => $outflows->sortBy('date')->values(), 'draws' => $draws->sortBy('date')->values(), 'summary' => $summary, ]; } private function streamFiresOnDate(Stream $stream, Carbon $date): bool { // Check if date is within stream's active period if ($date->lt($stream->start_date)) { return false; } if ($stream->end_date && $date->gt($stream->end_date)) { return false; } // Check frequency-specific rules return match ($stream->frequency) { StreamFrequencyEnum::DAILY => true, StreamFrequencyEnum::WEEKLY => $this->isWeeklyOccurrence($stream, $date), StreamFrequencyEnum::MONTHLY => $this->isMonthlyOccurrence($stream, $date), StreamFrequencyEnum::YEARLY => $this->isYearlyOccurrence($stream, $date), default => false, }; } private function isWeeklyOccurrence(Stream $stream, Carbon $date): bool { // Check if it's the same day of week as the start date return $date->dayOfWeek === $stream->start_date->dayOfWeek; } private function isMonthlyOccurrence(Stream $stream, Carbon $date): bool { // Check if it's the same day of month as the start date // Handle end-of-month cases (e.g., 31st in months with fewer days) $targetDay = $stream->start_date->day; $lastDayOfMonth = $date->copy()->endOfMonth()->day; if ($targetDay > $lastDayOfMonth) { // If the target day doesn't exist in this month, use the last day return $date->day === $lastDayOfMonth; } return $date->day === $targetDay; } private function isYearlyOccurrence(Stream $stream, Carbon $date): bool { // Check if it's the same month and day as the start date return $date->month === $stream->start_date->month && $date->day === $stream->start_date->day; } }