Consolidate savings-rate math into CashflowSummaryService
The net and savings-rate derivation was byte-identical in DashboardController::calculateCashflowSummary and CashflowAnalyticsController::cashflowSummaryFromTransactions, so the two screens could silently diverge if one formula were ever edited. Extract a single CashflowSummaryService::summarize(income, expense) that owns the net and savings-rate math, and have both call sites consume it. Behaviour-preserving: each controller still computes income/expense its own way (single-currency SQL on the dashboard, multi-currency in-memory on the analytics endpoint) and only the shared derivation moves.
This commit is contained in:
parent
845f51abb5
commit
722c7d1bef
|
|
@ -8,6 +8,7 @@ use App\Http\Controllers\Api\Concerns\ConvertsTransactionCurrency;
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Transaction;
|
||||
use App\Services\CashflowSummaryService;
|
||||
use App\Services\CategoryTree;
|
||||
use App\Services\ExchangeRateService;
|
||||
use App\Services\PeriodComparator;
|
||||
|
|
@ -208,14 +209,8 @@ class CashflowAnalyticsController extends Controller
|
|||
$savings = $this->sumOutflowTransactions($transactions, $userCurrency, CategoryType::Savings);
|
||||
$investments = $this->sumOutflowTransactions($transactions, $userCurrency, CategoryType::Investment);
|
||||
|
||||
$net = $income - $expense;
|
||||
$savingsRate = $income > 0 ? round((($income - $expense) / $income) * 100, 1) : 0;
|
||||
|
||||
return [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'net' => $net,
|
||||
'savings_rate' => $savingsRate,
|
||||
...CashflowSummaryService::summarize($income, $expense),
|
||||
'savings' => $savings,
|
||||
'investments' => $investments,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Enums\CategoryType;
|
|||
use App\Models\Account;
|
||||
use App\Models\Transaction;
|
||||
use App\Services\AccountMetricsService;
|
||||
use App\Services\CashflowSummaryService;
|
||||
use App\Services\CategoryTree;
|
||||
use App\Services\PeriodComparator;
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -135,15 +136,7 @@ class DashboardController extends Controller
|
|||
$income = max(0, $this->getTransactionSum($userId, $from, $to, CategoryType::Income));
|
||||
$expense = max(0, -$this->getTransactionSum($userId, $from, $to, CategoryType::Expense));
|
||||
|
||||
$net = $income - $expense;
|
||||
$savingsRate = $income > 0 ? round((($income - $expense) / $income) * 100, 1) : 0;
|
||||
|
||||
return [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'net' => $net,
|
||||
'savings_rate' => $savingsRate,
|
||||
];
|
||||
return CashflowSummaryService::summarize($income, $expense);
|
||||
}
|
||||
|
||||
private function getTransactionSum(string $userId, Carbon $from, Carbon $to, CategoryType $type): int
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class CashflowSummaryService
|
||||
{
|
||||
/**
|
||||
* Derive the shared cashflow summary from already-clamped income and
|
||||
* expense totals (both non-negative, in minor units). Kept in one place so
|
||||
* the net and savings-rate math can never drift between the dashboard and
|
||||
* the cashflow analytics endpoints.
|
||||
*
|
||||
* @return array{income: int, expense: int, net: int, savings_rate: float|int}
|
||||
*/
|
||||
public static function summarize(int $income, int $expense): array
|
||||
{
|
||||
return [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'net' => $income - $expense,
|
||||
'savings_rate' => $income > 0 ? round((($income - $expense) / $income) * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use App\Services\CashflowSummaryService;
|
||||
|
||||
it('derives net and savings rate from income and expense', function () {
|
||||
expect(CashflowSummaryService::summarize(100000, 40000))->toBe([
|
||||
'income' => 100000,
|
||||
'expense' => 40000,
|
||||
'net' => 60000,
|
||||
'savings_rate' => 60.0,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a negative net when expense exceeds income', function () {
|
||||
expect(CashflowSummaryService::summarize(0, 10000))->toBe([
|
||||
'income' => 0,
|
||||
'expense' => 10000,
|
||||
'net' => -10000,
|
||||
'savings_rate' => 0,
|
||||
]);
|
||||
});
|
||||
|
||||
it('avoids division by zero when income is zero', function () {
|
||||
$summary = CashflowSummaryService::summarize(0, 0);
|
||||
|
||||
expect($summary['savings_rate'])->toBe(0)
|
||||
->and($summary['net'])->toBe(0);
|
||||
});
|
||||
|
||||
it('rounds the savings rate to one decimal place', function () {
|
||||
// (100000 - 33333) / 100000 * 100 = 66.667 -> 66.7
|
||||
expect(CashflowSummaryService::summarize(100000, 33333)['savings_rate'])->toBe(66.7);
|
||||
});
|
||||
Loading…
Reference in New Issue