diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb3bdcc97..f04aa58104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the border color of the drop area in the import activities dialog (dark mode) - Fixed the matching of assets and accounts in the import of activities with _Ghostfolio AI_ (experimental) +- Fixed an issue where the update of an activity could change the asset class and asset sub class of an asset profile which does not belong to the user ## 3.84.2 - 2026-10-11 diff --git a/apps/api/src/app/activities/activities.service.spec.ts b/apps/api/src/app/activities/activities.service.spec.ts index fb2c68ff40..57bd8ab665 100644 --- a/apps/api/src/app/activities/activities.service.spec.ts +++ b/apps/api/src/app/activities/activities.service.spec.ts @@ -18,7 +18,9 @@ import { Activity, Filter } from '@ghostfolio/common/interfaces'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { + AssetClass, AssetProfileSplit, + AssetSubClass, DataSource, Prisma, Type as ActivityType @@ -519,6 +521,105 @@ describe('ActivitiesService', () => { }); }); }); + + describe('updateActivity', () => { + it('updates the custom asset profile of the user', async () => { + const data = await getUpdatedActivityData({ userId: 'user-id' }); + + expect(data.SymbolProfile).toEqual({ + update: { + data: { + assetClass: AssetClass.COMMODITY, + assetSubClass: AssetSubClass.PRECIOUS_METAL + }, + where: { userId: 'user-id' } + } + }); + }); + + it.each([ + { owner: 'the admin', userId: null }, + { owner: 'another user', userId: 'other-user-id' } + ])('does not update the asset profile of $owner', async ({ userId }) => { + const data = await getUpdatedActivityData({ userId }); + + expect(data).not.toHaveProperty('SymbolProfile'); + }); + + it('does not update the asset profile of a data provider', async () => { + const data = await getUpdatedActivityData({ + dataSource: DataSource.YAHOO, + symbol: 'AAPL', + type: ActivityType.FEE, + userId: null + }); + + expect(data).not.toHaveProperty('SymbolProfile'); + }); + + async function getUpdatedActivityData({ + dataSource = DataSource.MANUAL, + symbol = 'GF_GOLD', + type = ActivityType.BUY, + userId + }: { + dataSource?: DataSource; + symbol?: string; + type?: ActivityType; + userId: string | null; + }) { + const update = jest.fn().mockResolvedValue({ userId: 'user-id' }); + + const service = new ActivitiesService( + null, + null, + null, + null, + null, + null, + { emit: jest.fn() } as unknown as EventEmitter2, + null, + null, + { + order: { + update, + findUnique: jest + .fn() + .mockResolvedValue({ SymbolProfile: { userId } }) + } + } as unknown as PrismaService, + null, + { validateTagIds: jest.fn() } as unknown as TagService + ); + + await service.updateActivity({ + data: { + type, + assetClass: AssetClass.COMMODITY, + assetSubClass: AssetSubClass.PRECIOUS_METAL, + date: parseDate('2024-01-01'), + SymbolProfile: { + connect: { + dataSource_symbol: { + dataSource, + symbol + } + }, + update: { + assetClass: AssetClass.COMMODITY, + assetSubClass: AssetSubClass.PRECIOUS_METAL, + name: symbol + } + } + }, + originalDate: parseDate('2024-01-01'), + userId: 'user-id', + where: { id: 'activity-id' } + }); + + return (update.mock.calls[0] as [Prisma.OrderUpdateArgs])[0].data; + } + }); }); function createActivity({ diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 0de3f7c3c4..0dce736f4e 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -933,7 +933,23 @@ export class ActivitiesService { data.type === 'BUY') ) { delete data.SymbolProfile.connect; - delete data.SymbolProfile.update.name; + + const activityToUpdate = await this.prismaService.order.findUnique({ + select: { SymbolProfile: { select: { userId: true } } }, + where + }); + + if (activityToUpdate?.SymbolProfile.userId === userId) { + data.SymbolProfile.update = { + data: { + assetClass: data.assetClass, + assetSubClass: data.assetSubClass + }, + where: { userId } + }; + } else { + delete data.SymbolProfile; + } } else { delete data.SymbolProfile.update;