From 0ffd06ecd3cbe056b867493125abc45defb1214f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:20:07 +0200 Subject: [PATCH] Support migrating asset profile to MANUAL data source --- apps/api/src/app/admin/admin.service.ts | 32 ++++++++++- .../symbol-profile/symbol-profile.service.ts | 9 +++ .../asset-profile-dialog.component.ts | 55 +++++++++++++++++++ .../asset-profile-dialog.html | 11 ++++ 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 8325fa90ec..2ed5716594 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -19,7 +19,8 @@ import { AdminData, AdminUserResponse, AdminUsersResponse, - AssetProfileIdentifier + AssetProfileIdentifier, + Holding } from '@ghostfolio/common/interfaces'; import { PropertyKey } from '@ghostfolio/common/types'; @@ -283,6 +284,35 @@ export class AdminService { ) ]); + if (DataSource[newDataSource.toString()] === DataSource.MANUAL) { + await Promise.all([ + this.symbolProfileService.deleteAssetProfileOverrides({ + dataSource: DataSource.MANUAL, + symbol: newSymbol as string + }), + this.symbolProfileService.updateSymbolProfile( + { + dataSource: DataSource.MANUAL, + symbol: newSymbol as string + }, + { + assetClass, + assetSubClass, + countries, + name, + sectors, + url, + holdings: (holdings as unknown as Holding[])?.map((holding) => { + return { + name: holding.name, + weight: holding.allocationInPercentage + }; + }) + } + ) + ]); + } + const [updatedAssetProfile] = await this.symbolProfileService.getSymbolProfiles([ { diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 7157f0856d..5c057338e8 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -35,6 +35,15 @@ export class SymbolProfileService { }); } + public async deleteAssetProfileOverrides({ + dataSource, + symbol + }: AssetProfileIdentifier) { + return this.prismaService.assetProfileOverrides.deleteMany({ + where: { symbolProfile: { dataSource, symbol } } + }); + } + public async deleteById(id: string) { return this.prismaService.symbolProfile.delete({ where: { id } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index c34e8eb78c..dec979a869 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -75,6 +75,7 @@ import { AssetClass, AssetSubClass, DataGatheringFrequency, + DataSource, MarketData, Prisma, SymbolProfile @@ -467,6 +468,60 @@ export class GfAssetProfileDialogComponent implements OnInit { this.dialogRef.close(); } + protected onConvertToManualDataSource() { + const convertedAssetProfile: UpdateAssetProfileDto = { + assetClass: this.assetProfile?.assetClass ?? undefined, + assetSubClass: this.assetProfile?.assetSubClass ?? undefined, + countries: this.assetProfile?.countries?.map(({ code, weight }) => { + return { code, weight }; + }), + dataSource: DataSource.MANUAL, + holdings: this.assetProfile?.holdings?.map( + ({ allocationInPercentage, name }) => { + return { allocationInPercentage, name }; + } + ), + name: this.assetProfile?.name ?? undefined, + sectors: this.assetProfile?.sectors?.map(({ name, weight }) => { + return { name, weight }; + }), + symbol: crypto.randomUUID(), + url: this.assetProfile?.url ?? undefined + }; + + this.adminService + .patchAssetProfile( + { + dataSource: this.data.dataSource, + symbol: this.data.symbol + }, + convertedAssetProfile + ) + .pipe( + catchError(() => { + this.snackBar.open( + '😞 ' + + $localize`An error occurred while converting the asset profile to ${DataSource.MANUAL}.`, + undefined, + { + duration: ms('3 seconds') + } + ); + + return EMPTY; + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + const newAssetProfileIdentifier = { + dataSource: convertedAssetProfile.dataSource, + symbol: convertedAssetProfile.symbol + }; + + this.dialogRef.close(newAssetProfileIdentifier); + }); + } + protected onDeleteProfileData({ dataSource, symbol diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index 9b69ef6fce..221cb11963 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -162,6 +162,17 @@ Cancel + @if (data.dataSource !== 'MANUAL') { +

or

+ + } } @else {