fix(dashboard): avoid month overflow in real estate projection (#340)

## 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.
This commit is contained in:
Víctor Falcón 2026-04-30 14:53:01 +01:00 committed by GitHub
parent 0f2300bf3e
commit 8f42496a5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 3 additions and 3 deletions

View File

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

View File

@ -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,