fix: Budget period not found on last day of period (#91)

## 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
This commit is contained in:
Víctor Falcón 2026-01-31 21:13:56 +01:00 committed by Víctor Falcón
parent b4897ef425
commit 00b2ca7c55
4 changed files with 128 additions and 7 deletions

View File

@ -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) {

View File

@ -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();

View File

@ -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();
}
}

View File

@ -0,0 +1,121 @@
<?php
use App\Enums\BudgetPeriodType;
use App\Models\Budget;
use App\Models\BudgetPeriod;
use App\Models\User;
use Carbon\Carbon;
use Laravel\Pennant\Feature;
test('getCurrentPeriod finds period on its last day regardless of time', function () {
Carbon::setTestNow(Carbon::parse('2026-01-31 15:30:00'));
$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');
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();
});