feat(accounts): per-account owner override#1909
Merged
Merged
Conversation
…ounts discovered on sync SimpleFIN is a bridge: one access URL spans every bank linked at the bridge. The prior integration modeled it like a per-bank provider — it prompted for a setup token in the Add-connection flow and never re-discovered accounts after connect, so banks added at the bridge later never appeared. This reworks it to reflect what SimpleFIN actually is. Sync-time account discovery (the real backend gap): - Provider.SyncResult gains an Accounts field. SimpleFIN populates the full deduped account set each sync; fixed-set providers (Plaid, CSV) leave it nil. - The sync engine upserts discovered accounts (metadata only, never balances) via a new UpsertAccountMetadata query before processing transactions, refreshing its account caches. Banks linked at the bridge after connect now flow in automatically. Settings-managed singleton token: - The token is pasted in Settings → Providers → SimpleFIN (side drawer), not the per-bank flow. At most one active connection per household: GetActiveConnectionByProvider resolves the singleton, so the handler creates on first claim or rotates the credential in place — re-pasting can no longer orphan accounts. - Add-connection shows SimpleFIN as a non-selectable informational row linking out to the bridge and to Settings, reinforcing the one-token mental model. Tests: unit coverage for deduped account discovery across sync windows; integration coverage for mid-sync account creation + transaction resolution onto the new account. Docs and provider rules updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ect path + collapse dedup Addresses two review findings on the bridge-model PR: 1. Singleton enforced in one place. ProvidersSaveSimpleFINHandler creates or rotates the household's single SimpleFIN connection, but the generic /-/exchange-token handler still accepted provider=simplefin and called RegisterNewConnection unconditionally — a second creation path that bypassed the GetActiveConnectionByProvider singleton and left duplicate connections the Settings drawer (LIMIT 1) couldn't see or manage. Reject simplefin there (Settings is the one connect surface; relink rotates in place via reauthSimplefin). This also lets us delete the now-unreachable SimpleFIN-specific branches in ExchangeTokenHandler (institution-name override + daily-schedule assignment). 2. One dedup authority. The provider re-collected the full account set on every sync window and deduped it with a map, duplicating the engine's generic cross-result dedup. The bridge returns the full set on every window (the date range only bounds nested transactions), so capture it once and let the engine remain the single dedup point. Adds a unit test asserting /-/exchange-token rejects SimpleFIN with 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er-connection + sync-discovery Reverses the singleton bridge model from this PR's earlier commits and returns SimpleFIN to the normal per-connection flow (like Plaid/Teller): each pasted setup token creates its own connection, attributed to a household member, and a household can have multiple SimpleFIN bridges. The singleton bought nothing that per-connection didn't already give — one token already covers every bank at a bridge — while blocking multi-bridge households and fighting the connections model. Kept (the genuinely valuable half): sync-time account discovery. Each SimpleFIN connection re-reads its full account set every sync and the engine upserts new accounts (metadata only, never balances) before processing transactions, so banks added at the bridge after connect flow in automatically. This is model-agnostic and works the same per connection. Reverted to pre-PR state: the Settings token drawer + singleton create-or-rotate handler, the GetActiveConnectionByProvider singleton query, the /-/exchange-token SimpleFIN reject, and the connect-flow UI (restores the inline token-paste step). Docs updated to describe per-connection + sync discovery. Per-account owner editing (for a single bridge shared across members) is tracked as a separate, provider-agnostic follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address code-review findings on the discovery path: - engine: reset pendingAccounts + seen-set on ErrSyncRetryable so a retried sync rebuilds the discovery buffers from scratch - simplefin: restore union-across-windows account dedup (a single window can omit accounts; capture-once dropped them) - integration test: assert a pre-existing account stays exactly one row after discovery, guarding against duplication Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EtFunz2hhqiMwRnWk6gdvE
Add a nullable, provider-agnostic accounts.owner_user_id that overrides
the bank connection's owner for a single account. Needed when one
connection — e.g. a SimpleFIN bridge — spans accounts belonging to
different household members.
Attribution resolves at read time via
COALESCE(t.attributed_user_id, a.owner_user_id, bc.user_id), so
reassigning an account's owner re-routes its existing transactions
across every per-user total with no backfill. Precedence: per-transaction
override (account links) > per-account owner > connection owner.
- migration: additive owner_user_id FK + partial index
- queries: join owner columns on the account SELECTs; ListAccountsByUser
now filters on the effective owner; new UpdateAccountOwner; account-links
GetDependentUserID resolves the effective owner too
- service: UpdateAccountParams.OwnerUserID (set/clear), owner fields on
AccountResponse; COALESCE chain threaded through transactions /
summaries / category aggregation
- REST: owner_user_id on PATCH /accounts/{id} (+ openapi)
- admin UI: editable Owner select in the account-detail Settings section
(editors only), POST /-/accounts/{id}/owner
- docs: data-model, api-reference/endpoints, simplefin-integration, CLAUDE.md
- tests: attribution re-routing, effective-owner account listing, set/clear
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtFunz2hhqiMwRnWk6gdvE
…erride # Conflicts: # docs/simplefin-integration.md
The "Inherit from connection (<name>)" option overflowed the narrow settings cell — daisy 5's appearance:base-select ignores text-overflow on the <select>, so a long selected value bled over the dropdown arrow and out of the card (verified: select scrollWidth 286 vs clientWidth 214; card gained 52px of horizontal overflow). Drop the appended connection-owner name; "Inherit from connection" fits cleanly (214/214). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EtFunz2hhqiMwRnWk6gdvE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-account owner override
Follow-up to #1877 (SimpleFIN sync-time account discovery). A single SimpleFIN bridge can pull in accounts belonging to different household members — but until now an account's owner was inherited from its bank connection and was immutable. This adds a nullable, provider-agnostic
accounts.owner_user_idthat overrides the connection owner for a single account.How it works
Ownership resolves at read time:
Precedence: per-transaction override (account links) > per-account owner (new) > connection owner. Because it's resolved at read time, reassigning an account's owner re-routes its existing transactions across every per-user total with no backfill.
Scope
PATCH /api/v1/accounts/{id}gainsowner_user_id; reads updated everywhere (transactions, summaries, category aggregation, account lists → MCP read tools inherit it).Changes
owner_user_idFK + partial index (mirrorstransactions.attributed_user_id).ListAccountsByUserfilters on the effective owner; newUpdateAccountOwner; account-linksGetDependentUserIDresolves the effective owner too. These account SELECTsLEFT JOIN bank_connectionsso an override still resolves when a connection was removed (connection_idSET NULL, history preserved).UpdateAccountParams.OwnerUserID(set via short_id, clear via""); owner fields onAccountResponse; COALESCE chain threaded through transactions / summaries / categories.owner_user_idon the PATCH body (+ openapi);internal/clientAccount/AccountPatch parity.POST /-/accounts/{id}/owner; inline Owner select replacing the old read-only "Family Member" field.Not changed (deliberate)
WipeUserDatastays connection-scoped (deletes the connections you own + everything physically under them). Foldingowner_user_idin would let "wipe Alice" delete accounts living under Bob's still-live bridge — strictly worse. The override is an attribution concept, not connection ownership.Tests
connection_idNULL + owner set still surfaces under the override owner).go build/vet/go test ./...clean; integration green acrossservice/db/api/sync/admin.🤖 Generated with Claude Code