From 8f42496a5f6cd655828df7c49f358ad61d7e8002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Thu, 30 Apr 2026 14:53:01 +0100 Subject: [PATCH] fix(dashboard): avoid month overflow in real estate projection (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `DashboardAnalyticsTest > real estate balance evolution appends projected mortgage balance from linked loan` fails on certain calendar dates (e.g. when run on 2026-04-30) with: ``` Failed asserting that 17324874 is less than 17324874. ``` ## Root cause Carbon's `addMonths()` defaults to overflow mode, so adding months to a day that doesn't exist in the target month rolls forward into the next month: - `2026-04-30 + 10 months` → `2027-02-30` → overflow → `2027-03-02` - `2026-04-30 + 11 months` → `2027-03-30` Both end up in the same `Y-m` (`2027-03`). The dashboard balance-evolution endpoint and `LoanAmortizationService::projectFromBalance` use `addMonths` to build month-keyed projections and to label projected chart points. When two iterations collapse onto the same `Y-m`: 1. `generateProjection` overwrites the earlier balance with the later one. 2. The controller emits two consecutive projected points labeled with that month and assigns the same `mortgage_balance` to both, breaking the strictly-decreasing assertion. ## Fix Switch every `addMonths()` call in the projection paths to `addMonthsNoOverflow()` so each iteration lands on a distinct month. - `app/Services/LoanAmortizationService.php` — `projectFromBalance`, `projectFromOriginal` - `app/Http/Controllers/Api/DashboardAnalyticsController.php` — real estate projected loop ## Verification ``` php artisan test --compact tests/Feature/DashboardAnalyticsTest.php Tests: 34 passed (220 assertions) ``` Previously failing on 2026-04-30, now green. --- app/Http/Controllers/Api/DashboardAnalyticsController.php | 2 +- app/Services/LoanAmortizationService.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/DashboardAnalyticsController.php b/app/Http/Controllers/Api/DashboardAnalyticsController.php index 0f410742..b671d18e 100644 --- a/app/Http/Controllers/Api/DashboardAnalyticsController.php +++ b/app/Http/Controllers/Api/DashboardAnalyticsController.php @@ -240,7 +240,7 @@ class DashboardAnalyticsController extends Controller : []; for ($i = 1; $i <= $monthsAhead; $i++) { - $projectedDate = $now->copy()->addMonths($i)->endOfMonth(); + $projectedDate = $now->copy()->addMonthsNoOverflow($i)->endOfMonth(); $yearMonth = $projectedDate->format('Y-m'); $projectedValue = (int) round($baseValue * pow(1 + $monthlyRate, $i)); diff --git a/app/Services/LoanAmortizationService.php b/app/Services/LoanAmortizationService.php index 27f2dd85..a1098b34 100644 --- a/app/Services/LoanAmortizationService.php +++ b/app/Services/LoanAmortizationService.php @@ -127,7 +127,7 @@ class LoanAmortizationService $monthsToProject = min($monthsAhead, $remainingMonths); for ($i = 1; $i <= $monthsToProject; $i++) { - $date = $fromDate->copy()->addMonths($i); + $date = $fromDate->copy()->addMonthsNoOverflow($i); $balance = $this->calculateRemainingBalance( $currentBalanceCents, $annualRate, @@ -163,7 +163,7 @@ class LoanAmortizationService break; } - $date = $now->copy()->addMonths($i); + $date = $now->copy()->addMonthsNoOverflow($i); $balance = $this->calculateRemainingBalance( $loanDetail->original_amount, $loanDetail->annual_interest_rate,