diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.test.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.test.ts index 7baefbac..62cc9174 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.test.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.test.ts @@ -7,6 +7,40 @@ import ClearedEnum = SaveTransaction.ClearedEnum; describe('ynab', () => { describe('isSameTransaction', () => { + test('Two transactions with different payee names should be considered the same if they have the same import id', () => { + const differentPayeeTransactionFromYnab: TransactionDetail = { + id: '579ae642-d161-4bbe-9d54-ae3322c93cf7', + date: '2019-06-27', + amount: -1000000, + memo: null, + cleared: SaveTransaction.ClearedEnum.Cleared, + approved: true, + flag_color: null, + account_id: 'SOME_ACCOUNT_ID', + account_name: 'My great account', + payee_id: 'fd7f187c-0633-434f-aaxe-1fevd68492cb', + payee_name: 'שיק', + category_id: null, + category_name: null, + transfer_account_id: null, + transfer_transaction_id: null, + matched_transaction_id: null, + import_id: '2019-06-27-1000000שיק', + deleted: false, + subtransactions: [], + }; + const transactionFromFinancialAccount: SaveTransaction = { + account_id: 'SOME_ACCOUNT_ID', + date: '2019-06-27', + amount: -1000000, + payee_name: 'גן', + category_id: '4e0ttc69-b4f6-420b-8d07-986c8225a3d4', + cleared: ClearedEnum.Cleared, + import_id: '2019-06-27-1000000שיק', + }; + + expect(ynab.isSameTransaction(transactionFromFinancialAccount, differentPayeeTransactionFromYnab)).toBeTruthy(); + }); test('Two transactions with different payee names should be considered the same if one of them is a transfer transaction', () => { const transferTransactionFromYnab: TransactionDetail = { id: '579ae642-d161-4bbe-9d54-ae3322c93cf7', diff --git a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts index 3394d7d0..b555d5ce8d 100644 --- a/packages/main/src/backend/export/outputVendors/ynab/ynab.ts +++ b/packages/main/src/backend/export/outputVendors/ynab/ynab.ts @@ -16,6 +16,7 @@ import * as ynab from 'ynab'; const YNAB_DATE_FORMAT = 'YYYY-MM-DD'; const NOW = moment(); const MIN_YNAB_ACCESS_TOKEN_LENGTH = 43; +const MAX_YNAB_IMPORT_ID_LENGTH = 36; const categoriesMap = new Map>(); const transactionsFromYnab = new Map(); @@ -109,7 +110,6 @@ export function getPayeeName(transaction: EnrichedTransaction, payeeNameMaxLengt function convertTransactionToYnabFormat(originalTransaction: EnrichedTransaction): ynab.SaveTransaction { const amount = Math.round(originalTransaction.chargedAmount * 1000); const date = convertTimestampToYnabDateFormat(originalTransaction); - return { account_id: getYnabAccountIdByAccountNumberFromTransaction(originalTransaction.accountNumber), date, // "2019-01-17", @@ -119,12 +119,20 @@ function convertTransactionToYnabFormat(originalTransaction: EnrichedTransaction category_id: getYnabCategoryIdFromCategoryName(originalTransaction.category), memo: originalTransaction.memo, cleared: ynab.SaveTransaction.ClearedEnum.Cleared, + import_id: buildImportId(originalTransaction), // [date][amount][description] // "approved": true, // "flag_color": "red", // "import_id": buildImportId(originalTransaction.description, amount, date) // 'YNAB:[milliunit_amount]:[iso_date]:[occurrence]' }; } +function buildImportId(transaction: EnrichedTransaction): string { + return `${transaction.date.substring(0, 10)}${transaction.chargedAmount}${transaction.description}`.substring( + 0, + MAX_YNAB_IMPORT_ID_LENGTH, + ); +} + function getYnabAccountIdByAccountNumberFromTransaction(transactionAccountNumber: string): string { const ynabAccountId = ynabConfig!.options.accountNumbersToYnabAccountIds[transactionAccountNumber]; if (!ynabAccountId) { @@ -189,6 +197,7 @@ export function isSameTransaction( transactionFromYnab: ynab.TransactionDetail, ) { const isATransferTransaction = !!transactionFromYnab.transfer_account_id; + const isTransactionsImportIdEqual = isSameImportId(transactionToCreate, transactionFromYnab); return ( transactionToCreate.account_id === transactionFromYnab.account_id && transactionToCreate.date === transactionFromYnab.date && @@ -196,7 +205,8 @@ export function isSameTransaction( Math.abs(transactionToCreate.amount - transactionFromYnab.amount) < 1000 && // In a transfer transaction the payee name changes, but we still consider this the same transaction (areStringsEqualIgnoreCaseAndWhitespace(transactionToCreate.payee_name, transactionFromYnab.payee_name) || - isATransferTransaction) + isATransferTransaction || + isTransactionsImportIdEqual) ); } @@ -321,3 +331,10 @@ export const ynabOutputVendor: OutputVendor = { init, exportTransactions: createTransactions, }; + +function isSameImportId( + transactionToCreate: ynab.SaveTransaction, + transactionFromYnab: ynab.TransactionDetail, +): boolean { + return !!transactionToCreate.import_id && transactionToCreate.import_id === transactionFromYnab.import_id; +}