diff --git a/app/Http/Controllers/Api/CashflowAnalyticsController.php b/app/Http/Controllers/Api/CashflowAnalyticsController.php index c4edd40a..3a155b51 100644 --- a/app/Http/Controllers/Api/CashflowAnalyticsController.php +++ b/app/Http/Controllers/Api/CashflowAnalyticsController.php @@ -17,6 +17,8 @@ use Illuminate\Support\Collection; class CashflowAnalyticsController extends Controller { + private const MAX_TREND_MONTHS = 24; + public function __construct( private ExchangeRateService $exchangeRateService, private CategoryTree $tree, @@ -70,7 +72,7 @@ class CashflowAnalyticsController extends Controller public function trend(Request $request): JsonResponse { $validated = $request->validate([ - 'months' => 'nullable|integer|min:1|max:24', + 'months' => 'nullable|integer|min:1|max:'.self::MAX_TREND_MONTHS, 'from' => 'nullable|date', 'to' => 'nullable|date', ]); @@ -88,6 +90,15 @@ class CashflowAnalyticsController extends Controller $start = $end->copy()->subMonthsNoOverflow($months - 1)->startOfMonth(); } + // Bound the window to the most recent MAX_TREND_MONTHS months so an + // unbounded from/to range cannot make the month loop below iterate + // indefinitely and exhaust the request timeout. + $earliestStart = $end->copy()->subMonthsNoOverflow(self::MAX_TREND_MONTHS - 1)->startOfMonth(); + + if ($start->lt($earliestStart)) { + $start = $earliestStart; + } + $monthlyTotals = $this->getMonthlyTrendTotals($user->id, $user->currency_code, $start, $end); $data = []; diff --git a/tests/Feature/CashflowAnalyticsTest.php b/tests/Feature/CashflowAnalyticsTest.php index 42bd965e..348ac467 100644 --- a/tests/Feature/CashflowAnalyticsTest.php +++ b/tests/Feature/CashflowAnalyticsTest.php @@ -703,6 +703,16 @@ test('cashflow trend can use explicit period bounds', function () { expect($data['2025-05']['income'])->toBe(48000); }); +test('cashflow trend caps the window for unbounded date ranges', function () { + $response = $this->getJson('/api/cashflow/trend?'.http_build_query([ + 'from' => '0001-01-01', + 'to' => '9999-12-31', + ])); + + $response->assertOk(); + expect(count($response->json('data')))->toBe(24); +}); + test('cashflow trend defaults to 12 months', function () { $response = $this->getJson('/api/cashflow/trend');