Transfer Management#
A Transfer in Sure is a join record linking two Transaction rows: one outflow from the source account and one inflow to the destination account . Creation is handled by Transfer::Creator, a service object that wraps both legs plus optional fee transactions inside a single database transaction . On success it calls sync_later on both accounts . The controller entry point is TransfersController.
Kind Assignment#
The outflow transaction's kind is determined by Transfer::Creator#outflow_transaction_kind based on the destination account type:
| Destination account | kind |
|---|---|
loan? | loan_payment |
liability? (credit card or other) | cc_payment |
investment? or crypto? — and source is not | investment_contribution |
| everything else | funds_movement |
The inflow leg always receives kind: "funds_movement" regardless of account type .
When kind == "investment_contribution", the outflow transaction is also auto-assigned the family's Investment Contributions category .
Budget Inclusion and Reporting#
Two constants in Transaction drive how these kinds flow through analytics :
TRANSFER_KINDS—[funds_movement, cc_payment, loan_payment, investment_contribution]— identifies all transfer legs for search filter splits.BUDGET_EXCLUDED_KINDS—[funds_movement, one_time, cc_payment]— only these are dropped from budget/income-statement queries.
loan_payment and investment_contribution are intentionally absent from BUDGET_EXCLUDED_KINDS because they represent real cash outflow . In IncomeStatement::Totals, transactions with either of those kinds are forced into the expense bucket regardless of the raw entry amount sign .
Known reporting tensions#
- Investment contributions as expenses — transfers to investment/crypto accounts count as expenses in Reports. This is intentional by design but widely debated; PR #1314 proposed excluding them and was closed. A user-settable toggle (PR #1999) is under review .
- Mortgage / loan-payment double-counting —
loan_paymentis always counted as expense. For mortgages where interest is also imported as a separate transaction, this produces double-counting. Special-casingmortgagesubtype accounts asfunds_movementhas been raised as a potential fix .
Bug: Investment→Investment Transfer Misclassification#
Issue: Auto-matched investment-to-investment transfers are misclassified as investment_contribution (counted as a budget expense) instead of funds_movement (excluded from budget).
Root cause: Three code paths that call Transfer.kind_for_account pass only the destination account and therefore miss the "source is also investment" guard present in Transfer::Creator#outflow_transaction_kind :
app/models/family/auto_transfer_matchable.rb:58app/controllers/transfer_matches_controller.rb:21app/models/rule/action_executor/set_as_transfer_or_payment.rb:24
All three carry a comment claiming they match Transfer::Creator logic, but only pass one account .
auto_match_transfers! runs on every sync, so any investment↔investment pair (brokerage→crypto, crypto→brokerage, etc.) is affected in normal operation.
Suggested fix: Add an optional source_account: keyword to Transfer.kind_for_account that mirrors the !source_is_investment? guard .
Transfer Details Drawer (UI)#
The Transfer Details drawer is rendered from app/views/transfers/show.html.erb and follows a two-phase editing pattern:
- Overview section (always open) — read-only display of
from/toaccounts, dates, amounts, and any fees for both legs . - Details section (collapsed, auto-submitting form) — category selector (only rendered when
@transfer.categorizable?is true, i.e. loan destination only),DS::TagSelectfor tag management, and a notes textarea. The category and tag fields usedata-auto-submit-form-target: "auto"which submits the form on change via theauto-submit-formStimulus controller. Tags applied to a transfer are automatically applied to both the outflow and inflow transactions . - Settings section — contains "mark as recurring" (behind feature flag + write-access guard) and a destructive delete button .
On save, TransfersController#update runs three operations inside a single transaction: status change (confirm! / reject!), fee/amount edits, and detail updates . The Turbo Stream response (update.turbo_stream.erb) replaces both the inflow and outflow entry rows and their category-menu partials inline without a full page reload .
Destroying a transfer resets both linked transaction kind values back to "standard" before deletion to prevent orphaned kinds .
Key Files#
| File | Role |
|---|---|
app/models/transfer/creator.rb | Creates paired transactions; authoritative kind routing; accepts tag_ids parameter and applies family-scoped tags to both transactions |
app/controllers/transfers_controller.rb | CRUD + mark_as_recurring endpoint; update_tags action handles tag updates via PATCH /transfers/:id/tags |
app/views/transfers/show.html.erb | Transfer Details Drawer template; includes DS::TagSelect for tag management |
app/views/transfers/_form.html.erb | New transfer form; includes DS::TagSelect for choosing tags during creation |
app/views/transfers/update.turbo_stream.erb | Turbo Stream response for inline updates |
app/models/transfer.rb | Model, validations, kind_for_account, destroy!, reject! |
app/models/transaction.rb | TRANSFER_KINDS, BUDGET_EXCLUDED_KINDS constants |