{"record":{"id":"f64c9389470457c4","repo":"actualbudget/actual","slug":"validformergeerror-one-of-one-of-the-provide","errorCode":null,"errorMessage":"${validForMergeError} (one of: 'One of the provided transactions does not exist', 'Cannot merge transactions from different accounts', 'Cannot merge transactions with different amounts', 'Cannot merge transfers to different accounts')","messagePattern":"(.+?) \\(one of: 'One of the provided transactions does not exist', 'Cannot merge transactions from different accounts', 'Cannot merge transactions with different amounts', 'Cannot merge transfers to different accounts'\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/transactions/merge.ts","lineNumber":105,"sourceCode":"  const [aTransfer, bTransfer] = await mapAndValidateTransactions(\n    aTransferId,\n    bTransferId,\n  );\n  await setTransfers([aTransfer.id, bTransfer.id], null);\n  return mergeTransactionsNoTransfer(aTransfer, bTransfer);\n}\n\nasync function mapAndValidateTransactions(\n  aId: TransactionEntity['id'],\n  bId: TransactionEntity['id'],\n): Promise<TransactionEntity[]> {\n  // get most recent transactions\n  const a: TransactionEntity = await db.getTransaction(aId);\n  const b: TransactionEntity = await db.getTransaction(bId);\n\n  const validForMergeError = validForMergeExplanation(a, b);\n  if (validForMergeError) {\n    throw new Error(validForMergeError);\n  }\n  return [a, b];\n}\n\nexport async function mergeTransactionsNoTransfer(\n  a: TransactionEntity,\n  b: TransactionEntity,\n): Promise<TransactionEntity['id']> {\n  const { keep, drop } = determineKeepDrop(a, b);\n\n  // Load subtransactions with a single query, then split by parent_id in memory\n  const keepSubtransactions: TransactionEntity[] = [];\n  const dropSubtransactions: TransactionEntity[] = [];\n  const parents: string[] = [];\n  if (keep.is_parent) parents.push(keep.id);\n  if (drop.is_parent) parents.push(drop.id);\n\n  let rows: TransactionEntity[] = [];","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/transactions/merge.ts#L87-L123","documentation":"mapAndValidateTransactions fetches both transactions and runs validForMergeExplanation to check merge preconditions. If any precondition fails (nonexistent transaction, different accounts, different amounts, or transfers to different accounts), it throws with that explanation directly as the message — the template here just documents the four possible texts.","triggerScenarios":"Merging two transactions that (a) reference deleted/nonexistent ids, (b) live in different accounts, (c) have different amounts, or (d) are transfers whose counterparties point at different accounts. Called via mergeTransactions on user-selected 'duplicate' pairs that are actually not mergeable.","commonSituations":"Duplicate-detection heuristics matching on payee/date but not amount or account; one side of the pair deleted between selection and merge (race); a transaction edited by another client after selection changing its amount.","solutions":["Read the thrown message — it states exactly which precondition failed — and surface it to the user.","Before merging, verify both transactions exist, share the same account, and have identical amounts.","Refresh the transaction list before merging to avoid merging stale/deleted rows.","Exclude transfer transactions with mismatched counterpart accounts from duplicate candidates, or merge them via mergeTransactionsNoTransfer when appropriate."],"exampleFix":"// before\nawait mergeTransactions([{ id: aId }, { id: bId }]);\n// after\nconst [a, b] = await Promise.all([getTransaction(aId), getTransaction(bId)]);\nif (!a || !b) throw new Error('One of the transactions no longer exists');\nif (a.account !== b.account || a.amount !== b.amount) {\n  throw new Error('Transactions are not mergeable (account/amount differ)');\n}\nawait mergeTransactions([{ id: aId }, { id: bId }]);","handlingStrategy":"validation","validationCode":"const [a, b] = await Promise.all([getTransaction(aId), getTransaction(bId)]);\nconst mergeable = a && b && a.account === b.account && a.amount === b.amount;\nif (!mergeable) throw new Error('Selected transactions are not valid merge candidates');","typeGuard":"function isMergeable(a: TransactionEntity | null, b: TransactionEntity | null): boolean {\n  return !!a && !!b && a.account === b.account && a.amount === b.amount;\n}","tryCatchPattern":"try {\n  await mergeTransactions([{ id: aId }, { id: bId }]);\n} catch (e) {\n  if (/does not exist|different accounts|different amounts|different transfers/.test(e.message)) {\n    showError(`Cannot merge: ${e.message}`);\n  } else { throw e; }\n}","preventionTips":["Refresh transaction data immediately before merging to avoid deleted-row races","Check account and amount equality in the duplicate-detection UI before offering merge","Exclude transfers with mismatched counterpart accounts from merge candidates"],"tags":["validation","transactions","merge","business-rules"],"backgroundTag":"merge-precondition-failed","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}