Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-find-imported-account-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tim-smart/actualbudget-sync": patch
---

Fix duplicate and missing transactions when the same account is included in multiple sync runs
6 changes: 5 additions & 1 deletion src/Actual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ export class Actual extends ServiceMap.Service<Actual>()("Actual", {
Effect.map((result: any) => result.data as ReadonlyArray<A>),
)

const findImported = (importedIds: ReadonlyArray<string>) => {
const findImported = (
importedIds: ReadonlyArray<string>,
accountId: string,
) => {
if (importedIds.length === 0) {
return Effect.succeed(new Map<string, TransactionEntity>())
}
Expand All @@ -135,6 +138,7 @@ export class Actual extends ServiceMap.Service<Actual>()("Actual", {
q("transactions")
.select(["*"])
.filter({
account: accountId,
$or: chunk.map((imported_id) => ({ imported_id })),
})
.withDead(),
Expand Down
23 changes: 8 additions & 15 deletions src/Sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Array<APICategoryEntity>>,
)
const payees = yield* actual.use(
(_) => _.getPayees() as Promise<Array<APIPayeeEntity>>,
)
const categories = yield* actual.use((_) => _.getCategories())
const payees = yield* actual.use((_) => _.getPayees())

const results = yield* runCollect({
...options,
Expand All @@ -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<Fiber.Fiber<unknown, ActualError>>()
for (const transaction of transactions) {
Expand Down Expand Up @@ -193,13 +185,14 @@ export const run = Effect.fnUntraced(function* (options: {

const makeImportId = () => {
const counters = new Map<string, number>()
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}`
}
}
Expand Down