whisper-money/app/Http/Requests/Settings
Víctor Falcón c9a70ffa1a
feat(accounts): count shared accounts at the owner's percentage (#750)
## Why

Some accounts are shared. A joint account funded 50/50 with a partner
holds money that is only half yours, so its expenses and income should
only count towards your figures by half — otherwise the dashboard and
the cashflow screen overstate both sides of every month.

## What

An account can now be configured with **the share of it you actually
own**, in Settings → Accounts → Edit account.

- **Transactions are always weighted** by that percentage. This covers
the cashflow screen (summary, sankey, trend, category breakdown) and the
dashboard (cashflow summary, monthly spending, top categories).
- **Balances stay at face value by default**, so an account keeps
matching what the bank shows. An opt-in checkbox — only offered below
100% — applies the same percentage to the balance as well, which then
flows through net worth, its evolution, and the account cards.

Defaults are 100% / opt-in off, so existing users see no change at all.

## How

The share is computed at exactly two choke points, one per code path:

- **PHP** — `ConvertsTransactionCurrency::convertTransactionAmount()`
applies `Account::shareOfAmount()` after the currency conversion,
covering every row-by-row analytics consumer.
- **SQL** — `Transaction::OWNED_AMOUNT_SQL` plus the
`joinOwningAccount()` scope weigh the four aggregate queries that never
hydrate models.

Balances go through `BalanceLookup::forAccounts()`, the single point
every net-worth, evolution and account-metrics reader already shares —
so they all stay consistent for free, while the surfaces that must show
the real bank figure (the balance editor, imports, bank sync) read
`account_balances` directly and are untouched.

`ownership_percentage` is a **signed** `tinyInteger` on purpose: MySQL
promotes `signed * unsigned` to `BIGINT UNSIGNED`, which overflows on
the negative amount of an expense. A test caught this.

## Deliberate boundaries

- **Transaction rows keep showing the real amount.** A row should say
what the bank actually charged; only totals are your share.
- **The balance editor writes the full amount**, and now says so when
the account is shared — that is what stops a shared balance being halved
again on every correction.
- **Percentages are whole numbers, 1–100.** A three-way split (33.33%)
is not expressible yet.
- **Configurable on edit only.** `StoreAccountRequest` and the create
form are unchanged, so a new joint account counts at 100% until you edit
it.

## Known gap (follow-up)

**Budgets are not weighted.** `budget_transactions.amount` is a snapshot
written at assignment time, so a 50% account still counts at 100% in
budget spend, carry-over and threshold alerts. Fixing it properly needs
the write path weighted, a re-snapshot when the percentage changes, and
a backfill of existing rows — a separate PR rather than a line in this
one. Until then a shared account can read €400 on the dashboard and €800
in Budgets for the same category.

Loan/mortgage *projections* also seed off the raw latest balance, so a
shared **loan** with the balance opt-in on draws a step at today's date.
Narrow, and follow-up material.

## Testing

`tests/Feature/SharedAccountOwnershipTest.php` covers every weighted
endpoint, net worth with and without the balance opt-in, the 1–100
validation, and pins the PHP and SQL rounding to the same answer on an
uneven share (33% of an odd amount).

Manual QA on real local data (June 2026, account "Daily" set to 50%):

| | Before | After |
|---|---|---|
| Cashflow income | €4,460 | €4,057 (−€403 = half of the account's
€805.80) |
| Cashflow expenses | €4,122 | €3,217 (−€905 = half of the account's
€1,809.85) |
| Net worth (opt-in off) | 30,516,453 | 30,516,453 — unchanged |
| Net worth (opt-in on) | 30,516,453 | 30,470,797 (−45,656 = half of the
€913.12 balance) |

Also verified: the balance editor still prefills the real €244,310.08
and shows the shared-account notice; an empty percentage field leaves
the setting untouched instead of resetting to 100%; out-of-range values
are rejected.

## Demo


https://github.com/user-attachments/assets/de47e41a-ff72-4b2b-8e5b-51076e048504


<!-- PLACEHOLDER: drag the QA video here -->
2026-08-11 13:26:54 +00:00
..
Concerns feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
ApplyAutomationRuleRequest.php Apply automation rules to existing transactions (#413) 2026-05-22 08:36:18 +02:00
DeleteCategoryRequest.php feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
ProfileUpdateRequest.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
StoreAccountRequest.php refactor(requests): share account detail validation rules (#481) 2026-06-03 19:01:03 +02:00
StoreAutomationRuleRequest.php fix(ai): stop a learned rule title from 500-ing the user's category change (#741) 2026-08-09 15:39:14 +00:00
StoreBankRequest.php Accounts: Allow users to create custom banks with his own logo, and name (#12) 2025-12-04 15:24:04 +01:00
StoreCategoryRequest.php feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
StoreLabelRequest.php feat: add transaction labels feature (#24) 2025-12-13 13:02:19 +01:00
StoreMcpTokenRequest.php feat(mcp): add write tools (Phase 2) (#690) 2026-07-17 15:25:03 +00:00
TwoFactorAuthenticationRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateAccountRequest.php feat(accounts): count shared accounts at the owner's percentage (#750) 2026-08-11 13:26:54 +00:00
UpdateAutomationRuleRequest.php refactor(requests): dedupe UpdateAutomationRuleRequest (#476) 2026-06-03 17:29:23 +02:00
UpdateBudgetNotificationPreferencesRequest.php feat(budgets): add per-budget email notifications (#731) 2026-07-24 12:52:03 +02:00
UpdateCategoryRequest.php feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
UpdateChartColorSchemeRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateLabelRequest.php feat: add transaction labels feature (#24) 2025-12-13 13:02:19 +01:00
UpdateNetWorthChartLoanPreferenceRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateNetWorthChartRealEstatePreferenceRequest.php chore: upgrade Laravel 12 to 13 (#242) 2026-03-25 12:56:33 +00:00
UpdateNotificationPreferencesRequest.php feat(settings): let users disable bank transactions email (#472) 2026-06-02 12:24:39 +02:00
UpdateTimezoneRequest.php fix(user): persist detected timezones (#296) 2026-04-16 11:36:57 +01:00