[GH-ISSUE #5881] [Feature] Debug console API for client scripts #116905

Closed
opened 2026-06-11 10:39:39 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @alshdavid on GitHub (Oct 7, 2025).
Original GitHub issue: https://github.com/actualbudget/actual/issues/5881

Verified feature request does not already exist?

  • I have searched and found no existing issue

💻

  • Would you like to implement this feature?

Pitch: what problem are you trying to solve?

I have a bulk data import that adds 1000+ transfers (500+ pairs).

Right now, I'm clicking manually to find and pair the transfers, I don't see a way to add a rule smart enough to figure out what is a transfer.

My transfers look like:

Date Account Notes Payment Deposit
01/01/2025 Account1 Internal Transfer - Internal Transfer - Receipt 5600 Savings Accelerator xxxxxx $5
01/01/2025 Account2 Internal Transfer - Receipt 5600 - To Orange Everyday $5

And my workflow is:

  • Scroll & visually find a line item that I know is an account transfer
  • Click on "notes"
  • highlight & copy the transaction ID (in the notes)
  • Search for the transaction ID
  • Click on the table whitespace (Ctrl + A does not work otherwise)
  • Ctrl + A
  • Click on "▼ 2 transactions"
  • Click on "make transfer"
  • Click on all accounts (browser back does not send me to all transactions)

Repeat for each pair I manually identify. I'm going to be here all day, haha

I have poked around the debug console looking for something on globalThis that exposes some kind of API. I found globalThis.$q which has some documentation here but I don't know if it can be used to create transfers.

Describe your ideal solution to this problem

It would be awesome if GUI functionality was exposed on the console for client scripts.

I could then write something that matches against a regex and just paste it into the browser console

const { Actual } = globalThis
const RE = /(.*)- Receipt ([\S]+)(.*)/

/** @type {Record<string, [Actual.Transaction, Actual.Transaction]>} */
const pairs = {}

for await (const transaction of Actual.getTransactions("*")) {
  const match = transaction.notes.match(RE)
  if (!match) continue
  const [,,id] = match
  if (!pairs[id]) {
    pairs[id] = [transaction] 
  } else {
    pairs[id].push(transaction)
  }
}

for (const [a, b] of Object.values(pairs) {
  // validation & try/catch
  await Actual.makeTransfer(a, b)
}

Teaching and learning

Expose a console API for client scripts and document it.

It might even be useful to introduce the concept of "stored procedures"/"stored queries" that can be used as custom filters or post-import scripts.

I realize that running loop-based JavaScript scripts introduces the potential for negatively affecting performance, but the capability is super valuable and we can slap warnings on it.

Queries can probably be sandboxed to a Worker or SharedWorker to help alleviate the blocking nature of these scripts

Originally created by @alshdavid on GitHub (Oct 7, 2025). Original GitHub issue: https://github.com/actualbudget/actual/issues/5881 ### Verified feature request does not already exist? - [x] I have searched and found no existing issue ### 💻 - [ ] Would you like to implement this feature? ### Pitch: what problem are you trying to solve? I have a bulk data import that adds 1000+ transfers (500+ pairs). Right now, I'm clicking manually to find and pair the transfers, I don't see a way to add a rule smart enough to figure out what is a transfer. My transfers look like: |Date|Account|Notes|Payment|Deposit| |-----|---------|-------|------|------| |01/01/2025| Account1 | Internal Transfer - Internal Transfer - Receipt 5600 Savings Accelerator xxxxxx| | $5 | |01/01/2025| Account2 | Internal Transfer - Receipt 5600 - To Orange Everyday | $5 | | And my workflow is: - Scroll & visually find a line item that I know is an account transfer - Click on "notes" - highlight & copy the transaction ID (in the notes) - Search for the transaction ID - Click on the table whitespace (Ctrl + A does not work otherwise) - Ctrl + A - Click on "▼ 2 transactions" - Click on "make transfer" - Click on all accounts (browser back does not send me to all transactions) Repeat for each pair I manually identify. I'm going to be here all day, haha I have poked around the debug console looking for something on `globalThis` that exposes some kind of API. I found `globalThis.$q` which [has some documentation here](https://actualbudget.org/docs/api/actual-ql/examples) but I don't know if it can be used to create transfers. ### Describe your ideal solution to this problem It would be awesome if GUI functionality was exposed on the console for client scripts. I could then write something that matches against a regex and just paste it into the browser console ```javascript const { Actual } = globalThis const RE = /(.*)- Receipt ([\S]+)(.*)/ /** @type {Record<string, [Actual.Transaction, Actual.Transaction]>} */ const pairs = {} for await (const transaction of Actual.getTransactions("*")) { const match = transaction.notes.match(RE) if (!match) continue const [,,id] = match if (!pairs[id]) { pairs[id] = [transaction] } else { pairs[id].push(transaction) } } for (const [a, b] of Object.values(pairs) { // validation & try/catch await Actual.makeTransfer(a, b) } ``` ### Teaching and learning Expose a console API for client scripts and document it. It might even be useful to introduce the concept of "stored procedures"/"stored queries" that can be used as custom filters or post-import scripts. I realize that running loop-based JavaScript scripts introduces the potential for negatively affecting performance, but the capability is super valuable and we can slap warnings on it. Queries can probably be sandboxed to a `Worker` or `SharedWorker` to help alleviate the blocking nature of these scripts
GiteaMirror added the featureneeds votes labels 2026-06-11 10:39:40 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Oct 7, 2025):

Thanks for sharing your idea!

This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).

The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+

Don’t forget to upvote the top comment with 👍!

<!-- gh-comment-id:3378836582 --> @github-actions[bot] commented on GitHub (Oct 7, 2025): :sparkles: Thanks for sharing your idea! :sparkles: This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution). The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+ Don’t forget to upvote the top comment with 👍! <!-- feature-auto-close-comment -->
Author
Owner

@alshdavid commented on GitHub (Oct 7, 2025):

Using the query system, I've been able to find all my transactions with this:

(async () => {
  const q = globalThis.$q("transactions")
    .filter({ notes: { $regexp: "Internal Transfer - Receipt" } })
    .select(['notes', 'category'])

  const transactions = await globalThis.$query(q)

  const RE = /(.*)- Receipt ([\S]+)(.*)/
  const pairsById = {}

  for (const transaction of transactions.data) {
    // Skip entries that are already organized
    if (transaction.category) continue

    // Pair up matches with the same Receipt ID
    const match = transaction.notes.match(RE)
    if (!match) continue
    const [, , id] = match
    if (!pairsById[id]) {
      pairsById[id] = [transaction]
    } else {
      pairsById[id].push(transaction)
    }
  }

  const pairs = Object.values(pairsById).filter(v => v.length === 2)
  console.log(pairs)
})()

But I'm not sure how to tell Actual to make them transfers

<!-- gh-comment-id:3378914456 --> @alshdavid commented on GitHub (Oct 7, 2025): Using the query system, I've been able to find all my transactions with this: ```javascript (async () => { const q = globalThis.$q("transactions") .filter({ notes: { $regexp: "Internal Transfer - Receipt" } }) .select(['notes', 'category']) const transactions = await globalThis.$query(q) const RE = /(.*)- Receipt ([\S]+)(.*)/ const pairsById = {} for (const transaction of transactions.data) { // Skip entries that are already organized if (transaction.category) continue // Pair up matches with the same Receipt ID const match = transaction.notes.match(RE) if (!match) continue const [, , id] = match if (!pairsById[id]) { pairsById[id] = [transaction] } else { pairsById[id].push(transaction) } } const pairs = Object.values(pairsById).filter(v => v.length === 2) console.log(pairs) })() ``` But I'm not sure how to tell Actual to make them transfers
Author
Owner

@alshdavid commented on GitHub (Oct 7, 2025):

It looks like the nodejs api is available with different method names via

globalThis.$send(methodName: string, args: Serializable): Promise<unknown>
API Reference
access-add
access-delete-all
access-get-available-users
account-balance
account-close
account-create
account-move
account-properties
account-reopen
account-unlink
account-update
accounts-bank-sync
accounts-get
api/abort-import
api/account-balance
api/account-close
api/account-create
api/account-delete
api/account-reopen
api/account-update
api/accounts-get
api/bank-sync
api/batch-budget-end
api/batch-budget-start
api/budget-hold-for-next-month
api/budget-month
api/budget-months
api/budget-reset-hold
api/budget-set-amount
api/budget-set-carryover
api/categories-get
api/category-create
api/category-delete
api/category-group-create
api/category-group-delete
api/category-group-update
api/category-groups-get
api/category-update
api/common-payees-get
api/download-budget
api/finish-import
api/get-budgets
api/get-id-by-name
api/get-server-version
api/load-budget
api/payee-create
api/payee-delete
api/payee-rules-get
api/payee-update
api/payees-get
api/payees-merge
api/query
api/rule-create
api/rule-delete
api/rule-update
api/rules-get
api/schedule-create
api/schedule-delete
api/schedule-update
api/schedules-get
api/start-import
api/sync
api/transaction-delete
api/transaction-update
api/transactions-add
api/transactions-export
api/transactions-get
api/transactions-import
app-focused
backup-load
backup-make
backups-get
budget/apply-goal-template
budget/apply-multiple-templates
budget/apply-single-template
budget/budget-amount
budget/check-templates
budget/cleanup-goal-template
budget/copy-previous-month
budget/copy-single-month
budget/cover-overbudgeted
budget/cover-overspending
budget/get-category-automations
budget/hold-for-next-month
budget/overwrite-goal-template
budget/render-note-templates
budget/reset-hold
budget/reset-income-carryover
budget/set-3month-avg
budget/set-6month-avg
budget/set-12month-avg
budget/set-carryover
budget/set-category-automations
budget/set-n-month-avg
budget/set-zero
budget/store-note-templates
budget/transfer-available
budget/transfer-category
category-create
category-delete
category-group-create
category-group-delete
category-group-move
category-group-update
category-move
category-update
close-budget
common-payees-get
create-budget
create-demo-budget
create-query
dashboard-add-widget
dashboard-import
dashboard-remove-widget
dashboard-reset
dashboard-update
dashboard-update-widget
delete-budget
download-budget
duplicate-budget
enable-openid
enable-password
envelope-budget-month
export-budget
filter-create
filter-delete
filter-update
get-budget-bounds
get-budgets
get-categories
get-category-groups
get-cell
get-cell-names
get-did-bootstrap
get-earliest-transaction
get-last-opened-backup
get-latest-transaction
get-openid-config
get-remote-files
get-server-url
get-server-version
get-user-file-info
gocardless-accounts-link
gocardless-create-web-token
gocardless-get-banks
gocardless-poll-web-token
gocardless-poll-web-token-stop
gocardless-status
import-budget
key-make
key-test
load-budget
load-global-prefs
load-prefs
make-filters-from-conditions
must-category-transfer
notes-save
notes-save-undoable
owner-created
payee-create
payees-batch-change
payees-check-orphaned
payees-get
payees-get-orphaned
payees-get-rule-counts
payees-get-rules
payees-merge
pluggyai-accounts
pluggyai-accounts-link
pluggyai-status
preferences/get
preferences/save
query
redo
report/create
report/delete
report/update
reset-budget-cache
rule-add
rule-add-payee-rename
rule-apply-actions
rule-delete
rule-delete-all
rule-get
rule-update
rule-validate
rules-get
rules-run
save-global-prefs
save-prefs
schedule/create
schedule/delete
schedule/discover
schedule/force-run-service
schedule/get-upcoming-dates
schedule/post-transaction
schedule/skip-next-date
schedule/update
secret-check
secret-set
set-server-url
simplefin-accounts
simplefin-accounts-link
simplefin-batch-sync
simplefin-status
subscribe-bootstrap
subscribe-change-password
subscribe-get-login-methods
subscribe-get-user
subscribe-needs-bootstrap
subscribe-set-token
subscribe-sign-in
subscribe-sign-out
sync
sync-budget
sync-repair
sync-reset
tags-create
tags-delete
tags-delete-all
tags-find
tags-get
tags-update
tools/fix-split-transactions
tracking-budget-month
transaction-add
transaction-delete
transaction-update
transactions-batch-update
transactions-export
transactions-export-query
transactions-import
transactions-merge
transactions-parse-file
transfer-ownership
undo
unique-budget-name
upload-budget
upload-file-web
user-add
user-delete-all
user-update
users-get
validate-budget-name

I wrote a little mapper that translates to something similar to the Node API

function createProxy(path = []) {
  return new Proxy(function() {}, {
    apply(_recv, _this, args) {
      const rawMethod = path.pop()
      const result = rawMethod.replace(/([A-Z])/g, " $1");
      const segs = result.split(' ')
      const method = segs.map(x => x.toLowerCase()).join('-')
      return globalThis.$send([...path, method].join('/'), args[0])
    },
    get(_recv, key) {
      return createProxy([...path, key])
    }
  })
}

let $api = createProxy()

let result = await $api.accountsGet()
console.log(await $api.api.accountBalance({ id: result[0].id }))

However I don't fully understand how "transfers" work

<!-- gh-comment-id:3378960794 --> @alshdavid commented on GitHub (Oct 7, 2025): It looks like the nodejs api is available with different method names via ```typescript globalThis.$send(methodName: string, args: Serializable): Promise<unknown> ``` <details> <summary>API Reference</summary> ``` access-add access-delete-all access-get-available-users account-balance account-close account-create account-move account-properties account-reopen account-unlink account-update accounts-bank-sync accounts-get api/abort-import api/account-balance api/account-close api/account-create api/account-delete api/account-reopen api/account-update api/accounts-get api/bank-sync api/batch-budget-end api/batch-budget-start api/budget-hold-for-next-month api/budget-month api/budget-months api/budget-reset-hold api/budget-set-amount api/budget-set-carryover api/categories-get api/category-create api/category-delete api/category-group-create api/category-group-delete api/category-group-update api/category-groups-get api/category-update api/common-payees-get api/download-budget api/finish-import api/get-budgets api/get-id-by-name api/get-server-version api/load-budget api/payee-create api/payee-delete api/payee-rules-get api/payee-update api/payees-get api/payees-merge api/query api/rule-create api/rule-delete api/rule-update api/rules-get api/schedule-create api/schedule-delete api/schedule-update api/schedules-get api/start-import api/sync api/transaction-delete api/transaction-update api/transactions-add api/transactions-export api/transactions-get api/transactions-import app-focused backup-load backup-make backups-get budget/apply-goal-template budget/apply-multiple-templates budget/apply-single-template budget/budget-amount budget/check-templates budget/cleanup-goal-template budget/copy-previous-month budget/copy-single-month budget/cover-overbudgeted budget/cover-overspending budget/get-category-automations budget/hold-for-next-month budget/overwrite-goal-template budget/render-note-templates budget/reset-hold budget/reset-income-carryover budget/set-3month-avg budget/set-6month-avg budget/set-12month-avg budget/set-carryover budget/set-category-automations budget/set-n-month-avg budget/set-zero budget/store-note-templates budget/transfer-available budget/transfer-category category-create category-delete category-group-create category-group-delete category-group-move category-group-update category-move category-update close-budget common-payees-get create-budget create-demo-budget create-query dashboard-add-widget dashboard-import dashboard-remove-widget dashboard-reset dashboard-update dashboard-update-widget delete-budget download-budget duplicate-budget enable-openid enable-password envelope-budget-month export-budget filter-create filter-delete filter-update get-budget-bounds get-budgets get-categories get-category-groups get-cell get-cell-names get-did-bootstrap get-earliest-transaction get-last-opened-backup get-latest-transaction get-openid-config get-remote-files get-server-url get-server-version get-user-file-info gocardless-accounts-link gocardless-create-web-token gocardless-get-banks gocardless-poll-web-token gocardless-poll-web-token-stop gocardless-status import-budget key-make key-test load-budget load-global-prefs load-prefs make-filters-from-conditions must-category-transfer notes-save notes-save-undoable owner-created payee-create payees-batch-change payees-check-orphaned payees-get payees-get-orphaned payees-get-rule-counts payees-get-rules payees-merge pluggyai-accounts pluggyai-accounts-link pluggyai-status preferences/get preferences/save query redo report/create report/delete report/update reset-budget-cache rule-add rule-add-payee-rename rule-apply-actions rule-delete rule-delete-all rule-get rule-update rule-validate rules-get rules-run save-global-prefs save-prefs schedule/create schedule/delete schedule/discover schedule/force-run-service schedule/get-upcoming-dates schedule/post-transaction schedule/skip-next-date schedule/update secret-check secret-set set-server-url simplefin-accounts simplefin-accounts-link simplefin-batch-sync simplefin-status subscribe-bootstrap subscribe-change-password subscribe-get-login-methods subscribe-get-user subscribe-needs-bootstrap subscribe-set-token subscribe-sign-in subscribe-sign-out sync sync-budget sync-repair sync-reset tags-create tags-delete tags-delete-all tags-find tags-get tags-update tools/fix-split-transactions tracking-budget-month transaction-add transaction-delete transaction-update transactions-batch-update transactions-export transactions-export-query transactions-import transactions-merge transactions-parse-file transfer-ownership undo unique-budget-name upload-budget upload-file-web user-add user-delete-all user-update users-get validate-budget-name ``` </details> I wrote a little mapper that translates to something similar to the Node API ```javascript function createProxy(path = []) { return new Proxy(function() {}, { apply(_recv, _this, args) { const rawMethod = path.pop() const result = rawMethod.replace(/([A-Z])/g, " $1"); const segs = result.split(' ') const method = segs.map(x => x.toLowerCase()).join('-') return globalThis.$send([...path, method].join('/'), args[0]) }, get(_recv, key) { return createProxy([...path, key]) } }) } let $api = createProxy() let result = await $api.accountsGet() console.log(await $api.api.accountBalance({ id: result[0].id })) ``` However I don't fully understand how "transfers" work
Author
Owner

@alshdavid commented on GitHub (Oct 8, 2025):

I tried to update two transactions using the JavaScript API however it doesn't appear to work. It only updates one of the transactions

Image
(async () => {
  const uuidv4 = await import(
    "https://cdn.jsdelivr.net/npm/uuid-v4@0.1.0/+esm"
  );

  function uuid() {
    return uuidv4.default();
  }

  const payees = {}
  const payees_arr = await globalThis.$send('payees-get', [])
  for (const payee of payees_arr) {
    payees[payee.transfer_acct] = payee
  }

  console.log(payees)

  const transactions = {
    to: (
      await globalThis.$query(
        globalThis
          .$q("transactions")
          .filter({ id: "3860320d-8002-483b-9ff8-51cdff4bda0b" })
          .select(["*"])
      )
    ).data[0],
    from: (
      await globalThis.$query(
        globalThis
          .$q("transactions")
          .filter({ id: "afd55452-df66-49f9-8ff1-408943142496" })
          .select(["*"])
      )
    ).data[0],
  };

  console.log(transactions);

  let id = uuid();

  await globalThis.$send("api/transaction-update", {
    id: transactions.from.id,
    fields: { payee: payees[transactions.to.account].id, transfer_id: id },
  });

  await globalThis.$send("api/transaction-update", {
    id: transactions.to.id,
    fields: { transfer_id: id },
  });
})();
<!-- gh-comment-id:3379282094 --> @alshdavid commented on GitHub (Oct 8, 2025): I tried to update two transactions using the JavaScript API however it doesn't appear to work. It only updates one of the transactions <img width="891" height="119" alt="Image" src="https://github.com/user-attachments/assets/8400de23-e4d9-424c-8a8f-4b0719f72e6d" /> ```javascript (async () => { const uuidv4 = await import( "https://cdn.jsdelivr.net/npm/uuid-v4@0.1.0/+esm" ); function uuid() { return uuidv4.default(); } const payees = {} const payees_arr = await globalThis.$send('payees-get', []) for (const payee of payees_arr) { payees[payee.transfer_acct] = payee } console.log(payees) const transactions = { to: ( await globalThis.$query( globalThis .$q("transactions") .filter({ id: "3860320d-8002-483b-9ff8-51cdff4bda0b" }) .select(["*"]) ) ).data[0], from: ( await globalThis.$query( globalThis .$q("transactions") .filter({ id: "afd55452-df66-49f9-8ff1-408943142496" }) .select(["*"]) ) ).data[0], }; console.log(transactions); let id = uuid(); await globalThis.$send("api/transaction-update", { id: transactions.from.id, fields: { payee: payees[transactions.to.account].id, transfer_id: id }, }); await globalThis.$send("api/transaction-update", { id: transactions.to.id, fields: { transfer_id: id }, }); })(); ```
Author
Owner

@alshdavid commented on GitHub (Oct 19, 2025):

Looks like I need to use this API to combine two existing transactions: https://github.com/actualbudget/actual/blob/master/packages/desktop-client/src/hooks/useTransactionBatchActions.ts#L501

const changes = {
  updated: [
    {
      ...from,
      category: null,
      payee: to_payee.id,
      transfer_id: to.id,
    },
    {
      ...to,
      category: null,
      payee: from_payee?.id,
      transfer_id: from.id,
    }
  ],
  runTransfers: false,
}

await globalThis.$send("transactions-batch-update", changes);
<!-- gh-comment-id:3420011508 --> @alshdavid commented on GitHub (Oct 19, 2025): Looks like I need to use this API to combine two existing transactions: https://github.com/actualbudget/actual/blob/master/packages/desktop-client/src/hooks/useTransactionBatchActions.ts#L501 ```javascript const changes = { updated: [ { ...from, category: null, payee: to_payee.id, transfer_id: to.id, }, { ...to, category: null, payee: from_payee?.id, transfer_id: from.id, } ], runTransfers: false, } await globalThis.$send("transactions-batch-update", changes); ```
Author
Owner

@alshdavid commented on GitHub (Oct 19, 2025):

And for reference, my completed import script

(async () => {
  const payees = {};
  const payees_arr = await globalThis.$send("payees-get", []);
  for (const payee of payees_arr) {
    payees[payee.transfer_acct] = payee;
  }

  const q = globalThis
    .$q("transactions")
    .filter({ notes: { $regexp: "Internal Transfer - Receipt" } })
    .select(["*"]);

  const transactions = await globalThis.$query(q);

  const RE = /(.*)- Receipt ([\S]+)(.*)/;
  const pairsById = {};

  for (const transaction of transactions.data) {
    // Skip entries that are already organized
    if (transaction.category) continue;

    // Pair up matches with the same Receipt ID
    const match = transaction.notes.match(RE);
    if (!match) continue;

    const [, , id] = match;
    if (!pairsById[id]) {
      pairsById[id] = [transaction];
    } else {
      pairsById[id].push(transaction);
    }
  }

  // Ensure there are two records
  const pairs = Object.values(pairsById).filter((v) => v.length === 2);


  for (const [i, pair] of pairs.entries()) {
    const from = pair.find((entry) => entry.amount < 0);
    const to = pair.find((entry) => entry.amount > 0);

    const from_payee = payees[from.account];
    const to_payee = payees[to.account];

    const changes = {
      updated: [
        {
          ...from,
          category: null,
          payee: to_payee.id,
          transfer_id: to.id,
        },
        {
          ...to,
          category: null,
          payee: from_payee?.id,
          transfer_id: from.id,
        },
      ],
      runTransfers: false,
    };

    console.log(`${i}/${pairs.length}`, from, to)
    await globalThis.$send("transactions-batch-update", changes);
  }
})();
<!-- gh-comment-id:3420018573 --> @alshdavid commented on GitHub (Oct 19, 2025): And for reference, my completed import script ```javascript (async () => { const payees = {}; const payees_arr = await globalThis.$send("payees-get", []); for (const payee of payees_arr) { payees[payee.transfer_acct] = payee; } const q = globalThis .$q("transactions") .filter({ notes: { $regexp: "Internal Transfer - Receipt" } }) .select(["*"]); const transactions = await globalThis.$query(q); const RE = /(.*)- Receipt ([\S]+)(.*)/; const pairsById = {}; for (const transaction of transactions.data) { // Skip entries that are already organized if (transaction.category) continue; // Pair up matches with the same Receipt ID const match = transaction.notes.match(RE); if (!match) continue; const [, , id] = match; if (!pairsById[id]) { pairsById[id] = [transaction]; } else { pairsById[id].push(transaction); } } // Ensure there are two records const pairs = Object.values(pairsById).filter((v) => v.length === 2); for (const [i, pair] of pairs.entries()) { const from = pair.find((entry) => entry.amount < 0); const to = pair.find((entry) => entry.amount > 0); const from_payee = payees[from.account]; const to_payee = payees[to.account]; const changes = { updated: [ { ...from, category: null, payee: to_payee.id, transfer_id: to.id, }, { ...to, category: null, payee: from_payee?.id, transfer_id: from.id, }, ], runTransfers: false, }; console.log(`${i}/${pairs.length}`, from, to) await globalThis.$send("transactions-batch-update", changes); } })(); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/actual#116905