From 6a6459c4a898030d7be38f4256ce144dc8404503 Mon Sep 17 00:00:00 2001 From: AkashNegi1 Date: Tue, 9 Jun 2026 23:43:45 +0530 Subject: [PATCH] Support importing account platforms --- apps/api/src/app/import/import-data.dto.ts | 29 ++++++++++++- apps/api/src/app/import/import.controller.ts | 12 +----- apps/api/src/app/import/import.service.ts | 43 ++++++++++++++++++- .../import-activities-dialog.component.ts | 4 ++ .../app/services/import-activities.service.ts | 13 ++++++ 5 files changed, 88 insertions(+), 13 deletions(-) diff --git a/apps/api/src/app/import/import-data.dto.ts b/apps/api/src/app/import/import-data.dto.ts index bf45c7cda..8e7fdc4f1 100644 --- a/apps/api/src/app/import/import-data.dto.ts +++ b/apps/api/src/app/import/import-data.dto.ts @@ -6,9 +6,36 @@ import { } from '@ghostfolio/common/dtos'; import { Type } from 'class-transformer'; -import { IsArray, IsOptional, ValidateNested } from 'class-validator'; +import { + IsArray, + IsOptional, + ValidateNested, + IsString, + IsUrl +} from 'class-validator'; + +export class ImportPlatformDto { + @IsOptional() + @IsString() + id?: string; + + @IsString() + name: string; + + @IsUrl({ + protocols: ['http', 'https'], + require_protocol: true + }) + url: string; +} export class ImportDataDto { + @IsArray() + @IsOptional() + @Type(() => ImportPlatformDto) + @ValidateNested({ each: true }) + platforms?: ImportPlatformDto[]; + @IsArray() @IsOptional() @Type(() => CreateAccountWithBalancesDto) diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index c3e79a29f..17d982dc1 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -5,7 +5,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { SubscriptionType } from '@ghostfolio/common/enums'; import { ImportResponse } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { @@ -50,15 +50,6 @@ export class ImportController { ): Promise { const isDryRun = isDryRunParam === 'true'; - if ( - !hasPermission(this.request.user.permissions, permissions.createAccount) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - let maxActivitiesToImport = this.configurationService.get( 'MAX_ACTIVITIES_TO_IMPORT' ); @@ -77,6 +68,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 b82f763a0..769b60886 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -179,6 +179,7 @@ export class ImportService { assetProfilesWithMarketDataDto, isDryRun = false, maxActivitiesToImport, + platformsDto, tagsDto, user }: { @@ -187,11 +188,13 @@ 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; @@ -209,6 +212,37 @@ export class ImportService { this.platformService.getPlatforms() ]); + const canCreatePlatform = hasPermission( + user.permissions, + permissions.createPlatform + ); + + for (const platform of platformsDto ?? []) { + const existingPlatform = existingPlatforms.find(({ url }) => { + return url === platform.url; + }); + + if (existingPlatform) { + if (platform.id && platform.id !== existingPlatform.id) { + platformIdMapping[platform.id] = existingPlatform.id; + } + + continue; + } + + if (!canCreatePlatform) { + continue; + } + + const newPlatform = await this.platformService.createPlatform({ + ...(platform.id && { id: platform.id }), + name: platform.name, + url: platform.url + }); + + existingPlatforms.push(newPlatform); + } + for (const accountWithBalances of accountsWithBalancesDto) { // Check if there is any existing account with the same ID const accountWithSameId = existingAccounts.find((existingAccount) => { @@ -240,14 +274,19 @@ export class ImportService { user: { connect: { id: user.id } } }; + const resolvedPlatformId = platformId + ? (platformIdMapping[platformId] ?? platformId) + : undefined; + if ( + resolvedPlatformId && existingPlatforms.some(({ id }) => { - return id === platformId; + return id === resolvedPlatformId; }) ) { accountObject = { ...accountObject, - platform: { connect: { id: platformId } } + platform: { connect: { id: resolvedPlatformId } } }; } 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 c3dbe6cf2..20918c56a 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 @@ -109,6 +109,7 @@ export class GfImportActivitiesDialogComponent { private accounts: CreateAccountWithBalancesDto[] = []; private activities: Activity[] = []; private assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; + private platforms: { id?: string; name: string; url: string }[] = []; private tags: CreateTagDto[] = []; private readonly changeDetectorRef = inject(ChangeDetectorRef); @@ -175,6 +176,7 @@ export class GfImportActivitiesDialogComponent { accounts: this.accounts, activities: this.selectedActivities, assetProfiles: this.assetProfiles, + platforms: this.platforms, tags: this.tags }); @@ -306,6 +308,7 @@ export class GfImportActivitiesDialogComponent { this.accounts = content.accounts; this.assetProfiles = content.assetProfiles; + this.platforms = content.platforms ?? []; this.tags = content.tags; if (!isArray(content.activities)) { @@ -339,6 +342,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 55b5c44d5..0262666d6 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -15,6 +15,12 @@ import { parse as csvToJson } from 'papaparse'; import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; +interface ImportPlatform { + id?: string; + name: string; + url: string; +} + @Injectable({ providedIn: 'root' }) @@ -116,12 +122,14 @@ export class ImportActivitiesService { activities, assetProfiles, isDryRun = false, + platforms, tags }: { activities: CreateOrderDto[]; accounts?: CreateAccountWithBalancesDto[]; assetProfiles?: CreateAssetProfileWithMarketDataDto[]; isDryRun?: boolean; + platforms?: ImportPlatform[]; tags?: CreateTagDto[]; }): Promise<{ activities: Activity[]; @@ -132,6 +140,7 @@ export class ImportActivitiesService { accounts, activities, assetProfiles, + platforms, tags }, isDryRun @@ -154,11 +163,13 @@ export class ImportActivitiesService { accounts, activities, assetProfiles, + platforms, tags }: { accounts?: CreateAccountWithBalancesDto[]; activities: Activity[]; assetProfiles?: CreateAssetProfileWithMarketDataDto[]; + platforms?: ImportPlatform[]; tags?: CreateTagDto[]; }): Promise<{ activities: Activity[]; @@ -172,6 +183,7 @@ export class ImportActivitiesService { return this.importJson({ accounts, assetProfiles, + platforms, tags, activities: importData }); @@ -445,6 +457,7 @@ export class ImportActivitiesService { accounts?: CreateAccountWithBalancesDto[]; activities: CreateOrderDto[]; assetProfiles?: CreateAssetProfileWithMarketDataDto[]; + platforms?: ImportPlatform[]; tags?: CreateTagDto[]; }, aIsDryRun = false