From 005bb0a8f574b850e4495a7fdc8ee6b337e027fb Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Sun, 9 Feb 2025 22:30:49 +0200 Subject: [PATCH 01/11] Skip unmapped YNAB accounts --- packages/main/src/backend/commonTypes.ts | 9 +++ .../backend/export/outputVendors/ynab/ynab.ts | 55 +++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/main/src/backend/commonTypes.ts b/packages/main/src/backend/commonTypes.ts index 78def43e..56fc902b 100644 --- a/packages/main/src/backend/commonTypes.ts +++ b/packages/main/src/backend/commonTypes.ts @@ -100,6 +100,15 @@ export type ExportTransactionsFunction = ( eventPublisher: EventPublisher, ) => Promise; +export interface ExportTransactionsForAccountParams extends ExportTransactionsParams { + accountNumber: string; +} + +export type ExportTransactionsForAccountFunction = ( + exportTransactionsParams: ExportTransactionsForAccountParams, + eventPublisher: EventPublisher, +) => Promise; + export interface OutputVendor { name: OutputVendorName; init?: (outputVendorsConfig: Config['outputVendors']) => Promise; diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index b555d5ce8d..d50e832e 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -2,6 +2,7 @@ import { OutputVendorName, type Config, type EnrichedTransaction, + type ExportTransactionsForAccountFunction, type ExportTransactionsFunction, type OutputVendor, type YnabAccountDetails, @@ -37,14 +38,50 @@ async function initFromToken(accessToken?: string) { } } -const createTransactions: ExportTransactionsFunction = async ({ transactionsToCreate, startDate }, eventPublisher) => { +const createTransactions: ExportTransactionsFunction = async ( + { transactionsToCreate, startDate, outputVendorsConfig }, + eventPublisher, +) => { + if (!categoriesMap.size) { + await initCategories(); + } + const accountNumbers = _.uniq(transactionsToCreate.map((t) => t.accountNumber)); + const promises = accountNumbers.map((accountNumber) => + createTransactionsForAccount( + { + accountNumber, + transactionsToCreate, + startDate, + outputVendorsConfig, + }, + eventPublisher, + ), + ); + const results = await Promise.all(promises); + const exportedTransactionsNums = results.map((v) => v.exportedTransactionsNum); + return { + exportedTransactionsNum: _.sum(exportedTransactionsNums), + }; +}; + +const createTransactionsForAccount: ExportTransactionsForAccountFunction = async ( + { accountNumber, transactionsToCreate: allTransactions, startDate }, + eventPublisher, +) => { if (!ynabConfig) { throw new Error('Must call init before using ynab functions'); } - if (!categoriesMap.size) { - await initCategories(); + try { + getYnabAccountIdByAccountNumberFromTransaction(accountNumber); + } catch (e) { + await emitProgressEvent(eventPublisher, allTransactions, `Account ${accountNumber} is unmapped. Skipping.`); + return { + exportedTransactionsNum: 0, + }; } - const transactionsFromFinancialAccount = transactionsToCreate.map(convertTransactionToYnabFormat); + const transactionsFromFinancialAccount = allTransactions + .filter((v) => v.accountNumber === accountNumber) + .map(convertTransactionToYnabFormat); let transactionsThatDontExistInYnab = await filterOnlyTransactionsThatDontExistInYnabAlready( startDate, transactionsFromFinancialAccount, @@ -54,11 +91,7 @@ const createTransactions: ExportTransactionsFunction = async ({ transactionsToCr moment(transaction.date, YNAB_DATE_FORMAT).isBefore(NOW), ); if (!transactionsThatDontExistInYnab.length) { - await emitProgressEvent( - eventPublisher, - transactionsToCreate, - 'All transactions already exist in ynab. Doing nothing.', - ); + await emitProgressEvent(eventPublisher, allTransactions, 'All transactions already exist in ynab. Doing nothing.'); return { exportedTransactionsNum: 0, }; @@ -66,7 +99,7 @@ const createTransactions: ExportTransactionsFunction = async ({ transactionsToCr await emitProgressEvent( eventPublisher, - transactionsToCreate, + allTransactions, `Creating ${transactionsThatDontExistInYnab.length} transactions in ynab`, ); try { @@ -83,7 +116,7 @@ const createTransactions: ExportTransactionsFunction = async ({ transactionsToCr message: (e as Error).message, error: e as Error, exporterName: ynabOutputVendor.name, - allTransactions: transactionsToCreate, + allTransactions: allTransactions, }), ); console.error('Failed to create transactions in ynab', e); From 06658f1297c5859b2bac27d201c688c1d990b47f Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:01:34 +0200 Subject: [PATCH 02/11] Add budget id to ynab mapping table --- .../exporters/YnabAccountMappingTable.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx b/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx index e65dfaec..f664a7b5 100644 --- a/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx +++ b/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx @@ -44,6 +44,21 @@ const YnabAccountMappingTable = ({ type: Type.TEXT, }, }, + { + dataField: 'ynabBudgetId', + text: 'Ynab budget id', + editor: { + type: Type.SELECT, + getOptions: () => { + return ynabAccountData?.ynabAccountData?.budgets.map((ynabBudget) => { + return { + label: ynabBudget.name, + value: ynabBudget.id, + }; + }); + }, + }, + }, { dataField: 'ynabAccountId', text: 'Ynab account id', From 215583e62da37f6a47905d0bf2af744dc7164586 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:28:53 +0200 Subject: [PATCH 03/11] Update accountNumbersToYnabAccountIds type to contain budgetId --- packages/main/src/backend/commonTypes.ts | 2 +- .../backend/export/outputVendors/ynab/ynab.ts | 2 +- packages/preload/src/commonTypes.ts | 2 +- .../exporters/YnabAccountMappingTable.tsx | 9 ++++++--- packages/renderer/src/store/Store.test.tsx | 8 ++++---- .../store/__snapshots__/Store.test.tsx.snap | 20 +++++++++++++++---- packages/renderer/src/types.tsx | 2 +- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/main/src/backend/commonTypes.ts b/packages/main/src/backend/commonTypes.ts index 56fc902b..48090195 100644 --- a/packages/main/src/backend/commonTypes.ts +++ b/packages/main/src/backend/commonTypes.ts @@ -65,7 +65,7 @@ export interface GoogleSheetsConfig extends OutputVendorConfigBase { export interface YnabConfig extends OutputVendorConfigBase { options: { accessToken: string; - accountNumbersToYnabAccountIds: Record; + accountNumbersToYnabAccountIds: Record; budgetId: string; maxPayeeNameLength?: number; }; diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index d50e832e..d4809d81 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -171,7 +171,7 @@ function getYnabAccountIdByAccountNumberFromTransaction(transactionAccountNumber if (!ynabAccountId) { throw new Error(`Unhandled account number ${transactionAccountNumber}`); } - return ynabAccountId; + return ynabAccountId.ynabAccountId; } function convertTimestampToYnabDateFormat(originalTransaction: EnrichedTransaction): string { diff --git a/packages/preload/src/commonTypes.ts b/packages/preload/src/commonTypes.ts index bac6c7c2..3d53bb1a 100644 --- a/packages/preload/src/commonTypes.ts +++ b/packages/preload/src/commonTypes.ts @@ -67,7 +67,7 @@ export interface GoogleSheetsConfig extends OutputVendorConfigBase { export interface YnabConfig extends OutputVendorConfigBase { options: { accessToken: string; - accountNumbersToYnabAccountIds: Record; + accountNumbersToYnabAccountIds: Record; budgetId: string; maxPayeeNameLength?: number; }; diff --git a/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx b/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx index f664a7b5..3a280983 100644 --- a/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx +++ b/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx @@ -17,6 +17,7 @@ interface YnabAccountMappingTableProps { type AccountMappingArray = { accountNumber: string; + ynabBudgetId: string; ynabAccountId: string; index?: number; }[]; @@ -94,6 +95,7 @@ const YnabAccountMappingTable = ({ ...accountMappingArray, { accountNumber: '12345678', + ynabBudgetId: '##########', ynabAccountId: '##########', index: accountMappingArray.length, }, @@ -117,15 +119,16 @@ function accountMappingObjectToArray( return { index, accountNumber, - ynabAccountId: accountNumbersToYnabAccountIds[accountNumber], + ynabBudgetId: accountNumbersToYnabAccountIds[accountNumber].ynabBudgetId, + ynabAccountId: accountNumbersToYnabAccountIds[accountNumber].ynabAccountId, }; }); } function accountMappingArrayToObject(accountMappingArray: AccountMappingArray) { const mappingObject: AccountNumberToYnabAccountIdMappingObject = {}; - accountMappingArray.forEach(({ accountNumber, ynabAccountId }) => { - mappingObject[accountNumber] = ynabAccountId; + accountMappingArray.forEach(({ accountNumber, ynabBudgetId, ynabAccountId }) => { + mappingObject[accountNumber] = { ynabBudgetId, ynabAccountId }; }); return mappingObject; } diff --git a/packages/renderer/src/store/Store.test.tsx b/packages/renderer/src/store/Store.test.tsx index 1ae4f397..95e9b991 100644 --- a/packages/renderer/src/store/Store.test.tsx +++ b/packages/renderer/src/store/Store.test.tsx @@ -156,10 +156,10 @@ export const dummyConfig: Config = { accessToken: '################-######-####-###', budgetId: 'advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf', accountNumbersToYnabAccountIds: { - 5555: 'asdvasdvsdvs', - 1111: 'xcvxcvxcvsd', - 3333: 'xvxcdv', - 555555555: 'vsdvsdvserverv', + 5555: { ynabBudgetId: 'advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf', ynabAccountId: 'asdvasdvsdvs' }, + 1111: { ynabBudgetId: 'advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf', ynabAccountId: 'xcvxcvxcvsd' }, + 3333: { ynabBudgetId: 'advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf', ynabAccountId: 'xvxcdv' }, + 555555555: { ynabBudgetId: 'advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf', ynabAccountId: 'vsdvsdvserverv' }, }, }, }, diff --git a/packages/renderer/src/store/__snapshots__/Store.test.tsx.snap b/packages/renderer/src/store/__snapshots__/Store.test.tsx.snap index 40f7ac9d..1abca400 100644 --- a/packages/renderer/src/store/__snapshots__/Store.test.tsx.snap +++ b/packages/renderer/src/store/__snapshots__/Store.test.tsx.snap @@ -45,10 +45,22 @@ exports[`Store > Properties and getters > exporters 1`] = ` "options": { "accessToken": "################-######-####-###", "accountNumbersToYnabAccountIds": { - "1111": "xcvxcvxcvsd", - "3333": "xvxcdv", - "5555": "asdvasdvsdvs", - "555555555": "vsdvsdvserverv", + "1111": { + "ynabAccountId": "xcvxcvxcvsd", + "ynabBudgetId": "advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf", + }, + "3333": { + "ynabAccountId": "xvxcdv", + "ynabBudgetId": "advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf", + }, + "5555": { + "ynabAccountId": "asdvasdvsdvs", + "ynabBudgetId": "advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf", + }, + "555555555": { + "ynabAccountId": "vsdvsdvserverv", + "ynabBudgetId": "advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf", + }, }, "budgetId": "advasdvasd-asdvasdva-sdvasdvasdv-asdvasdvf", }, diff --git a/packages/renderer/src/types.tsx b/packages/renderer/src/types.tsx index f24fb543..fdef92f3 100644 --- a/packages/renderer/src/types.tsx +++ b/packages/renderer/src/types.tsx @@ -70,7 +70,7 @@ export interface GoogleSheetsConfig extends OutputVendorConfigBase { export interface YnabConfig extends OutputVendorConfigBase { options: { accessToken: string; - accountNumbersToYnabAccountIds: Record; + accountNumbersToYnabAccountIds: Record; budgetId: string; maxPayeeNameLength?: number; }; From 674f25b53902725b1e68463f0c43dbcf60aa7149 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:06:42 +0200 Subject: [PATCH 04/11] Filter ynab accounts by row budget --- .../components/exporters/YnabAccountMappingTable.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx b/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx index 3a280983..ec361dba 100644 --- a/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx +++ b/packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx @@ -15,12 +15,14 @@ interface YnabAccountMappingTableProps { onUpdate: (accountNumberToYnabIdMapping: AccountNumberToYnabAccountIdMappingObject) => void; } -type AccountMappingArray = { +interface AccountMapping { accountNumber: string; ynabBudgetId: string; ynabAccountId: string; index?: number; -}[]; +} + +type AccountMappingArray = AccountMapping[]; const YnabAccountMappingTable = ({ accountNumberToYnabIdMapping, @@ -65,7 +67,9 @@ const YnabAccountMappingTable = ({ text: 'Ynab account id', editor: { type: Type.SELECT, - getOptions: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getOptions: (_: any, { row }: { row: AccountMapping }) => { + const budgetId = (row as AccountMapping).ynabBudgetId; return ynabAccountData?.ynabAccountData?.accounts .filter((ynabAccount) => ynabAccount.active && ynabAccount.budgetId === budgetId) .map((ynabAccount) => { @@ -95,7 +99,7 @@ const YnabAccountMappingTable = ({ ...accountMappingArray, { accountNumber: '12345678', - ynabBudgetId: '##########', + ynabBudgetId: budgetId, ynabAccountId: '##########', index: accountMappingArray.length, }, From 9c942cee6b33b65ca1dd38ebe1c6bfdb257d22bf Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:51:12 +0200 Subject: [PATCH 05/11] Store ynab categories per budget --- .../backend/export/outputVendors/ynab/ynab.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index d4809d81..ee630cd1 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -19,7 +19,9 @@ const NOW = moment(); const MIN_YNAB_ACCESS_TOKEN_LENGTH = 43; const MAX_YNAB_IMPORT_ID_LENGTH = 36; -const categoriesMap = new Map>(); +type YnabCategory = Pick; + +const budgetCategoriesMap = new Map>(); const transactionsFromYnab = new Map(); let ynabConfig: YnabConfig | undefined; @@ -42,7 +44,7 @@ const createTransactions: ExportTransactionsFunction = async ( { transactionsToCreate, startDate, outputVendorsConfig }, eventPublisher, ) => { - if (!categoriesMap.size) { + if (!budgetCategoriesMap.size) { await initCategories(); } const accountNumbers = _.uniq(transactionsToCreate.map((t) => t.accountNumber)); @@ -182,7 +184,8 @@ function getYnabCategoryIdFromCategoryName(categoryName?: string) { if (!categoryName) { return null; } - const categoryToReturn = categoriesMap.get(categoryName); + // TODO: pass budgetId + const categoryToReturn = budgetCategoriesMap.values().next().value?.get(categoryName); if (!categoryToReturn) { return null; } @@ -190,17 +193,22 @@ function getYnabCategoryIdFromCategoryName(categoryName?: string) { } export async function initCategories() { - const categories = await ynabAPI!.categories.getCategories(ynabConfig!.options.budgetId); - categories.data.category_groups.forEach((categoryGroup) => { - categoryGroup.categories - .map((category) => ({ - id: category.id, - name: category.name, - category_group_id: category.category_group_id, - })) - .forEach((category) => { - categoriesMap.set(category.name, category); - }); + const budgetIds = Object.values(ynabConfig!.options.accountNumbersToYnabAccountIds).map((v) => v.ynabBudgetId); + budgetIds.forEach(async (budgetId) => { + const categories = await ynabAPI!.categories.getCategories(budgetId); + const categoriesMap = new Map(); + categories.data.category_groups.forEach((categoryGroup) => { + categoryGroup.categories + .map((category) => ({ + id: category.id, + name: category.name, + category_group_id: category.category_group_id, + })) + .forEach((category) => { + categoriesMap.set(category.name, category); + }); + }); + budgetCategoriesMap.set(budgetId, categoriesMap); }); } From caa463131ae212db6621cf61194484e75b605d84 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:57:13 +0200 Subject: [PATCH 06/11] Add method for getting budget by account number --- .../main/src/backend/export/outputVendors/ynab/ynab.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index ee630cd1..07cf5aea 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -75,6 +75,7 @@ const createTransactionsForAccount: ExportTransactionsForAccountFunction = async } try { getYnabAccountIdByAccountNumberFromTransaction(accountNumber); + getYnabBudgetIdByAccountNumberFromTransaction(accountNumber); } catch (e) { await emitProgressEvent(eventPublisher, allTransactions, `Account ${accountNumber} is unmapped. Skipping.`); return { @@ -176,6 +177,14 @@ function getYnabAccountIdByAccountNumberFromTransaction(transactionAccountNumber return ynabAccountId.ynabAccountId; } +function getYnabBudgetIdByAccountNumberFromTransaction(transactionAccountNumber: string): string { + const ynabAccount = ynabConfig!.options.accountNumbersToYnabAccountIds[transactionAccountNumber]; + if (!ynabAccount) { + throw new Error(`Unhandled account number ${transactionAccountNumber}`); + } + return ynabAccount.ynabBudgetId; +} + function convertTimestampToYnabDateFormat(originalTransaction: EnrichedTransaction): string { return moment(originalTransaction.date).format(YNAB_DATE_FORMAT); // 2018-12-29T22:00:00.000Z -> 2018-12-29 } From 5f24135b2867e70f416e6541238b53608b478408 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:59:14 +0200 Subject: [PATCH 07/11] Use account-specific budgetId --- packages/main/src/backend/export/outputVendors/ynab/ynab.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index 07cf5aea..1c2618f6 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -73,9 +73,10 @@ const createTransactionsForAccount: ExportTransactionsForAccountFunction = async if (!ynabConfig) { throw new Error('Must call init before using ynab functions'); } + let budgetId: string; try { getYnabAccountIdByAccountNumberFromTransaction(accountNumber); - getYnabBudgetIdByAccountNumberFromTransaction(accountNumber); + budgetId = getYnabBudgetIdByAccountNumberFromTransaction(accountNumber); } catch (e) { await emitProgressEvent(eventPublisher, allTransactions, `Account ${accountNumber} is unmapped. Skipping.`); return { @@ -106,7 +107,7 @@ const createTransactionsForAccount: ExportTransactionsForAccountFunction = async `Creating ${transactionsThatDontExistInYnab.length} transactions in ynab`, ); try { - await ynabAPI!.transactions.createTransactions(ynabConfig.options.budgetId, { + await ynabAPI!.transactions.createTransactions(budgetId, { transactions: transactionsThatDontExistInYnab, }); return { From 1a3dc7bf8d0e902c3369514ce86f48dccc2d5487 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:07:33 +0200 Subject: [PATCH 08/11] get category from relevant budget --- .../main/src/backend/export/outputVendors/ynab/ynab.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index 1c2618f6..f64a9784 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -147,13 +147,14 @@ export function getPayeeName(transaction: EnrichedTransaction, payeeNameMaxLengt function convertTransactionToYnabFormat(originalTransaction: EnrichedTransaction): ynab.SaveTransaction { const amount = Math.round(originalTransaction.chargedAmount * 1000); const date = convertTimestampToYnabDateFormat(originalTransaction); + const budgetId = getYnabBudgetIdByAccountNumberFromTransaction(originalTransaction.accountNumber); return { account_id: getYnabAccountIdByAccountNumberFromTransaction(originalTransaction.accountNumber), date, // "2019-01-17", amount, // "payee_id": "string", payee_name: getPayeeName(originalTransaction, ynabConfig!.options.maxPayeeNameLength), - category_id: getYnabCategoryIdFromCategoryName(originalTransaction.category), + category_id: getYnabCategoryIdFromCategoryName(budgetId, originalTransaction.category), memo: originalTransaction.memo, cleared: ynab.SaveTransaction.ClearedEnum.Cleared, import_id: buildImportId(originalTransaction), // [date][amount][description] @@ -190,12 +191,11 @@ function convertTimestampToYnabDateFormat(originalTransaction: EnrichedTransacti return moment(originalTransaction.date).format(YNAB_DATE_FORMAT); // 2018-12-29T22:00:00.000Z -> 2018-12-29 } -function getYnabCategoryIdFromCategoryName(categoryName?: string) { +function getYnabCategoryIdFromCategoryName(budgetId: string, categoryName?: string) { if (!categoryName) { return null; } - // TODO: pass budgetId - const categoryToReturn = budgetCategoriesMap.values().next().value?.get(categoryName); + const categoryToReturn = budgetCategoriesMap.get(budgetId)?.get(categoryName); if (!categoryToReturn) { return null; } From e85bc5eee2ba478a39a2ffedec5ee489eb7d7f06 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:10:56 +0200 Subject: [PATCH 09/11] get ynab transactions by budget --- .../src/backend/export/outputVendors/ynab/ynab.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index f64a9784..dae58b7d 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -87,6 +87,7 @@ const createTransactionsForAccount: ExportTransactionsForAccountFunction = async .filter((v) => v.accountNumber === accountNumber) .map(convertTransactionToYnabFormat); let transactionsThatDontExistInYnab = await filterOnlyTransactionsThatDontExistInYnabAlready( + budgetId, startDate, transactionsFromFinancialAccount, ); @@ -128,11 +129,8 @@ const createTransactionsForAccount: ExportTransactionsForAccountFunction = async } }; -function getTransactions(startDate: Date): Promise { - return ynabAPI!.transactions.getTransactions( - ynabConfig!.options.budgetId, - moment(startDate).format(YNAB_DATE_FORMAT), - ); +function getTransactions(budgetId: string, startDate: Date): Promise { + return ynabAPI!.transactions.getTransactions(budgetId, moment(startDate).format(YNAB_DATE_FORMAT)); } export function getPayeeName(transaction: EnrichedTransaction, payeeNameMaxLength = 50) { @@ -223,6 +221,7 @@ export async function initCategories() { } async function filterOnlyTransactionsThatDontExistInYnabAlready( + budgetId: string, startDate: Date, transactionsFromFinancialAccounts: ynab.SaveTransaction[], ) { @@ -230,7 +229,7 @@ async function filterOnlyTransactionsThatDontExistInYnabAlready( if (transactionsFromYnab.has(startDate)) { transactionsInYnabBeforeCreatingTheseTransactions = transactionsFromYnab.get(startDate)!; } else { - const transactionsFromYnabResponse = await getTransactions(startDate); + const transactionsFromYnabResponse = await getTransactions(budgetId, startDate); transactionsInYnabBeforeCreatingTheseTransactions = transactionsFromYnabResponse.data.transactions; transactionsFromYnab.set(startDate, transactionsInYnabBeforeCreatingTheseTransactions); } From 04a8232ab2291dc29e65dc2b016f0008c34ab5f4 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:12:20 +0200 Subject: [PATCH 10/11] get ynab categories by budget --- packages/main/src/backend/export/outputVendors/ynab/ynab.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index dae58b7d..72dbd9ab 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -287,7 +287,7 @@ export async function getYnabAccountDetails( let categories: YnabAccountDetails['categories']; if (doesBudgetIdExistInYnab(budgetIdToCheck)) { console.log('Getting ynab categories'); - categories = await getYnabCategories(); + categories = await getYnabCategories(budgetIdToCheck); } else { // eslint-disable-next-line console.warn(`Budget id ${budgetIdToCheck} doesn't exist in ynab`); @@ -354,8 +354,8 @@ async function getBudgetsAndAccountsData() { }; } -async function getYnabCategories() { - const categoriesResponse = await ynabAPI!.categories.getCategories(ynabConfig!.options.budgetId); +async function getYnabCategories(budgetId: string) { + const categoriesResponse = await ynabAPI!.categories.getCategories(budgetId); const categories = _.flatMap(categoriesResponse.data.category_groups, (categoryGroup) => categoryGroup.categories); const categoryNames = categories.map((category) => category.name); return categoryNames; From bb3a7e7993c67bb535560b23719b4d7fa35c54b6 Mon Sep 17 00:00:00 2001 From: whatuserever <83753332+whatuserever@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:09:41 +0200 Subject: [PATCH 11/11] cleanly await each iteration of initCategories --- .../backend/export/outputVendors/ynab/ynab.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index 72dbd9ab..4b1ce2e5 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -202,22 +202,24 @@ function getYnabCategoryIdFromCategoryName(budgetId: string, categoryName?: stri export async function initCategories() { const budgetIds = Object.values(ynabConfig!.options.accountNumbersToYnabAccountIds).map((v) => v.ynabBudgetId); - budgetIds.forEach(async (budgetId) => { - const categories = await ynabAPI!.categories.getCategories(budgetId); - const categoriesMap = new Map(); - categories.data.category_groups.forEach((categoryGroup) => { - categoryGroup.categories - .map((category) => ({ - id: category.id, - name: category.name, - category_group_id: category.category_group_id, - })) - .forEach((category) => { - categoriesMap.set(category.name, category); - }); - }); - budgetCategoriesMap.set(budgetId, categoriesMap); - }); + await Promise.all( + budgetIds.map(async (budgetId) => { + const categories = await ynabAPI!.categories.getCategories(budgetId); + const categoriesMap = new Map(); + categories.data.category_groups.forEach((categoryGroup) => { + categoryGroup.categories + .map((category) => ({ + id: category.id, + name: category.name, + category_group_id: category.category_group_id, + })) + .forEach((category) => { + categoriesMap.set(category.name, category); + }); + }); + budgetCategoriesMap.set(budgetId, categoriesMap); + }), + ); } async function filterOnlyTransactionsThatDontExistInYnabAlready(