diff --git a/.changeset/fix-find-imported-account-scope.md b/.changeset/fix-find-imported-account-scope.md new file mode 100644 index 0000000..fc9d312 --- /dev/null +++ b/.changeset/fix-find-imported-account-scope.md @@ -0,0 +1,5 @@ +--- +"@tim-smart/actualbudget-sync": patch +--- + +Fix duplicate and missing transactions when the same account is included in multiple sync runs diff --git a/src/Actual.ts b/src/Actual.ts index 994adb7..c0e6e02 100644 --- a/src/Actual.ts +++ b/src/Actual.ts @@ -122,7 +122,10 @@ export class Actual extends ServiceMap.Service()("Actual", { Effect.map((result: any) => result.data as ReadonlyArray), ) - const findImported = (importedIds: ReadonlyArray) => { + const findImported = ( + importedIds: ReadonlyArray, + accountId: string, + ) => { if (importedIds.length === 0) { return Effect.succeed(new Map()) } @@ -135,6 +138,7 @@ export class Actual extends ServiceMap.Service()("Actual", { q("transactions") .select(["*"]) .filter({ + account: accountId, $or: chunk.map((imported_id) => ({ imported_id })), }) .withDead(), diff --git a/src/Sync.ts b/src/Sync.ts index 4b37404..94c5c15 100644 --- a/src/Sync.ts +++ b/src/Sync.ts @@ -16,10 +16,6 @@ import { Bank, } from "./Bank.ts" import { Actual, type ActualError } from "./Actual.ts" -import type { - APICategoryEntity, - APIPayeeEntity, -} from "@actual-app/api/@types/loot-core/src/server/api-models.js" const bigDecimal100 = BigDecimal.fromNumberUnsafe(100) const amountToInt = (amount: BigDecimal.BigDecimal) => @@ -83,7 +79,7 @@ export const runCollect = Effect.fnUntraced(function* (options: { // oxlint-disable-next-line unicorn/no-array-sort Array.sort(AccountTransactionOrder), Array.map((transaction) => { - const imported_id = importId(transaction) + const imported_id = importId(bankAccountId, transaction) const category = options.categorize && categoryId(transaction) const transferPayee = transaction.transfer && transferAccountId(transaction) @@ -127,12 +123,8 @@ export const run = Effect.fnUntraced(function* (options: { readonly clearedOnly: boolean }) { const actual = yield* Actual - const categories = yield* actual.use( - (_) => _.getCategories() as Promise>, - ) - const payees = yield* actual.use( - (_) => _.getPayees() as Promise>, - ) + const categories = yield* actual.use((_) => _.getCategories()) + const payees = yield* actual.use((_) => _.getPayees()) const results = yield* runCollect({ ...options, @@ -141,7 +133,7 @@ export const run = Effect.fnUntraced(function* (options: { }) for (const { transactions, ids, actualAccountId } of results) { - const alreadyImported = yield* actual.findImported(ids) + const alreadyImported = yield* actual.findImported(ids, actualAccountId) let toImport: typeof transactions = [] const updates = Array.empty>() for (const transaction of transactions) { @@ -193,13 +185,14 @@ export const run = Effect.fnUntraced(function* (options: { const makeImportId = () => { const counters = new Map() - return (self: AccountTransaction) => { + return (accountId: string, self: AccountTransaction) => { const dateParts = DateTime.toParts(self.dateTime) const dateString = `${dateParts.year.toString().padStart(4, "0")}${dateParts.month.toString().padStart(2, "0")}${dateParts.day.toString().padStart(2, "0")}` const amountInt = amountToInt(self.amount) const prefix = `${dateString}${amountInt}` - const count = counters.has(prefix) ? counters.get(prefix)! + 1 : 1 - counters.set(prefix, count) + const key = `${accountId}:${prefix}` + const count = counters.has(key) ? counters.get(key)! + 1 : 1 + counters.set(key, count) return `${prefix}-${count}` } }