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
135 changes: 84 additions & 51 deletions packages/loot-core/src/server/importers/ynab5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ import {
type Transaction,
} from './ynab5-types';

const MAX_RETRY = 20;

function normalizeError(e: unknown): string {
if (e instanceof Error) {
return e.message;
}
if (typeof e === 'string') {
return e;
}
return String(e);
}

type FlaggedTransaction = Pick<
Transaction | ScheduledTransaction,
'flag_name' | 'flag_color' | 'deleted'
Expand Down Expand Up @@ -318,40 +330,71 @@ async function importCategories(
// Can't be done in parallel to have
// correct sort order.

async function createCategoryGroupWithUniqueName(params: {
name: string;
is_income: boolean;
hidden: boolean;
}) {
const baseName = params.hidden ? `${params.name} (hidden)` : params.name;
let count = 0;

while (true) {
const name = count === 0 ? baseName : `${baseName} (${count})`;
try {
const id = await actual.createCategoryGroup({ ...params, name });
return { id, name };
} catch (e) {
if (count >= MAX_RETRY) {
const errorMsg = normalizeError(e);
throw Error('Unable to create category group: ' + errorMsg);
}
count += 1;
}
}
}

async function createCategoryWithUniqueName(params: {
name: string;
group_id: string;
hidden: boolean;
}) {
const baseName = params.hidden ? `${params.name} (hidden)` : params.name;
let count = 0;

while (true) {
const name = count === 0 ? baseName : `${baseName} (${count})`;
try {
const id = await actual.createCategory({ ...params, name });
return { id, name };
} catch (e) {
if (count >= MAX_RETRY) {
const errorMsg = normalizeError(e);
throw Error('Unable to create category: ' + errorMsg);
}
count += 1;
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for (const group of data.category_groups) {
if (!group.deleted) {
let groupId;
let groupId: string;
// Ignores internal category and credit cards
if (
!equalsIgnoreCase(group.name, 'Internal Master Category') &&
!equalsIgnoreCase(group.name, 'Credit Card Payments') &&
!equalsIgnoreCase(group.name, 'Hidden Categories') &&
!equalsIgnoreCase(group.name, 'Income')
) {
let run = true;
const MAX_RETRY = 10;
let count = 1;
const origName = group.name;
while (run) {
try {
groupId = await actual.createCategoryGroup({
name: group.name,
is_income: false,
hidden: group.hidden,
});
entityIdMap.set(group.id, groupId);
if (group.note) {
send('notes-save', { id: groupId, note: group.note });
}
run = false;
} catch (e) {
group.name = origName + '-' + count.toString();
count += 1;
if (count >= MAX_RETRY) {
run = false;
throw Error(e.message);
}
}
const createdGroup = await createCategoryGroupWithUniqueName({
name: group.name,
is_income: false,
hidden: group.hidden,
});
groupId = createdGroup.id;
entityIdMap.set(group.id, groupId);
if (group.note) {
send('notes-save', { id: groupId, note: group.note });
}
}

Expand Down Expand Up @@ -379,30 +422,20 @@ async function importCategories(
case 'internal': // uncategorized is ignored too, handled by actual
break;
default: {
let run = true;
const MAX_RETRY = 10;
let count = 1;
const origName = cat.name;
while (run) {
try {
const id = await actual.createCategory({
name: cat.name,
group_id: groupId,
hidden: cat.hidden,
});
entityIdMap.set(cat.id, id);
if (cat.note) {
send('notes-save', { id, note: cat.note });
}
run = false;
} catch (e) {
cat.name = origName + '-' + count.toString();
count += 1;
if (count >= MAX_RETRY) {
run = false;
throw Error(e.message);
}
}
if (!groupId) {
break;
}
const createdCategory = await createCategoryWithUniqueName({
name: cat.name,
group_id: groupId,
hidden: cat.hidden,
});
entityIdMap.set(cat.id, createdCategory.id);
if (cat.note) {
send('notes-save', {
id: createdCategory.id,
note: cat.note,
});
}
}
}
Expand Down Expand Up @@ -785,15 +818,15 @@ async function importScheduledTransactions(
date: RecurConfig | string;
}) {
const baseName = params.name;
const MAX_RETRY = 50;
let count = 1;

while (true) {
try {
return await actual.createSchedule({ ...params, name: params.name });
} catch (e) {
if (count >= MAX_RETRY) {
throw Error(e.message);
const errorMsg = normalizeError(e);
throw Error(errorMsg);
}
params.name = `${baseName} (${count})`;
count += 1;
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/6878.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [StephenBrown2]
---

Avoid duplicate category import errors in YNAB5 importer
Loading