[GH-ISSUE #5000] [Bug]: Can't merge transfers with new merge functionality #79564

Open
opened 2026-05-19 02:32:06 -05:00 by GiteaMirror · 16 comments
Owner

Originally created by @alecbakholdin on GitHub (May 16, 2025).
Original GitHub issue: https://github.com/actualbudget/actual/issues/5000

Verified issue does not already exist?

  • I have searched and found no existing issue

What happened?

Bug described here by another user @cesjn

I have been an Actual user for several months now and was really excited for the new Merge feature that came out in the latest release. In theory it should address this problem, but the current implementation seems to fall short. I have a similar situation where a transfer is made between two accounts but the transaction is duplicated in the receiving account. So I end up with the following three transactions:

Account A, $20 Payment, categorized as Transfer, matched to SimpleLink Import and marked as cleared
Account B, $20 Credit, categorized as Transfer, unmatched and not cleared (appears to result from a rule that Actual automatically created for me)
Account B, $20 Credit, Not Categorized, matched to import and marked as cleared
Before the merge function was introduced I would change the category of transaction https://github.com/actualbudget/actual/pull/1 to something else, delete transaction https://github.com/actualbudget/actual/pull/2 and then use the "make transfer" function to link https://github.com/actualbudget/actual/pull/1 and https://github.com/actualbudget/actual/pull/3 together, thus categorizing them both as a transfer (Actual then wants me to make a rule for this, which I try to always decline in order to avoid the same situation the next month when these transactions occur again).

With the merge function I had the expectation that I should simply be able to "Merge" transactions https://github.com/actualbudget/actual/pull/2 and https://github.com/actualbudget/actual/pull/3 and we should be good to go. Unfortunately, when I do so, the resulting transaction is NOT categorized as a transfer, but https://github.com/actualbudget/actual/pull/1 stays as a transfer. This results in me being unable to link https://github.com/actualbudget/actual/pull/1 and the https://github.com/actualbudget/actual/pull/2/3 merge.

So, I would propose that there is a Bug here, specifically in the new merge feature. I.e. that if you merge a "transfer" transaction with another transaction, that the resulting merged transaction should maintain the "transfer" category. The alternative would be to unlink the transaction linked to the transfer transaction (set it back to uncategorized) so that you can re-link the resulting transaction, but that seems far more complicated.

How can we reproduce the issue?

create a transfer, create a transaction. Attempt to merge the two transactions. It fails.

Where are you hosting Actual?

None

What browsers are you seeing the problem on?

No response

Operating System

None

Originally created by @alecbakholdin on GitHub (May 16, 2025). Original GitHub issue: https://github.com/actualbudget/actual/issues/5000 ### Verified issue does not already exist? - [x] I have searched and found no existing issue ### What happened? **Bug described here by another user @cesjn** I have been an Actual user for several months now and was really excited for the new Merge feature that came out in the latest release. In theory it should address this problem, but the current implementation seems to fall short. I have a similar situation where a transfer is made between two accounts but the transaction is duplicated in the receiving account. So I end up with the following three transactions: Account A, $20 Payment, categorized as Transfer, matched to SimpleLink Import and marked as cleared Account B, $20 Credit, categorized as Transfer, unmatched and not cleared (appears to result from a rule that Actual automatically created for me) Account B, $20 Credit, Not Categorized, matched to import and marked as cleared Before the merge function was introduced I would change the category of transaction https://github.com/actualbudget/actual/pull/1 to something else, delete transaction https://github.com/actualbudget/actual/pull/2 and then use the "make transfer" function to link https://github.com/actualbudget/actual/pull/1 and https://github.com/actualbudget/actual/pull/3 together, thus categorizing them both as a transfer (Actual then wants me to make a rule for this, which I try to always decline in order to avoid the same situation the next month when these transactions occur again). With the merge function I had the expectation that I should simply be able to "Merge" transactions https://github.com/actualbudget/actual/pull/2 and https://github.com/actualbudget/actual/pull/3 and we should be good to go. Unfortunately, when I do so, the resulting transaction is NOT categorized as a transfer, but https://github.com/actualbudget/actual/pull/1 stays as a transfer. This results in me being unable to link https://github.com/actualbudget/actual/pull/1 and the https://github.com/actualbudget/actual/pull/2/3 merge. So, I would propose that there is a Bug here, specifically in the new merge feature. I.e. that if you merge a "transfer" transaction with another transaction, that the resulting merged transaction should maintain the "transfer" category. The alternative would be to unlink the transaction linked to the transfer transaction (set it back to uncategorized) so that you can re-link the resulting transaction, but that seems far more complicated. ### How can we reproduce the issue? create a transfer, create a transaction. Attempt to merge the two transactions. It fails. ### Where are you hosting Actual? None ### What browsers are you seeing the problem on? _No response_ ### Operating System None
GiteaMirror added the buguser interfacetransactions labels 2026-05-19 02:32:07 -05:00
Author
Owner

@alecbakholdin commented on GitHub (May 16, 2025):

Created as a result of discussion in #4899

<!-- gh-comment-id:2885381683 --> @alecbakholdin commented on GitHub (May 16, 2025): Created as a result of discussion in #4899
Author
Owner

@karanj commented on GitHub (Sep 22, 2025):

Encountered this today, was very confused as to why it would always seem to pick the non-transfer as the merged entry.

If merge is broken when transfers are included in the merge set, is there a way to disable or caution the user trying to perform the action until a fix is developed?

<!-- gh-comment-id:3316608945 --> @karanj commented on GitHub (Sep 22, 2025): Encountered this today, was very confused as to why it would always seem to pick the non-transfer as the merged entry. If merge is broken when transfers are included in the merge set, is there a way to disable or caution the user trying to perform the action until a fix is developed?
Author
Owner

@oddmaniv commented on GitHub (Feb 17, 2026):

Really struggling with this issue - has there been any progress / alternative methods?

<!-- gh-comment-id:3912026558 --> @oddmaniv commented on GitHub (Feb 17, 2026): Really struggling with this issue - has there been any progress / alternative methods?
Author
Owner

@nicepopo86-lang commented on GitHub (Feb 17, 2026):

I’d like to work on this bounty issue. I’ll reproduce the transfer-merge bug, implement a focused fix, and open a PR with regression steps.

<!-- gh-comment-id:3912425465 --> @nicepopo86-lang commented on GitHub (Feb 17, 2026): I’d like to work on this bounty issue. I’ll reproduce the transfer-merge bug, implement a focused fix, and open a PR with regression steps.
Author
Owner

@nicepopo86-lang commented on GitHub (Feb 17, 2026):

I opened a draft PR with a focused fix and regression test for this merge path:\n\n- PR: https://github.com/actualbudget/actual/pull/7000\n\nIt preserves/relinks when a merge keeps the imported transaction, so transfer pairing stays valid instead of pointing to a deleted transaction id.

<!-- gh-comment-id:3912462248 --> @nicepopo86-lang commented on GitHub (Feb 17, 2026): I opened a draft PR with a focused fix and regression test for this merge path:\n\n- PR: https://github.com/actualbudget/actual/pull/7000\n\nIt preserves/relinks when a merge keeps the imported transaction, so transfer pairing stays valid instead of pointing to a deleted transaction id.
Author
Owner

@lmee commented on GitHub (Feb 17, 2026):

I reviewed "Can't merge transfers with new merge functionality" and read all comments before proposing a patch path.

Problem I see: - [x] I have searched and found no existing issue.
Root-cause hypothesis: inconsistent filter/query state between two rendering paths.
First patch plan: reproduce the issue against acceptance criteria, isolate the root cause in the relevant module, and submit a focused fix with regression tests.
If this aligns with maintainer expectations, I will post repro evidence first and then open the scoped patch.

<!-- gh-comment-id:3912485359 --> @lmee commented on GitHub (Feb 17, 2026): I reviewed "Can't merge transfers with new merge functionality" and read all comments before proposing a patch path. Problem I see: - [x] I have searched and found no existing issue. Root-cause hypothesis: inconsistent filter/query state between two rendering paths. First patch plan: reproduce the issue against acceptance criteria, isolate the root cause in the relevant module, and submit a focused fix with regression tests. If this aligns with maintainer expectations, I will post repro evidence first and then open the scoped patch.
Author
Owner

@cesjn commented on GitHub (Apr 23, 2026):

@oddmaniv, @karanj I am attempting to address this issue (with some help from Claude) and I have an interesting question for you (and any other user who has an opinion). If you have two sets of transfer transactions (accountATransaction1 <-> accountBTransaction2, accountATransaction3 <-> accountBTransaction4) and you merge transactions accountATransaction1 and accountAtransaction3. Let's assume that accountATransaction1 is the kept transaction. We would have accountATransaction1merged3 <-> accountBTransaction2. The question is: what should we do with accountBTransaction4? Should we remove the transfer link and leave it, or delete it?
In my personal workflow, I would prefer to delete it. BUT, I think the safer thing overall is to leave it as a non-transfer transaction so that the user can choose what to do with it.

<!-- gh-comment-id:4306319956 --> @cesjn commented on GitHub (Apr 23, 2026): @oddmaniv, @karanj I am attempting to address this issue (with some help from Claude) and I have an interesting question for you (and any other user who has an opinion). If you have two sets of transfer transactions (accountATransaction1 <-> accountBTransaction2, accountATransaction3 <-> accountBTransaction4) and you merge transactions accountATransaction1 and accountAtransaction3. Let's assume that accountATransaction1 is the kept transaction. We would have accountATransaction1merged3 <-> accountBTransaction2. The question is: what should we do with accountBTransaction4? Should we remove the transfer link and leave it, or delete it? In my personal workflow, I would prefer to delete it. BUT, I think the safer thing overall is to leave it as a non-transfer transaction so that the user can choose what to do with it.
Author
Owner

@alecbakholdin commented on GitHub (Apr 23, 2026):

@cesjn thanks for looking into this!

EDIT
I realized that I misread what you wrote. To answer the question you actually asked, I think we should delete the second transaction. If you're merging a transfer with another transfer, then the 4 transactions should be reduced to 2, and we simply perform two merges: one in account A and one in account B, using the logic already present. Just have to make sure the transfer is properly conserved even if we end up keeping accountATransaction1 and accountBTransaction4, for example

I think what you're describing is a symptom of the merge criteria not being strict enough. In my original implementation #4739 I have it set to only allow merges if the amounts are the same. I'm thinking we can add new criteria to simplify the problem statement:

I propose that we allow merges if all the below are true:

  1. Amounts are the same (currently the only requirement)
  2. Accounts are the same
  3. If both are transfers, the destination account (aka payee) must be the same

I'm struggling to come up with a real-world scenario where I would need to do what you're describing. Can you share an example where you would need to merge transfers that don't follow the above criteria?

<!-- gh-comment-id:4306961303 --> @alecbakholdin commented on GitHub (Apr 23, 2026): @cesjn thanks for looking into this! **EDIT** I realized that I misread what you wrote. To answer the question you actually asked, I think we should delete the second transaction. If you're merging a transfer with another transfer, then the 4 transactions should be reduced to 2, and we simply perform two merges: one in account A and one in account B, using the logic already present. Just have to make sure the transfer is properly conserved even if we end up keeping accountATransaction1 and accountBTransaction4, for example <del> I think what you're describing is a symptom of the merge criteria not being strict enough. In my original implementation #4739 I have it set to only allow merges if the amounts are the same. I'm thinking we can add new criteria to simplify the problem statement: I propose that we allow merges if all the below are true: 1. Amounts are the same (currently the *only* requirement) 2. Accounts are the same 3. If both are transfers, the destination account (aka payee) must be the same I'm struggling to come up with a real-world scenario where I would need to do what you're describing. Can you share an example where you would need to merge transfers that don't follow the above criteria? </del>
Author
Owner

@alecbakholdin commented on GitHub (Apr 23, 2026):

I've gone ahead and implemented a fix for this which simply adds logic to the keep/drop determination step. A transfer is now prioritized over an imported transaction for keep/drop logic.

<!-- gh-comment-id:4309120969 --> @alecbakholdin commented on GitHub (Apr 23, 2026): I've gone ahead and implemented a fix for this which simply adds logic to the keep/drop determination step. A transfer is now prioritized over an imported transaction for keep/drop logic.
Author
Owner

@cesjn commented on GitHub (Apr 24, 2026):

I will pull the branch and try it out. It might help if I describe my use case a little better:
I often have bank transfers or credit card payments that show up from import on different days (i.e. the payment leaves my checking account on 4/22 but isn't credited to my credit card until 4/23 or later (if there is a weekend or holiday involved). Since actual has "learned" that these transactions are transfers, it will assign them as such when they are imported. Since the import doesn't match the two (due to the different dates) this results in the 4 transaction scenario described above:
CheckingTransaction1 which is imported/cleared and linked as a transfer to CreditCardPayment2 (uncleared and no matching import)
CreditCardPayment3 which is imported/cleared and linked as a transfer to CheckingTransaction4 (uncleared and no matching import)

In an ideal world I would be able to merge either CheckingTransaction1 and CheckingTransaction4 or CreditCardPayment2 and CreditCardPayment3 and end up with CheckingTransaction1 and CreditCardPayment3 (since these are the imported transactions and the dates will match the respective bank statements, etc.).

Obviously there is an alternative means to this end: namely remove or re-work the import rules to not mark the imported transactions as transfers and then manually link them as transfers after both are imported, but this is a less than intuitive workflow, especially for new users.

<!-- gh-comment-id:4310418259 --> @cesjn commented on GitHub (Apr 24, 2026): I will pull the branch and try it out. It might help if I describe my use case a little better: I often have bank transfers or credit card payments that show up from import on different days (i.e. the payment leaves my checking account on 4/22 but isn't credited to my credit card until 4/23 or later (if there is a weekend or holiday involved). Since actual has "learned" that these transactions are transfers, it will assign them as such when they are imported. Since the import doesn't match the two (due to the different dates) this results in the 4 transaction scenario described above: CheckingTransaction1 which is imported/cleared and linked as a transfer to CreditCardPayment2 (uncleared and no matching import) CreditCardPayment3 which is imported/cleared and linked as a transfer to CheckingTransaction4 (uncleared and no matching import) In an ideal world I would be able to merge either CheckingTransaction1 and CheckingTransaction4 or CreditCardPayment2 and CreditCardPayment3 and end up with CheckingTransaction1 and CreditCardPayment3 (since these are the imported transactions and the dates will match the respective bank statements, etc.). Obviously there is an alternative means to this end: namely remove or re-work the import rules to not mark the imported transactions as transfers and then manually link them as transfers after both are imported, but this is a less than intuitive workflow, especially for new users.
Author
Owner

@cesjn commented on GitHub (Apr 24, 2026):

Had a chance to test out the branch. It just so happens that in my current budget file I have a situation where I have CreditCardPayment1 on 4/21 (cleared with import info in notes, no category)
CheckingTransaction2 on 4/22 (cleared with import info in notes) linked as transfer to an auto-generated CreditCardPayment3 (uncleared, note matches CheckingTransaction2)
When I merge CreditCardPayment1 with CreditCardPayment3 I end up with the CreditCardPayment3 (4/22 date, cleared, note matches CheckingTransaction2).
While not perfect (it would be better to retain the 4/21 date for the CreditCardPayment), it is probably good enough.

The other scenario I need to test is when one of the accounts is off budget, thus requiring a category other than transfer for the on budget transaction.

<!-- gh-comment-id:4310758843 --> @cesjn commented on GitHub (Apr 24, 2026): Had a chance to test out the branch. It just so happens that in my current budget file I have a situation where I have CreditCardPayment1 on 4/21 (cleared with import info in notes, no category) CheckingTransaction2 on 4/22 (cleared with import info in notes) linked as transfer to an auto-generated CreditCardPayment3 (uncleared, note matches CheckingTransaction2) When I merge CreditCardPayment1 with CreditCardPayment3 I end up with the CreditCardPayment3 (4/22 date, cleared, note matches CheckingTransaction2). While not perfect (it would be better to retain the 4/21 date for the CreditCardPayment), it is probably good enough. The other scenario I need to test is when one of the accounts is off budget, thus requiring a category other than transfer for the on budget transaction.
Author
Owner

@alecbakholdin commented on GitHub (Apr 24, 2026):

I see what you mean. I think rather than modifying the keep/drop logic what I need to do is keep the keep/drop logic as is but if the dropped transaction is the transfer then I need to move the transfer link to point to the new transaction. Then if there are two transfers, I do keep/drop detection on both and create a single transfer based on the best 2 of the 4 transactions?

<!-- gh-comment-id:4310820279 --> @alecbakholdin commented on GitHub (Apr 24, 2026): I see what you mean. I think rather than modifying the keep/drop logic what I need to do is keep the keep/drop logic as is but if the dropped transaction is the transfer then I need to move the transfer link to point to the new transaction. Then if there are two transfers, I do keep/drop detection on both and create a single transfer based on the best 2 of the 4 transactions?
Author
Owner

@cesjn commented on GitHub (Apr 24, 2026):

Agreed that the original keep/drop logic was better and if the dropped is a transfer and the kept is not, we should move the transfer link over. If there are 4 transactions it seems like we need to be able to merge the two sets of two. I was playing with this last night and I haven't quite gotten it to work right yet (mostly my limited familiarity with javascript). I also need to think through what information can be merged when transactions are a transfer (notes are linked for instance and category is restricted for on budget accounts... I also need to think through whether subtransactions are a possibility here... maybe when the transfer is from an on-budget account to off-budget account).

<!-- gh-comment-id:4314978816 --> @cesjn commented on GitHub (Apr 24, 2026): Agreed that the original keep/drop logic was better and if the dropped is a transfer and the kept is not, we should move the transfer link over. If there are 4 transactions it seems like we need to be able to merge the two sets of two. I was playing with this last night and I haven't quite gotten it to work right yet (mostly my limited familiarity with javascript). I also need to think through what information can be merged when transactions are a transfer (notes are linked for instance and category is restricted for on budget accounts... I also need to think through whether subtransactions are a possibility here... maybe when the transfer is from an on-budget account to off-budget account).
Author
Owner

@alecbakholdin commented on GitHub (Apr 24, 2026):

@cesjn just pushed an implementation. If the builds fail, it's because I can't run unit tests locally for some reason so I'll be working on fixing that but in the meantime you're welcome to pull the branch and test for your use case.

Basically the new logic is (given accountA-> accountB):

  1. Unlink all transfers (set transfer_id to null) await setTransfers([a.id, b.id, aTransferId, bTransferId], null);
  2. Merge transaction(s) in accountB const transferId = await mergeTransfers(aTransferId, bTransferId);
  3. Merge transactions in accountA const keptTxId = await mergeTransactionsNoTransfer(a, b);
  4. If there was a transfer, update the accountA transaction by setting the proper transfer_id and payee
  5. For transfers to on-budget accounts, when merging a transfer with a non-transfer, we have to clear the category in the merged transaction.

Also, if you use the "make transfer" feature, then you can create transfers that have different dates and notes. This merge logic follows similar logic, meaning if you have
TransA (Apr 24) transNote -> TransB (Apr 24) transNote
merged with
TransC (Apr 1) note
You will end up with the following
TransC (Apr 1) note -> TransB (Apr 24) transNote

Please give this a shot and let me know if this works for you!

<!-- gh-comment-id:4315505571 --> @alecbakholdin commented on GitHub (Apr 24, 2026): @cesjn just pushed an implementation. If the builds fail, it's because I can't run unit tests locally for some reason so I'll be working on fixing that but in the meantime you're welcome to pull the branch and test for your use case. Basically the new logic is (given accountA-> accountB): 1. Unlink all transfers (set transfer_id to null) `await setTransfers([a.id, b.id, aTransferId, bTransferId], null);` 2. Merge transaction(s) in accountB `const transferId = await mergeTransfers(aTransferId, bTransferId);` 3. Merge transactions in accountA `const keptTxId = await mergeTransactionsNoTransfer(a, b);` 4. If there was a transfer, update the accountA transaction by setting the proper transfer_id and payee 5. For transfers to on-budget accounts, when merging a transfer with a non-transfer, we have to clear the category in the merged transaction. Also, if you use the "make transfer" feature, then you can create transfers that have different dates and notes. This merge logic follows similar logic, meaning if you have `TransA (Apr 24) transNote -> TransB (Apr 24) transNote` merged with `TransC (Apr 1) note` You will end up with the following `TransC (Apr 1) note -> TransB (Apr 24) transNote` Please give this a shot and let me know if this works for you!
Author
Owner

@cesjn commented on GitHub (Apr 24, 2026):

Looks great... here is a summary of what I have tested:
3 transactions to 2: both imported transactions are retained with their respective dates and notes
4 transaction to 2: both imported transactions are retained with their respective dates and notes (edit: I tested both merge scenarios, i.e. merge the two account A transactions or merge the two account B transactions)
I then created on Off-budget to on-budget transfer and an on-budget imported transaction that was split into sub-transactions. The imported/split transaction was retained with its sub-transactions.
I ran yarn test and everything passed.
From my point of view, this looks perfect.
Huge thanks for all the efforts here!

<!-- gh-comment-id:4316214246 --> @cesjn commented on GitHub (Apr 24, 2026): Looks great... here is a summary of what I have tested: 3 transactions to 2: both imported transactions are retained with their respective dates and notes 4 transaction to 2: both imported transactions are retained with their respective dates and notes (edit: I tested both merge scenarios, i.e. merge the two account A transactions or merge the two account B transactions) I then created on Off-budget to on-budget transfer and an on-budget imported transaction that was split into sub-transactions. The imported/split transaction was retained with its sub-transactions. I ran yarn test and everything passed. From my point of view, this looks perfect. Huge thanks for all the efforts here!
Author
Owner

@alecbakholdin commented on GitHub (Apr 24, 2026):

Sorry for the year-long turnaround!

<!-- gh-comment-id:4316310089 --> @alecbakholdin commented on GitHub (Apr 24, 2026): Sorry for the year-long turnaround!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/actual#79564