{"record":{"id":"7d3946540cab35c2","repo":"actualbudget/actual","slug":"transfer-payee-with-account-id-id-not-found","errorCode":null,"errorMessage":"Transfer payee with account ID ${id} not found.","messagePattern":"Transfer payee with account ID (.+?) not found\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/accounts/app.ts","lineNumber":640,"sourceCode":"    // If there are no transactions, we can simply delete the account\n    if (numTransactions === 0) {\n      await db.deleteAccount({ id });\n    } else if (forced) {\n      const rows = db.runQuery<\n        Pick<db.DbViewTransaction, 'id' | 'transfer_id'>\n      >(\n        'SELECT id, transfer_id FROM v_transactions WHERE account = ?',\n        [id],\n        true,\n      );\n\n      const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(\n        'SELECT id FROM payees WHERE transfer_acct = ?',\n        [id],\n      );\n\n      if (!transferPayee) {\n        throw new Error(`Transfer payee with account ID ${id} not found.`);\n      }\n\n      await batchMessages(async () => {\n        // TODO: what this should really do is send a special message that\n        // automatically marks the tombstone value for all transactions\n        // within an account... or something? This is problematic\n        // because another client could easily add new data that\n        // should be marked as deleted.\n\n        rows.forEach(row => {\n          if (row.transfer_id) {\n            void db.updateTransaction({\n              id: row.transfer_id,\n              payee: null,\n              transfer_id: null,\n            });\n          }\n","sourceCodeStart":622,"sourceCodeEnd":658,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/accounts/app.ts#L622-L658","documentation":"When closing an account that has a linked transfer account, closeAccount looks up the transfer payee (the auto-created payee whose transfer_acct points at the account being closed). If that payee is missing the invariant is broken and the close is aborted, since Actual must retarget the transfer payee's transactions to keep transfers consistent.","triggerScenarios":"Calling closeAccount with forceCloseAccount/transfer account options on an account whose transfer payee row was deleted directly (manual SQL, older data, or a buggy migration), so 'SELECT id FROM payees WHERE transfer_acct = ?' returns nothing.","commonSituations":"Budgets edited via raw SQL or the API deleting payees without cleaning transfer_acct links; data imported from other tools lacking transfer payees; budgets restored from partial backups.","solutions":["Recreate the missing transfer payee: insert a payee with transfer_acct set to the account id, then retry closeAccount.","Inspect the payees table ('SELECT * FROM payees WHERE transfer_acct = ?') to confirm whether the link exists before closing.","If the transfer relationship is unwanted, close the account without a transfer account so this path is skipped.","Restore the payee from a budget backup if the data was deleted accidentally."],"exampleFix":"// before\nawait send('close-account', { id, transferAccountId, forceCloseAccount });\n\n// after\nconst payee = await q('SELECT id FROM payees WHERE transfer_acct = ?', [id]);\nif (!payee) {\n  await send('create-transfer-payee', { accountId: id }); // recreate missing payee\n}\nawait send('close-account', { id, transferAccountId, forceCloseAccount });","handlingStrategy":"validation","validationCode":"const transferPayee = await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [accountId]);\nif (!transferPayee && transferAccountId) {\n  throw new Error('Transfer payee missing; recreate it or close without a transfer account');\n}","typeGuard":"async function hasTransferPayee(accountId: string): Promise<boolean> {\n  return !!(await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [accountId]));\n}","tryCatchPattern":"try {\n  await send('close-account', { id, transferAccountId, forceCloseAccount });\n} catch (e) {\n  if (/Transfer payee with account ID .* not found/.test(e.message)) {\n    // recreate payee or retry with forceCloseAccount / no transfer account\n  } else throw e;\n}","preventionTips":["Never delete payees with raw SQL; use the app/API so transfer_acct links stay consistent.","Audit budgets restored from backups for payees missing transfer_acct links.","Prefer closing accounts through the UI which manages transfer payees automatically."],"tags":["accounts","payees","data-integrity","missing-record"],"backgroundTag":"record-not-found","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}