diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f13393d1a..668441686c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the missing localization of the date picker in the asset profile details dialog of the admin control panel - Fixed the missing tooltip of the client of the last request in the users table of the admin control panel +### Fixed + +- Fixed an issue in the activities import where global asset profiles were duplicated as custom asset profiles + ## 3.88.0 - 2026-10-17 ### Added diff --git a/apps/api/src/app/import/import.service.spec.ts b/apps/api/src/app/import/import.service.spec.ts index d6d41134a5..fdf225820a 100644 --- a/apps/api/src/app/import/import.service.spec.ts +++ b/apps/api/src/app/import/import.service.spec.ts @@ -33,6 +33,7 @@ describe('ImportService', () => { let addSymbolProfile: jest.Mock; let createActivity: jest.Mock; let gatherSymbols: jest.Mock; + let getSymbolProfiles: jest.Mock; let importService: ImportService; let updateManyMarketData: jest.Mock; @@ -46,6 +47,7 @@ describe('ImportService', () => { addSymbolProfile = jest.fn(); createActivity = jest.fn(); gatherSymbols = jest.fn(); + getSymbolProfiles = jest.fn().mockResolvedValue([]); updateManyMarketData = jest.fn(); const validateActivities = jest.fn( @@ -94,8 +96,8 @@ describe('ImportService', () => { null, { add: addSymbolProfile, - getCustomSymbolProfilesByNames: jest.fn().mockResolvedValue([]), - getSymbolProfiles: jest.fn().mockResolvedValue([]) + getSymbolProfiles, + getCustomSymbolProfilesByNames: jest.fn().mockResolvedValue([]) } as unknown as SymbolProfileService, { getTagsForUser: jest.fn().mockResolvedValue([]) @@ -481,6 +483,82 @@ describe('ImportService', () => { ]); }); + it('reuses a global asset profile without updating its market data', async () => { + getSymbolProfiles.mockResolvedValue([ + { dataSource: DataSource.YAHOO, symbol: 'AAPL', userId: null } + ]); + + mockCreatedAssetProfiles([ + { dataSource: DataSource.YAHOO, symbol: 'AAPL' } + ]); + + await importActivities({ + activitiesDto: [ + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }) + ], + assetProfilesWithMarketDataDto: [ + { + currency: 'USD', + dataSource: DataSource.YAHOO, + marketData: [{ date: '2024-01-01', marketPrice: 1 }], + name: 'Apple Inc.', + symbol: 'AAPL' + } + ] + }); + + expect(addSymbolProfile).not.toHaveBeenCalled(); + expect(updateManyMarketData).not.toHaveBeenCalled(); + + expect( + createActivity.mock.calls[0][0].SymbolProfile.connectOrCreate.create + ).toMatchObject({ dataSource: DataSource.YAHOO, symbol: 'AAPL' }); + }); + + it('creates a custom asset profile with a UUID instead of the asset profile of another user', async () => { + getSymbolProfiles.mockResolvedValue([ + { + dataSource: DataSource.MANUAL, + symbol: CUSTOM_ASSET_PROFILE_SYMBOL, + userId: 'other-user-id' + } + ]); + + mockCreatedAssetProfiles([ + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + + await importActivities({ + activitiesDto: [ + createActivityDto({ + dataSource: DataSource.MANUAL, + date: '2024-01-01T00:00:00.000Z', + symbol: CUSTOM_ASSET_PROFILE_SYMBOL, + type: 'BUY' + }) + ], + assetProfilesWithMarketDataDto: [ + { + currency: 'USD', + dataSource: DataSource.MANUAL, + marketData: [{ date: '2024-01-01', marketPrice: 100 }], + name: 'Gold', + symbol: CUSTOM_ASSET_PROFILE_SYMBOL + } + ] + }); + + const { symbol } = addSymbolProfile.mock.calls[0][0]; + + expect(isUUID(symbol)).toBe(true); + expect(symbol).not.toBe(CUSTOM_ASSET_PROFILE_SYMBOL); + }); + it('keeps the asset profiles of the activities in a dry run', async () => { const activities = await importActivities({ activitiesDto: [ diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 2d09cb78df..dac423d682 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -624,10 +624,12 @@ export class ImportService { } ); + const isGlobalAssetProfile = existingAssetProfile?.userId === null; + // If there is no asset profile or if the asset profile belongs to a // different user, then reuse the custom asset profile of the user or // create a new asset profile - if (existingAssetProfile?.userId !== user.id) { + if (!isGlobalAssetProfile && existingAssetProfile?.userId !== user.id) { // Check if the user has a custom asset profile with the same name. // Skip asset profiles with a legacy free-text symbol as they would // fail the symbol validation on a future import. @@ -689,7 +691,8 @@ export class ImportService { } } - if (!isDryRun) { + // Never let an import change the market data of a global asset profile + if (!isDryRun && !isGlobalAssetProfile) { const marketDataObjects = ( assetProfileWithMarketData.marketData ?? [] ).map((marketData) => {