whisper-money/resources/js/components/transactions
Víctor Falcón 85c74604b2
feat(transactions): allow manual transactions on bank-connected accounts (#747)
## Why

You could not add a transaction by hand to a bank-connected account.
There was no good reason for it: bank sync **only inserts** rows it has
not seen before (dedup runs on `dedup_fingerprint` /
`external_transaction_id`, both `null` on manual rows) and **never
deletes or updates**, so a hand-entered transaction survives every later
sync untouched.

The one thing that genuinely does not make sense is letting a user set a
**balance** on a connected account, because the next sync overwrites it.
That restriction stays.

## What was actually blocking it

Less than it looked. The HTTP endpoint already allowed it,
`ManualBalanceAdjuster` already skipped connected accounts, and the
transaction dialog already handled them (it forces `updateBalance:
false`). Only two surfaces blocked it:

- **MCP** — `WriteTool::writableAccount()` rejected every connected
account for *all* writes. Split into `accountInSpace()` (no connection
check — used by `create_transaction` / `update_transaction`) and
`balanceWritableAccount()` (still rejects connected — used by
`create_balance`).
- **The account detail page** — hid its "Add transaction" button for
connected accounts, even though the same dialog on the transactions page
already offered them in its account picker.

## Also in here

Two problems the change surfaced, both fixed:

- **`EnableBankingSyncer` linked-account watermark** took the newest
transaction of *any* source. With manual rows now able to land on a
connected account, one dated later than the bank's last posting would
shrink the fetch window and skip the bank history in between —
permanently, since the watermark only moves forward. On the QA data this
would have skipped **36 days**. Now restricted to bank-sourced rows.
- **`calculateHistoricalBalances`** derives history by walking back from
a bank-provided reference balance, summing transactions unfiltered.
Counting a hand-entered row subtracts money the bank never had. Now
walks bank-sourced rows only. (Safe before only because manual rows
could not reach a connected account.)

Plus the honesty/copy work:

- `ManualBalanceAdjuster` returns whether it shifted anything, so
`create` / `update` / `delete_transaction` all report `balance_updated`
instead of silently no-opping and letting the agent claim a balance
moved.
- The **OAuth consent screen** and the AI Connector settings page said
"bank-connected accounts stay read-only". That was a trust statement,
and it is no longer true — both now say bank-*synced transactions*
cannot be edited or deleted and connected balances stay untouched.
- `create_balance`'s description no longer contradicts the server
instructions shipped alongside it.
- The transaction dialog now *explains* why the "Update account balance"
checkbox is absent on a connected account instead of just hiding it (it
defaults to on and is localStorage-persisted, so it used to vanish
mid-form with no reason given).

## What stays blocked

- Balances on connected accounts — MCP `create_balance` rejects them,
the adjuster no-ops, the UI shows the explanation instead of the
checkbox.
- Editing or deleting bank/imported transactions — still gated on
`source === manually_created`, unchanged.

## Testing

- `create_transaction` on a connected account leaves its balances alone;
moving a transaction onto a connected account unwinds only the manual
side it came from.
- A manual row survives a sync and does not block the bank's own rows —
the invariant the whole change rests on, previously untested.
- A manual transaction does not move the linked-account sync window.
- The account page offers "Add transaction" on connected accounts, and
hides it on non-transactional ones.
- Full suite: 2047 passing, phpstan clean.

QA'd in the browser end to end (create from a connected account, edit it
afterwards, contrast with a manual account, switch accounts mid-form)
and over real MCP calls against the running server (`create_transaction`
returned `balance_updated: false` on connected / `true` on manual;
`create_balance` refused the connected account).

## Demo


https://github.com/user-attachments/assets/7a7f1cbb-8f68-402d-ba35-3288dd7ea77f


<!-- PLACEHOLDER: drag the video here -->
2026-08-10 05:32:40 +02:00
..
ai-upsell-sample.test.ts AI auto-categorization: open to pro + consent, nudge free users (#561) 2026-06-19 14:08:40 +00:00
ai-upsell-sample.ts AI auto-categorization: open to pro + consent, nudge free users (#561) 2026-06-19 14:08:40 +00:00
bulk-actions-bar.tsx Fix label creation dropdown refresh (#363) 2026-05-07 10:26:43 +01:00
bulk-category-select.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
bulk-label-select.tsx Fix label creation dropdown refresh (#363) 2026-05-07 10:26:43 +01:00
categorizer-card.tsx feat(categorize): show debtor and creditor names (#454) 2026-05-30 12:27:59 +02:00
categorizer-command.tsx feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
category-cell.tsx feat(ai): learn from category corrections so the AI stops repeating the same mistake (#608) 2026-06-29 19:12:15 +02:00
category-select.tsx feat(shared): Add CategoryCombobox component 2025-11-14 16:05:53 +01:00
edit-transaction-dialog.test.tsx feat(transactions): allow manual transactions on bank-connected accounts (#747) 2026-08-10 05:32:40 +02:00
edit-transaction-dialog.tsx feat(transactions): allow manual transactions on bank-connected accounts (#747) 2026-08-10 05:32:40 +02:00
import-step-account.test.tsx Fix transaction importer account preselect (#396) 2026-05-14 12:38:44 +02:00
import-step-account.tsx Fix transaction importer account preselect (#396) 2026-05-14 12:38:44 +02:00
import-step-mapping.tsx fix(balances): allow saving a zero balance (#664) 2026-07-10 15:02:38 +02:00
import-step-preview.tsx feat(transactions): serve import dedup and account ledger from the backend (#631) 2026-07-03 16:49:59 +02:00
import-step-upload.test.tsx Fix transaction importer account preselect (#396) 2026-05-14 12:38:44 +02:00
import-step-upload.tsx Fix transaction importer account preselect (#396) 2026-05-14 12:38:44 +02:00
import-transactions-button.tsx refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
import-transactions-drawer.tsx feat(import): persist per-account import configuration on the backend (#698) 2026-07-18 16:10:40 +02:00
saved-filters.tsx fix(transactions): keep saved-filter delete button visible on touch and confirm before deleting (#648) 2026-07-06 09:37:38 +00:00
transaction-actions-menu.test.tsx feat(transactions): release transaction analysis to all users (#579) 2026-06-22 16:09:09 +02:00
transaction-actions-menu.tsx feat(transactions): release transaction analysis to all users (#579) 2026-06-22 16:09:09 +02:00
transaction-analysis-drawer.test.tsx feat(transactions): add a monthly trend view to the analysis drawer (#736) 2026-07-26 17:03:52 +02:00
transaction-analysis-drawer.tsx feat(transactions): add a monthly trend view to the analysis drawer (#736) 2026-07-26 17:03:52 +02:00
transaction-columns.test.tsx fix(transactions): pad Category column when Date column is hidden (#584) 2026-06-22 16:51:50 +00:00
transaction-columns.tsx fix(transactions): let date column size to its content (#610) 2026-06-29 15:51:51 +00:00
transaction-description.test.tsx refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
transaction-description.tsx refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
transaction-filters.test.tsx feat(transactions): reorder filters and switch accounts to a logo dropdown (#598) 2026-06-26 20:09:02 +02:00
transaction-filters.tsx feat(transactions): reorder filters and switch accounts to a logo dropdown (#598) 2026-06-26 20:09:02 +02:00
transaction-list.test.tsx fix(accounts): remove double-skeleton flash on account show (#634) 2026-07-03 15:56:49 +00:00
transaction-list.tsx feat(transactions): add delete button to the transaction edit modal (#697) 2026-07-18 11:03:09 +00:00