feat(up): flag internal transfers and round-ups as funds_movement#2460
feat(up): flag internal transfers and round-ups as funds_movement#2460threatsurfer wants to merge 2 commits into
Conversation
Up populates relationships.transferAccount on transactions that move money between the user's own accounts (including round-ups swept into a Saver), but flatten_transaction dropped it, so these imported as ordinary income/expense and distorted budgets and cashflow. - Provider::Up#flatten_transaction: lift transfer_account_id from relationships.transferAccount.data.id. - UpEntry::Processor: import transfers as funds_movement and persist transfer_account_id in extra["up"]. - Account::ProviderImportAdapter#import_transaction: optional kind: param; an explicit provider kind takes precedence over account-type auto-detection and is applied after the sync-protection check, so user re-categorisations survive re-sync. Complementary to Family#auto_match_transfers!: two-sided transfers between linked accounts are still paired into a Transfer (the matcher does not filter on kind); one-sided movements and round-ups, which the matcher cannot pair, are the cases this fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesUp Internal Transfer Detection
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a8a3fa2de
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| # precedence over the account-type auto-detection below: a provider such as Up that | ||
| # flags internal transfers and round-ups (via relationships.transferAccount) has | ||
| # authoritative knowledge that the movement is a transfer, so we honour it directly. | ||
| auto_kind = kind.presence |
There was a problem hiding this comment.
Preserve Up loan repayments as loan_payment
When an Up HOME_LOAN account is linked (UpAccount::UP_ACCOUNT_TYPE_MAP maps HOME_LOAN to Loan), repayments from another Up account can also carry transferAccount. Because UpEntry::Processor#kind now passes funds_movement and this line skips the existing account.accountable_type == "Loan" && amount.negative? branch below, those repayments are reclassified from budgeted loan_payment to budget-excluded funds_movement. Keep the loan account-type classification ahead of the provider transfer override, or only pass the provider kind for depository accounts.
Useful? React with 👍 / 👎.
jjmata
left a comment
There was a problem hiding this comment.
Good approach and well-reasoned. A few things to verify or watch:
Kind precedence vs. protection check – The description says the explicit kind: param is "applied after the protection check, so user re-categorisations survive re-sync." Looking at the diff, auto_kind = kind.presence is set before the nil-guard block, but I can't see the downstream protection-check code in this diff. Please confirm the protection gate (entry.transaction.kind_changed_by_user? or equivalent) still gates auto_kind even when it's set from the provider. If a user manually re-categorised a transfer as "standard" and then a re-sync runs, we'd want that to survive.
One-sided movements and future pairing – The PR description acknowledges auto_match_transfers! still filters on amount/date/currency, so funds_movement entries tagged with a transfer_account_id could still be paired if both accounts are linked. The transfer_account_id stored in extra["up"] is a solid hook for a follow-up that builds the Transfer record deterministically rather than relying on the probabilistic matcher. Worth filing as a follow-up issue so it doesn't get lost.
Test: FakeResponse stub – The new test stubs Provider::Up.get with a lambda that ignores the query: keyword. If get_account_transactions ever passes query params (pagination, date filters), the stub silently swallows them. For the purpose of this test that's fine, but a note in the stub would help future readers understand the omission is intentional.
Generated by Claude Code
Codex review caught that Up HOME_LOAN accounts map to a Loan account, so a repayment carrying transferAccount would be reclassified from loan_payment to funds_movement (budget-excluded). Make the provider kind: a fallback: activity-label and account-type classification now take precedence, so loan_payment and cc_payment survive. Adds a regression test (loan repayment stays loan_payment), a depository-applies test, and a note that the up_test stub ignores query: intentionally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @jjmata, really helpful review. Protection check (point 1). Confirmed. Loan accounts (Codex's catch). Good one. Since Deterministic pairing (point 2). Agree. The Stub note (point 3). Added a comment clarifying the |
Summary
Up sets
relationships.transferAccounton any movement between a user's own accounts (including round-ups swept into a Saver).flatten_transactioncurrently drops it, so these import as ordinary income/expense and distort budgets, cashflow, and the income statement. This PR imports them asfunds_movement(Sure's budget-excluded transfer kind).Why it matters (real data)
I run an independent Up integration on my own data (~7yr, ~26k txns, 9 accounts incl. a 2Up joint account and Savers). In a recent 536-txn snapshot, 299 (56%) had a non-null
transferAccount(e.g. a -$500 'Transfer to 2Up Spending'). Across full history they number in the thousands. All currently land as income/expense.Relationship to
auto_match_transfers!Complementary, not a replacement. Two-sided transfers between linked accounts are still paired into a Transfer (the matcher filters on amount/date/currency, not on kind). One-sided movements (counterpart not linked) and round-ups cannot be paired by the matcher; honouring
transferAccountis the only thing that classifies these correctly.Changes
Provider::Up#flatten_transactionliftstransfer_account_idfromrelationships.transferAccount.data.id.UpEntry::Processorimports transfers asfunds_movementand storestransfer_account_idinextraunder theupkey.Account::ProviderImportAdapter#import_transactiongains an optionalkind:param; an explicit provider kind wins over account-type auto-detection and is applied after the protection check, so user re-categorisations survive re-sync.Scope / design
Kept to marking. A deterministic Transfer record from the
transferAccountid is a sensible follow-up PR. Went with a shared-adapterkind:param (mirrorsinvestment_activity_label:, respects user edits); happy to switch to an Up-contained approach if you prefer.Tests
Processor:
transferAccountmaps tofunds_movement; an ordinary transaction staysstandard. Provider: flatten extractstransfer_account_id(nil when absent).Summary by CodeRabbit
New Features
Bug Fixes
Tests