From cc666a3111833bc6581b46e6f46247f6f9ed7ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Sat, 31 Jan 2026 14:38:17 +0100 Subject: [PATCH] test: Fix Carbon month overflow in date calculations (#87) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replace `subMonths()` with `subMonthsNoOverflow()` across controllers, commands, and tests to prevent date overflow on months with 31 days - On Jan 31, `subMonths(2)` overflows: Nov 31 doesn't exist → Carbon rolls forward to Dec 1, producing wrong date ranges and off-by-one month counts ## Root cause Carbon's `subMonths()` allows day overflow. When the target month has fewer days than the source date, it rolls into the next month. For example: - `2026-01-31 subMonths(2)` → Nov 31 → **2025-12-01** (expected Nov 30) - `2026-01-31 subMonths(11)` → Feb 31 → **2025-03-03** (expected Feb 28) `subMonthsNoOverflow()` clamps to the last valid day of the target month instead. ## Files changed - `app/Http/Controllers/Api/CashflowAnalyticsController.php` — cashflow trend start date calculation - `app/Console/Commands/ResetDemoAccountCommand.php` — balance history date generation (was causing duplicate `balance_date` entries) - `tests/Feature/AccountControllerTest.php` — balance evolution `from` param - `tests/Feature/CashflowAnalyticsTest.php` — transaction date generation in loop - `tests/Feature/DashboardAnalyticsTest.php` — net worth evolution `from` param ## Test plan - [x] `account balance evolution returns data` — expects 3 months, was getting 2 - [x] `cashflow trend returns monthly data` — expects 3 months, was getting 2 - [x] `cashflow trend defaults to 12 months` — expects 12 months, was getting 11 - [x] `net worth evolution returns monthly data points` — expects 3 months, was getting 2 - [x] `demo:reset creates demo user` — was throwing UniqueConstraintViolation on duplicate balance dates --- app/Console/Commands/ResetDemoAccountCommand.php | 2 +- app/Http/Controllers/Api/CashflowAnalyticsController.php | 2 +- tests/Feature/AccountControllerTest.php | 2 +- tests/Feature/CashflowAnalyticsTest.php | 4 ++-- tests/Feature/DashboardAnalyticsTest.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Console/Commands/ResetDemoAccountCommand.php b/app/Console/Commands/ResetDemoAccountCommand.php index 1bf78583..604a3653 100644 --- a/app/Console/Commands/ResetDemoAccountCommand.php +++ b/app/Console/Commands/ResetDemoAccountCommand.php @@ -276,7 +276,7 @@ class ResetDemoAccountCommand extends Command $balances = []; for ($i = 0; $i <= 12; $i++) { - $date = now()->subMonths($i)->endOfMonth(); + $date = now()->subMonthsNoOverflow($i)->endOfMonth(); if ($i === 0) { $date = now(); diff --git a/app/Http/Controllers/Api/CashflowAnalyticsController.php b/app/Http/Controllers/Api/CashflowAnalyticsController.php index 99dfe7a1..ed24ac44 100644 --- a/app/Http/Controllers/Api/CashflowAnalyticsController.php +++ b/app/Http/Controllers/Api/CashflowAnalyticsController.php @@ -67,7 +67,7 @@ class CashflowAnalyticsController extends Controller $userId = $request->user()->id; $end = Carbon::now()->endOfMonth(); - $start = Carbon::now()->subMonths($months - 1)->startOfMonth(); + $start = Carbon::now()->subMonthsNoOverflow($months - 1)->startOfMonth(); $data = []; $current = $start->copy(); diff --git a/tests/Feature/AccountControllerTest.php b/tests/Feature/AccountControllerTest.php index 98fb1cad..366b7113 100644 --- a/tests/Feature/AccountControllerTest.php +++ b/tests/Feature/AccountControllerTest.php @@ -167,7 +167,7 @@ test('account balance evolution returns data for single account', function () { ]); $response = $this->getJson('/api/dashboard/account/'.$account->id.'/balance-evolution?'.http_build_query([ - 'from' => now()->subMonths(2)->startOfMonth()->toDateString(), + 'from' => now()->subMonthsNoOverflow(2)->startOfMonth()->toDateString(), 'to' => now()->endOfMonth()->toDateString(), ])); diff --git a/tests/Feature/CashflowAnalyticsTest.php b/tests/Feature/CashflowAnalyticsTest.php index 2dec35bb..bc6432bb 100644 --- a/tests/Feature/CashflowAnalyticsTest.php +++ b/tests/Feature/CashflowAnalyticsTest.php @@ -223,14 +223,14 @@ test('cashflow trend returns monthly data for specified months', function () { 'account_id' => $account->id, 'category_id' => $incomeCategory->id, 'amount' => 100000 + ($i * 10000), - 'transaction_date' => now()->subMonths($i), + 'transaction_date' => now()->subMonthsNoOverflow($i), ]); Transaction::factory()->create([ 'user_id' => $this->user->id, 'account_id' => $account->id, 'category_id' => $expenseCategory->id, 'amount' => -(50000 + ($i * 5000)), - 'transaction_date' => now()->subMonths($i), + 'transaction_date' => now()->subMonthsNoOverflow($i), ]); } diff --git a/tests/Feature/DashboardAnalyticsTest.php b/tests/Feature/DashboardAnalyticsTest.php index 108d72f7..78c2c3a1 100644 --- a/tests/Feature/DashboardAnalyticsTest.php +++ b/tests/Feature/DashboardAnalyticsTest.php @@ -211,7 +211,7 @@ test('net worth evolution returns monthly data points with per-account balances' ]); $response = $this->getJson('/api/dashboard/net-worth-evolution?'.http_build_query([ - 'from' => now()->subMonths(2)->startOfMonth()->toDateString(), + 'from' => now()->subMonthsNoOverflow(2)->startOfMonth()->toDateString(), 'to' => now()->endOfMonth()->toDateString(), ]));