From 8ac551fc445fb68a50a680acde23b87db1ae1ab2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:54:35 +0200 Subject: [PATCH] Feature/support account platforms in activities import (#7515) * Support account platforms in activities import * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/import/import-data.dto.ts | 7 +++ apps/api/src/app/import/import.controller.ts | 1 + apps/api/src/app/import/import.service.ts | 44 ++++++++++++++++++- .../import-activities-dialog.component.ts | 5 +++ .../app/services/import-activities.service.ts | 8 ++++ .../src/lib/dtos/create-platform.dto.ts | 6 ++- test/import/not-ok/invalid-platform.json | 42 ++++++++++++++++++ 8 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 test/import/not-ok/invalid-platform.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ef5e2f0..bc4764602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for the account platforms in the activities import - Added the database model and endpoints to manage the stock splits of an asset profile (experimental) ### Changed diff --git a/apps/api/src/app/import/import-data.dto.ts b/apps/api/src/app/import/import-data.dto.ts index bf45c7cda..1ab6fe3e5 100644 --- a/apps/api/src/app/import/import-data.dto.ts +++ b/apps/api/src/app/import/import-data.dto.ts @@ -2,6 +2,7 @@ import { CreateAccountWithBalancesDto, CreateAssetProfileWithMarketDataDto, CreateOrderDto, + CreatePlatformDto, CreateTagDto } from '@ghostfolio/common/dtos'; @@ -26,6 +27,12 @@ export class ImportDataDto { @ValidateNested({ each: true }) assetProfiles?: CreateAssetProfileWithMarketDataDto[]; + @IsArray() + @IsOptional() + @Type(() => CreatePlatformDto) + @ValidateNested({ each: true }) + platforms?: CreatePlatformDto[]; + @IsArray() @IsOptional() @Type(() => CreateTagDto) diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index c2d53e3cb..cd378d07d 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -77,6 +77,7 @@ export class ImportController { accountsWithBalancesDto: importData.accounts ?? [], activitiesDto: importData.activities, assetProfilesWithMarketDataDto: importData.assetProfiles ?? [], + platformsDto: importData.platforms ?? [], tagsDto: importData.tags ?? [], user: this.request.user }); diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index b8579b795..52b0662d6 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -177,6 +177,7 @@ export class ImportService { assetProfilesWithMarketDataDto, isDryRun = false, maxActivitiesToImport, + platformsDto, tagsDto, user }: { @@ -185,14 +186,54 @@ export class ImportService { assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles']; isDryRun?: boolean; maxActivitiesToImport: number; + platformsDto: ImportDataDto['platforms']; tagsDto: ImportDataDto['tags']; user: UserWithSettings; }): Promise { const accountIdMapping: { [oldAccountId: string]: string } = {}; const assetProfileSymbolMapping: { [oldSymbol: string]: string } = {}; + const platformIdMapping: { [oldPlatformId: string]: string } = {}; const tagIdMapping: { [oldTagId: string]: string } = {}; const userCurrency = user.settings.settings.baseCurrency; + if (platformsDto?.length) { + const canCreatePlatform = hasPermission( + user.permissions, + permissions.createPlatform + ); + + const existingPlatforms = await this.platformService.getPlatforms(); + + for (const platform of platformsDto) { + // Check if there is any existing platform with the same ID, otherwise + // fall back to a platform with the same URL + const existingPlatform = + existingPlatforms.find(({ id }) => { + return id === platform.id; + }) ?? + existingPlatforms.find(({ url }) => { + return url === platform.url; + }); + + if (existingPlatform) { + // Store the new to old platform ID mappings for creating accounts + if (platform.id && existingPlatform.id !== platform.id) { + platformIdMapping[platform.id] = existingPlatform.id; + } + } else { + if (!canCreatePlatform) { + throw new Error( + `Insufficient permissions to create platform ("${platform.name}")` + ); + } + + if (!isDryRun) { + await this.platformService.createPlatform(platform); + } + } + } + } + const existingTagsOfUser = tagsDto?.length || (!isDryRun && accountsWithBalancesDto?.length) ? await this.tagService.getTagsForUser(user.id) @@ -282,7 +323,8 @@ export class ImportService { ]); let oldAccountId: string; - const platformId = account.platformId; + const platformId = + platformIdMapping[account.platformId] ?? account.platformId; delete account.platformId; diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 77d889b1a..48f1d5abd 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -4,6 +4,7 @@ import { DEFAULT_DATE_RANGE } from '@ghostfolio/common/config'; import { CreateAccountWithBalancesDto, CreateAssetProfileWithMarketDataDto, + CreatePlatformDto, CreateTagDto } from '@ghostfolio/common/dtos'; import { Activity, PortfolioPosition } from '@ghostfolio/common/interfaces'; @@ -107,6 +108,7 @@ export class GfImportActivitiesDialogComponent { private accounts: CreateAccountWithBalancesDto[] = []; private activities: Activity[] = []; private assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; + private platforms: CreatePlatformDto[] = []; private tags: CreateTagDto[] = []; private readonly changeDetectorRef = inject(ChangeDetectorRef); @@ -173,6 +175,7 @@ export class GfImportActivitiesDialogComponent { accounts: this.accounts, activities: this.selectedActivities, assetProfiles: this.assetProfiles, + platforms: this.platforms, tags: this.tags }); @@ -304,6 +307,7 @@ export class GfImportActivitiesDialogComponent { this.accounts = content.accounts; this.assetProfiles = content.assetProfiles; + this.platforms = content.platforms; this.tags = content.tags; if (!isArray(content.activities)) { @@ -337,6 +341,7 @@ export class GfImportActivitiesDialogComponent { activities: content.activities, assetProfiles: content.assetProfiles, isDryRun: true, + platforms: content.platforms, tags: content.tags }); diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index ff5679718..6a8f99bda 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -2,6 +2,7 @@ import { CreateAccountWithBalancesDto, CreateAssetProfileWithMarketDataDto, CreateOrderDto, + CreatePlatformDto, CreateTagDto } from '@ghostfolio/common/dtos'; import { parseDate as parseDateHelper } from '@ghostfolio/common/helper'; @@ -115,12 +116,14 @@ export class ImportActivitiesService { activities, assetProfiles, isDryRun = false, + platforms, tags }: { activities: CreateOrderDto[]; accounts?: CreateAccountWithBalancesDto[]; assetProfiles?: CreateAssetProfileWithMarketDataDto[]; isDryRun?: boolean; + platforms?: CreatePlatformDto[]; tags?: CreateTagDto[]; }): Promise<{ activities: Activity[]; @@ -131,6 +134,7 @@ export class ImportActivitiesService { accounts, activities, assetProfiles, + platforms, tags }, isDryRun @@ -142,11 +146,13 @@ export class ImportActivitiesService { accounts, activities, assetProfiles, + platforms, tags }: { accounts?: CreateAccountWithBalancesDto[]; activities: Activity[]; assetProfiles?: CreateAssetProfileWithMarketDataDto[]; + platforms?: CreatePlatformDto[]; tags?: CreateTagDto[]; }): Promise<{ activities: Activity[]; @@ -158,6 +164,7 @@ export class ImportActivitiesService { return this.importJson({ accounts, assetProfiles, + platforms, tags, activities: importData }); @@ -457,6 +464,7 @@ export class ImportActivitiesService { accounts?: CreateAccountWithBalancesDto[]; activities: CreateOrderDto[]; assetProfiles?: CreateAssetProfileWithMarketDataDto[]; + platforms?: CreatePlatformDto[]; tags?: CreateTagDto[]; }, aIsDryRun = false diff --git a/libs/common/src/lib/dtos/create-platform.dto.ts b/libs/common/src/lib/dtos/create-platform.dto.ts index e482dd241..5c6453f84 100644 --- a/libs/common/src/lib/dtos/create-platform.dto.ts +++ b/libs/common/src/lib/dtos/create-platform.dto.ts @@ -1,6 +1,10 @@ -import { IsString, IsUrl } from 'class-validator'; +import { IsOptional, IsString, IsUrl } from 'class-validator'; export class CreatePlatformDto { + @IsOptional() + @IsString() + id?: string; + @IsString() name: string; diff --git a/test/import/not-ok/invalid-platform.json b/test/import/not-ok/invalid-platform.json new file mode 100644 index 000000000..69a8e29ca --- /dev/null +++ b/test/import/not-ok/invalid-platform.json @@ -0,0 +1,42 @@ +{ + "meta": { + "date": "2023-02-05T00:00:00.000Z", + "version": "dev" + }, + "accounts": [ + { + "balance": 0, + "balances": [], + "currency": "USD", + "id": "e62be662-a2c8-4cff-8b79-dc0a46576659", + "name": "Lehman Brothers Account", + "platformId": "2807e8e7-3966-4ced-8d82-0fa2d9c97277" + } + ], + "platforms": [ + { + "id": "2807e8e7-3966-4ced-8d82-0fa2d9c97277", + "name": "Lehman Brothers", + "url": "https://lehmanbrothers.com" + } + ], + "activities": [ + { + "accountId": "e62be662-a2c8-4cff-8b79-dc0a46576659", + "fee": 19, + "quantity": 5, + "type": "BUY", + "unitPrice": 298.58, + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-09-16T00:00:00.000Z", + "symbol": "MSFT" + } + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } +}