From 00b2ca7c55d947d95c9582ce2039a91376f83db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Sat, 31 Jan 2026 21:13:56 +0100 Subject: [PATCH] fix: Budget period not found on last day of period (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Budget periods use `date` columns (`start_date`, `end_date`) but queries compared them against `now()` which includes a time component (e.g. `2026-01-31 15:30:00`). MySQL converts the date to midnight for comparison, so `end_date >= now()` evaluates to `'2026-01-31 00:00:00' >= '2026-01-31 15:30:00'` → **false** — causing "No active period" on the last day of every period. - Replaced `now()` with `today()` (date-only) in all budget period date comparisons across `Budget::getCurrentPeriod()`, `BudgetController::index()`, and `GenerateBudgetPeriods` command. ## Test plan - [x] Added 4 Pest tests covering the edge case (last day at various times, first day at end of day, index/show endpoints) - [x] All existing budget tests pass --- .../Commands/GenerateBudgetPeriods.php | 6 +- app/Http/Controllers/BudgetController.php | 4 +- app/Models/Budget.php | 4 +- tests/Feature/BudgetPeriodDateTest.php | 121 ++++++++++++++++++ 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/BudgetPeriodDateTest.php diff --git a/app/Console/Commands/GenerateBudgetPeriods.php b/app/Console/Commands/GenerateBudgetPeriods.php index 69912369..e94a8594 100644 --- a/app/Console/Commands/GenerateBudgetPeriods.php +++ b/app/Console/Commands/GenerateBudgetPeriods.php @@ -36,18 +36,18 @@ class GenerateBudgetPeriods extends Command } $completedPeriods = BudgetPeriod::where('budget_id', $budget->id) - ->where('end_date', '<', now()->subDay()) + ->where('end_date', '<', today()) ->get(); foreach ($completedPeriods as $period) { - if ($period->end_date < now()->subDay()) { + if ($period->end_date < today()) { $this->budgetPeriodService->closePeriod($period); $closedCount++; } } $futurePeriods = $budget->periods() - ->where('start_date', '>', now()) + ->where('start_date', '>', today()) ->count(); if ($futurePeriods < 2) { diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 0b82f35f..e1996418 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -25,8 +25,8 @@ class BudgetController extends Controller $budgets = $user ->budgets() ->with(['category', 'label', 'periods' => function ($query) { - $query->where('start_date', '<=', now()) - ->where('end_date', '>=', now()) + $query->where('start_date', '<=', today()) + ->where('end_date', '>=', today()) ->with(['budgetTransactions']); }]) ->get(); diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 0a023cf6..66569b95 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -59,8 +59,8 @@ class Budget extends Model public function getCurrentPeriod(): ?BudgetPeriod { return $this->periods() - ->where('start_date', '<=', now()) - ->where('end_date', '>=', now()) + ->where('start_date', '<=', today()) + ->where('end_date', '>=', today()) ->first(); } } diff --git a/tests/Feature/BudgetPeriodDateTest.php b/tests/Feature/BudgetPeriodDateTest.php new file mode 100644 index 00000000..dbd274ac --- /dev/null +++ b/tests/Feature/BudgetPeriodDateTest.php @@ -0,0 +1,121 @@ +create(['onboarded_at' => now()]); + $budget = Budget::factory()->create([ + 'user_id' => $user->id, + 'period_type' => BudgetPeriodType::Monthly, + 'period_start_day' => 1, + ]); + + BudgetPeriod::factory()->create([ + 'budget_id' => $budget->id, + 'start_date' => '2026-01-01', + 'end_date' => '2026-01-31', + 'allocated_amount' => 100000, + ]); + + $currentPeriod = $budget->getCurrentPeriod(); + + expect($currentPeriod)->not->toBeNull(); + expect($currentPeriod->start_date->toDateString())->toBe('2026-01-01'); + expect($currentPeriod->end_date->toDateString())->toBe('2026-01-31'); + + Carbon::setTestNow(); +}); + +test('getCurrentPeriod finds period on its first day regardless of time', function () { + Carbon::setTestNow(Carbon::parse('2026-01-01 23:59:59')); + + $user = User::factory()->create(['onboarded_at' => now()]); + $budget = Budget::factory()->create([ + 'user_id' => $user->id, + 'period_type' => BudgetPeriodType::Monthly, + 'period_start_day' => 1, + ]); + + BudgetPeriod::factory()->create([ + 'budget_id' => $budget->id, + 'start_date' => '2026-01-01', + 'end_date' => '2026-01-31', + 'allocated_amount' => 100000, + ]); + + $currentPeriod = $budget->getCurrentPeriod(); + + expect($currentPeriod)->not->toBeNull(); + expect($currentPeriod->start_date->toDateString())->toBe('2026-01-01'); + + Carbon::setTestNow(); +}); + +test('budget index loads current period on last day of period', function () { + Carbon::setTestNow(Carbon::parse('2026-01-31 18:00:00')); + + $user = User::factory()->create(['onboarded_at' => now()]); + Feature::for($user)->activate('budgets'); + + $budget = Budget::factory()->create([ + 'user_id' => $user->id, + 'period_type' => BudgetPeriodType::Monthly, + 'period_start_day' => 1, + ]); + + BudgetPeriod::factory()->create([ + 'budget_id' => $budget->id, + 'start_date' => '2026-01-01', + 'end_date' => '2026-01-31', + 'allocated_amount' => 100000, + ]); + + $response = $this->actingAs($user)->get('/budgets'); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('budgets/index') + ->has('budgets', 1) + ->where('budgets.0.periods', fn ($periods) => count($periods) === 1) + ); + + Carbon::setTestNow(); +}); + +test('budget show finds current period on last day of period', function () { + Carbon::setTestNow(Carbon::parse('2026-01-31 20:00:00')); + + $user = User::factory()->create(['onboarded_at' => now()]); + Feature::for($user)->activate('budgets'); + + $budget = Budget::factory()->create([ + 'user_id' => $user->id, + 'period_type' => BudgetPeriodType::Monthly, + 'period_start_day' => 1, + ]); + + BudgetPeriod::factory()->create([ + 'budget_id' => $budget->id, + 'start_date' => '2026-01-01', + 'end_date' => '2026-01-31', + 'allocated_amount' => 100000, + ]); + + $response = $this->actingAs($user)->get("/budgets/{$budget->id}"); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('budgets/show') + ->has('currentPeriod') + ->where('currentPeriod.start_date', '2026-01-01T00:00:00.000000Z') + ); + + Carbon::setTestNow(); +});