diff --git a/apps/api/src/app/import/import.service.spec.ts b/apps/api/src/app/import/import.service.spec.ts index 5858cf0b65..05f609d788 100644 --- a/apps/api/src/app/import/import.service.spec.ts +++ b/apps/api/src/app/import/import.service.spec.ts @@ -456,6 +456,58 @@ describe('ImportService', () => { { dataSource: DataSource.YAHOO, symbol: 'AAPL' } ]); }); + + it('detects duplicate activities within the same import payload', async () => { + const activities = await importActivities({ + activitiesDto: [ + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }), + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }) + ], + isDryRun: true + }); + + expect(activities[0].error).toBeUndefined(); + expect(activities[1].error).toEqual({ code: 'IS_DUPLICATE' }); + }); + + it('does not flag identical activities across different accounts as duplicates', async () => { + const activities = await importActivities({ + activitiesDto: [ + { + ...createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }), + accountId: 'account-1' + }, + { + ...createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }), + accountId: 'account-2' + } + ], + isDryRun: true + }); + + expect(activities[0].error).toBeUndefined(); + expect(activities[1].error).toBeUndefined(); + }); }); function importActivities({ diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 0f44fc2855..53b4191da7 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -1045,6 +1045,9 @@ export class ImportService { withExcludedAccountsAndActivities: true }); + const processedActivities: (Partial & + Pick)[] = []; + return activitiesDto.map( ({ accountId, @@ -1061,26 +1064,42 @@ export class ImportService { }) => { const date = parseISO(dateString); - const isDuplicate = existingActivities.some((activity) => { - return ( - (activity.comment || null) === (comment || null) && - (activity.currency === currency || - activity.assetProfile.currency === currency) && - activity.assetProfile.dataSource === dataSource && - isSameSecond(activity.date, date) && - activity.fee === fee && - activity.quantity === quantity && - activity.assetProfile.symbol === symbol && - activity.type === type && - activity.unitPrice === unitPrice - ); - }); + const isDuplicate = + existingActivities.some((activity) => { + return ( + (!accountId || activity.accountId === accountId) && + (activity.comment || null) === (comment || null) && + ((activity.currency || null) === (currency || null) || + activity.assetProfile.currency === currency) && + activity.assetProfile.dataSource === dataSource && + isSameSecond(activity.date, date) && + activity.fee === fee && + activity.quantity === quantity && + activity.assetProfile.symbol === symbol && + activity.type === type && + activity.unitPrice === unitPrice + ); + }) || + processedActivities.some((activity) => { + return ( + (!accountId || activity.accountId === accountId) && + (activity.comment || null) === (comment || null) && + (activity.currency || null) === (currency || null) && + activity.assetProfile.dataSource === dataSource && + isSameSecond(activity.date, date) && + activity.fee === fee && + activity.quantity === quantity && + activity.assetProfile.symbol === symbol && + activity.type === type && + activity.unitPrice === unitPrice + ); + }); const error: ActivityError = isDuplicate ? { code: 'IS_DUPLICATE' } : undefined; - return { + const activityExtended = { accountId, comment, currency, @@ -1107,6 +1126,12 @@ export class ImportService { }, tagIds: tags }; + + if (!error) { + processedActivities.push(activityExtended); + } + + return activityExtended; } ); }