{"record":{"id":"6efc2cc9cbbce127","repo":"actualbudget/actual","slug":"account-mismatch-transaction-belongs-to-account","errorCode":null,"errorMessage":"Account mismatch: transaction belongs to account ${transaction.account}, not ${accountId}","messagePattern":"Account mismatch: transaction belongs to account (.+?), not (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/transactions/app.ts","lineNumber":87,"sourceCode":"async function moveTransaction({\n  id,\n  accountId,\n  targetId,\n}: {\n  id: string;\n  accountId: string;\n  targetId: string | null;\n}) {\n  // Fetch the transaction to validate it exists and verify account\n  const transaction = await db.getTransaction(id);\n  if (!transaction) {\n    throw new Error(`Transaction not found: ${id}`);\n  }\n\n  // Validate that the provided accountId matches the transaction's actual account\n  // This prevents sort order calculations against the wrong account\n  if (transaction.account !== accountId) {\n    throw new Error(\n      `Account mismatch: transaction belongs to account ${transaction.account}, not ${accountId}`,\n    );\n  }\n\n  // Child transactions can be reordered within their parent's children\n  // The db.moveTransaction handles the sibling-scoped reordering for children\n\n  await db.moveTransaction(id, accountId, targetId);\n  return {};\n}\n\nasync function parseTransactionsFile({\n  filepath,\n  options,\n}: {\n  filepath: string;\n  options: ParseFileOptions;\n}) {","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/transactions/app.ts#L69-L105","documentation":"moveTransaction validates that the accountId you pass in matches the account the transaction actually belongs to. The sort-order recalculation it performs is scoped to a single account, so moving a transaction while claiming it belongs to a different account would corrupt sibling ordering. The library throws this error immediately when transaction.account !== accountId.","triggerScenarios":"Calling moveTransaction with an accountId that differs from the account stored on the transaction row — e.g. a stale accountId from a previous fetch, passing a destination account instead of the transaction's current account, or an off-by-one/wrong-row selection in a drag-and-drop reorder UI.","commonSituations":"UI bugs where a transaction was just reassigned to another account but the client caches the old accountId; concurrent edits where one client moves a transaction between accounts while another reorders it; tests hard-coding an accountId.","solutions":["Re-fetch the transaction and pass its actual account as accountId when calling moveTransaction.","If the intent is to move the transaction to a different account, do that first (update the transaction's account), then call moveTransaction with the new accountId.","Check the client for stale state — refresh the transaction list after any account reassignment before reordering.","Log both transaction.account and accountId at the call site to confirm which is wrong."],"exampleFix":"// before\nawait moveTransaction({ id: txId, accountId: targetAccountId });\n// after\nconst tx = await getTransaction(txId);\nif (tx.account !== targetAccountId) {\n  await updateTransaction(txId, { account: targetAccountId });\n}\nawait moveTransaction({ id: txId, accountId: tx.account });","handlingStrategy":"validation","validationCode":"const tx = await getTransaction(id);\nif (tx.account !== accountId) {\n  throw new Error(`Cannot move: transaction is in ${tx.account}, not ${accountId}`);\n}","typeGuard":"function canMove(tx: { account: string }, accountId: string): boolean {\n  return tx.account === accountId;\n}","tryCatchPattern":"try {\n  await moveTransaction({ id, accountId });\n} catch (e) {\n  if (e.message.startsWith('Account mismatch:')) {\n    await refreshTransactions(); // reload stale state\n  } else { throw e; }\n}","preventionTips":["Always source accountId from the freshly fetched transaction, not cached UI state","Refresh the transaction list after any account reassignment","Never pass a 'destination' account to moveTransaction; it expects the current account"],"tags":["validation","transactions","accounting","state-mismatch"],"backgroundTag":"account-id-mismatch","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}