diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a327edf2..0df96b0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Prevented the deletion of asset profiles that are currently in use +- Ensured market data is correctly removed when an asset profile with no remaining activities is deleted + ## 3.5.0 - 2026-05-24 ### Added diff --git a/apps/api/src/app/activities/activities.module.ts b/apps/api/src/app/activities/activities.module.ts index f4e592c3f..661163ff1 100644 --- a/apps/api/src/app/activities/activities.module.ts +++ b/apps/api/src/app/activities/activities.module.ts @@ -6,9 +6,11 @@ import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redac import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -23,11 +25,13 @@ import { ActivitiesService } from './activities.service'; exports: [ActivitiesService], imports: [ ApiModule, + BenchmarkModule, CacheModule, DataGatheringQueueModule, DataProviderModule, ExchangeRateDataModule, ImpersonationModule, + MarketDataModule, PrismaModule, RedactValuesInResponseModule, RedisCacheModule, diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 821185e11..f57507e3d 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -4,8 +4,10 @@ import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; @@ -16,7 +18,10 @@ import { ghostfolioPrefix, TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; -import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; +import { + canDeleteAssetProfile, + getAssetProfileIdentifier +} from '@ghostfolio/common/helper'; import { ActivitiesResponse, Activity, @@ -48,10 +53,12 @@ export class ActivitiesService { public constructor( private readonly accountBalanceService: AccountBalanceService, private readonly accountService: AccountService, + private readonly benchmarkService: BenchmarkService, private readonly dataGatheringService: DataGatheringService, private readonly dataProviderService: DataProviderService, private readonly eventEmitter: EventEmitter2, private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly symbolProfileService: SymbolProfileService ) {} @@ -262,7 +269,26 @@ export class ActivitiesService { activity.symbolProfileId ]); - if (symbolProfile.activitiesCount === 0) { + const benchmarkAssetProfiles = + await this.benchmarkService.getBenchmarkAssetProfiles(); + + const isBenchmark = benchmarkAssetProfiles.some(({ id }) => { + return id === symbolProfile.id; + }); + + if ( + canDeleteAssetProfile({ + isBenchmark, + activitiesCount: symbolProfile.activitiesCount, + symbol: symbolProfile.symbol, + watchedByCount: symbolProfile.watchedByCount + }) + ) { + await this.marketDataService.deleteMany({ + dataSource: symbolProfile.dataSource, + symbol: symbolProfile.symbol + }); + await this.symbolProfileService.deleteById(activity.symbolProfileId); } @@ -308,8 +334,31 @@ export class ActivitiesService { }) ); - for (const { activitiesCount, id } of symbolProfiles) { - if (activitiesCount === 0) { + const benchmarkAssetProfiles = + await this.benchmarkService.getBenchmarkAssetProfiles(); + + for (const { + activitiesCount, + dataSource, + id, + symbol, + watchedByCount + } of symbolProfiles) { + const isBenchmark = benchmarkAssetProfiles.some( + (benchmarkAssetProfile) => { + return benchmarkAssetProfile.id === id; + } + ); + + if ( + canDeleteAssetProfile({ + activitiesCount, + isBenchmark, + symbol, + watchedByCount + }) + ) { + await this.marketDataService.deleteMany({ dataSource, symbol }); await this.symbolProfileService.deleteById(id); } } diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 69b619625..8df75c045 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -2,9 +2,11 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { DemoService } from '@ghostfolio/api/services/demo/demo.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, @@ -16,7 +18,10 @@ import { UpdateAssetProfileDto, UpdatePropertyDto } from '@ghostfolio/common/dtos'; -import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; +import { + canDeleteAssetProfile, + getAssetProfileIdentifier +} from '@ghostfolio/common/helper'; import { AdminData, AdminMarketData, @@ -61,10 +66,12 @@ export class AdminController { public constructor( private readonly adminService: AdminService, private readonly apiService: ApiService, + private readonly benchmarkService: BenchmarkService, private readonly dataGatheringService: DataGatheringService, private readonly demoService: DemoService, private readonly manualService: ManualService, - @Inject(REQUEST) private readonly request: RequestWithUser + @Inject(REQUEST) private readonly request: RequestWithUser, + private readonly symbolProfileService: SymbolProfileService ) {} @Get() @@ -288,6 +295,33 @@ export class AdminController { @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { + const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ + { dataSource, symbol } + ]); + + if (assetProfile) { + const benchmarkAssetProfiles = + await this.benchmarkService.getBenchmarkAssetProfiles(); + + const isBenchmark = benchmarkAssetProfiles.some(({ id }) => { + return id === assetProfile.id; + }); + + if ( + !canDeleteAssetProfile({ + isBenchmark, + activitiesCount: assetProfile.activitiesCount, + symbol: assetProfile.symbol, + watchedByCount: assetProfile.watchedByCount + }) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + } + return this.adminService.deleteProfileData({ dataSource, symbol }); } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 217a67c49..2d85330a3 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -116,10 +116,12 @@ describe('PortfolioCalculator', () => { accountBalanceService, accountService, null, + null, dataProviderService, null, exchangeRateDataService, null, + null, null );