Browse Source

Merge f9323a34c7 into 5689326b12

pull/6942/merge
Thomas Kaul 23 hours ago
committed by GitHub
parent
commit
00f904ef7b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 4
      apps/api/src/app/activities/activities.module.ts
  3. 57
      apps/api/src/app/activities/activities.service.ts
  4. 38
      apps/api/src/app/admin/admin.controller.ts
  5. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

7
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/), 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). 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 ## 3.5.0 - 2026-05-24
### Added ### Added

4
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 { 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 { 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 { 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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.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 { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -23,11 +25,13 @@ import { ActivitiesService } from './activities.service';
exports: [ActivitiesService], exports: [ActivitiesService],
imports: [ imports: [
ApiModule, ApiModule,
BenchmarkModule,
CacheModule, CacheModule,
DataGatheringQueueModule, DataGatheringQueueModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
ImpersonationModule, ImpersonationModule,
MarketDataModule,
PrismaModule, PrismaModule,
RedactValuesInResponseModule, RedactValuesInResponseModule,
RedisCacheModule, RedisCacheModule,

57
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 { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; 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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -16,7 +18,10 @@ import {
ghostfolioPrefix, ghostfolioPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import {
canDeleteAssetProfile,
getAssetProfileIdentifier
} from '@ghostfolio/common/helper';
import { import {
ActivitiesResponse, ActivitiesResponse,
Activity, Activity,
@ -48,10 +53,12 @@ export class ActivitiesService {
public constructor( public constructor(
private readonly accountBalanceService: AccountBalanceService, private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -262,7 +269,26 @@ export class ActivitiesService {
activity.symbolProfileId 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); await this.symbolProfileService.deleteById(activity.symbolProfileId);
} }
@ -308,8 +334,31 @@ export class ActivitiesService {
}) })
); );
for (const { activitiesCount, id } of symbolProfiles) { const benchmarkAssetProfiles =
if (activitiesCount === 0) { 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); await this.symbolProfileService.deleteById(id);
} }
} }

38
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 { 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 { 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 { 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 { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { DemoService } from '@ghostfolio/api/services/demo/demo.service'; import { DemoService } from '@ghostfolio/api/services/demo/demo.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.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 { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
@ -16,7 +18,10 @@ import {
UpdateAssetProfileDto, UpdateAssetProfileDto,
UpdatePropertyDto UpdatePropertyDto
} from '@ghostfolio/common/dtos'; } from '@ghostfolio/common/dtos';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import {
canDeleteAssetProfile,
getAssetProfileIdentifier
} from '@ghostfolio/common/helper';
import { import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
@ -61,10 +66,12 @@ export class AdminController {
public constructor( public constructor(
private readonly adminService: AdminService, private readonly adminService: AdminService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly demoService: DemoService, private readonly demoService: DemoService,
private readonly manualService: ManualService, private readonly manualService: ManualService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@Get() @Get()
@ -288,6 +295,33 @@ export class AdminController {
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<void> { ): Promise<void> {
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 }); return this.adminService.deleteProfileData({ dataSource, symbol });
} }

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -116,10 +116,12 @@ describe('PortfolioCalculator', () => {
accountBalanceService, accountBalanceService,
accountService, accountService,
null, null,
null,
dataProviderService, dataProviderService,
null, null,
exchangeRateDataService, exchangeRateDataService,
null, null,
null,
null null
); );

Loading…
Cancel
Save