whisper-money/app/Http/Controllers/Api/Concerns
Víctor Falcón 986f43705a
fix(analysis): respect category types like the cashflow screen (#612)
## Why

On the **analysis** drawer, income/expense were derived from each
transaction's amount sign alone, ignoring the category type.
Transactions in `transfer` / `savings` / `investment` categories
therefore leaked into income/expense totals and every breakdown — e.g.
moving money between your own accounts inflated both sides. The
**cashflow** screen already keys off the category type; analysis now
does the same.

## What changed

Three commits:

1. **fix(analysis): exclude transfer, savings and investment categories
from income and expense**
Only `expense` categories (or uncategorized outflows) count as expenses,
and only `income` categories (or uncategorized inflows) count as income.
`transfer` / `savings` / `investment` are internal movements and sit on
neither side — identical to how cashflow computes income/expense/net.

2. **refactor(analytics): de-duplicate currency conversion and
category-type helpers**
`convertTransactionAmount()` / `preloadExchangeRates()` were
byte-identical in three controllers, and `categoryType()` in two.
Extracted the currency helpers into a shared
`Api\Concerns\ConvertsTransactionCurrency` trait (following the existing
`OpenBanking/Concerns` pattern) and moved `categoryType()` onto the
`Transaction` model. No behavior change.

3. **fix(analysis): net refunds within a category so totals reconcile
with cashflow**
Classification keyed off each transaction's own sign dropped contra-sign
rows entirely, so a refund booked to an expense category disappeared
instead of netting the spend down — and analysis disagreed with cashflow
on the same data (a `-10000` charge + `3000` refund read as `10000`
spent, not `7000`). A transaction's side is now decided by its category
type and signed amounts are summed before clamping, mirroring cashflow's
reconciliation across summary, category, payee, account, tag and
over-time. The largest-expenses list still shows only genuine outflows.

## Tests

Added analysis coverage for: transfer/savings/investment exclusion
(summary, breakdowns, largest, over-time), refund netting (asserts
parity with cashflow's `7000`), income-category reversals,
foreign-currency conversion, and uncategorized inflows. Full non-browser
suite green (1823 passed); `pint --test`, `bun run format`, `bun run
lint` all clean.

## Follow-up for product (not in this PR)

On **cashflow**, savings/investment outflows are excluded from expense
but re-surfaced in a dedicated "Saved & Invested" card. The **analysis**
drawer has no equivalent surface, so money categorized as
savings/investment is now correctly excluded from spending but isn't
shown anywhere. If we want analysis to fully account for it, we should
add a small "set aside" summary there. Flagged for a product decision
rather than bundled into this bugfix.
2026-06-29 21:04:18 +02:00
..
ConvertsTransactionCurrency.php fix(analysis): respect category types like the cashflow screen (#612) 2026-06-29 21:04:18 +02:00