diff --git a/CHANGELOG.md b/CHANGELOG.md index 7149a52583..73f5227a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the positive performance from the all time high in the watchlist - Fixed the portfolio calculation for holdings with historical market prices between the chart dates +- Fixed the asset profile identifier in the historical market data gathering of the `POST api/v1/activities` endpoint ## 3.77.0 - 2026-10-02 diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 0ed1d781c1..90851b2ca4 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator'; import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator'; import { isActivityInFuture } from '@ghostfolio/api/helper/activity.helper'; +import { isDataGatheringSupported } from '@ghostfolio/api/helper/data-source.helper'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; @@ -32,7 +33,7 @@ import { Query, UseInterceptors } from '@nestjs/common'; -import { Order } from '@prisma/client'; +import { Order, Prisma } from '@prisma/client'; import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -216,7 +217,7 @@ export class ActivitiesController { userId, userSubscription }: ImpersonationContext - ): Promise { + ): Promise> { // Evaluate the more restrictive subscription of the authenticated user // and the owner of the activity const subscription = @@ -284,15 +285,18 @@ export class ActivitiesController { user: { connect: { id: userId } } }); - if (dataSource && !isActivityInFuture({ date: activity.date })) { - // Gather symbol data in the background, if data source is set - // (not MANUAL) and the date is not in the future + if ( + isDataGatheringSupported(activity.SymbolProfile) && + !isActivityInFuture({ date: activity.date }) + ) { + // Gather symbol data in the background, if the asset profile supports + // data gathering and the date is not in the future this.dataGatheringService.gatherSymbols({ dataGatheringItems: [ { - dataSource, + dataSource: activity.SymbolProfile.dataSource, date: activity.date, - symbol: data.symbol + symbol: activity.SymbolProfile.symbol } ], priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 59d1ba9c33..75adcbc5d3 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -183,7 +183,7 @@ export class ActivitiesService { updateAccountBalance?: boolean; userId: string; } - ): Promise { + ): Promise> { const tags = data.tags ?? []; await this.tagService.validateTagIds({ diff --git a/apps/api/src/helper/data-source.helper.ts b/apps/api/src/helper/data-source.helper.ts index 79802883e8..19a26816d9 100644 --- a/apps/api/src/helper/data-source.helper.ts +++ b/apps/api/src/helper/data-source.helper.ts @@ -1,6 +1,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { DataSource } from '@prisma/client'; +import { DataSource, Prisma } from '@prisma/client'; +import { isEmpty } from 'lodash-es'; import { createHash } from 'node:crypto'; const DATA_SOURCES_WITHOUT_ENCODING: DataSource[] = [ @@ -95,6 +96,18 @@ export function getUnmaskedGhostfolioDataSource({ : dataSource; } +export function isDataGatheringSupported({ + dataSource, + scraperConfiguration +}: { + dataSource: DataSource; + scraperConfiguration: Prisma.JsonValue; +}) { + // An asset profile with the MANUAL data source supports data gathering only + // with a scraper configuration + return dataSource !== DataSource.MANUAL || !isEmpty(scraperConfiguration); +} + export function isDataSourceEncodedInResponse(dataSource: DataSource) { return !DATA_SOURCES_WITHOUT_ENCODING.includes(dataSource); } diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index 3a98757591..1162143421 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -1,3 +1,4 @@ +import { isDataGatheringSupported } from '@ghostfolio/api/helper/data-source.helper'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -33,7 +34,6 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { Job, JobOptions, Queue } from 'bull'; import { format, min, subDays, subMilliseconds, subYears } from 'date-fns'; -import { isEmpty } from 'lodash-es'; import ms, { StringValue } from 'ms'; @Injectable() @@ -506,12 +506,7 @@ export class DataGatheringService { return symbolProfiles .filter(({ dataSource, scraperConfiguration }) => { - const manualDataSourceWithScraperConfiguration = - dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); - - return ( - dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration - ); + return isDataGatheringSupported({ dataSource, scraperConfiguration }); }) .map(({ dataSource, symbol }) => { return { dataSource, symbol }; @@ -534,14 +529,10 @@ export class DataGatheringService { return symbolProfiles .filter(({ dataSource, scraperConfiguration, symbol }) => { - const manualDataSourceWithScraperConfiguration = - dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); - return ( !assetProfileIdentifiersWithRecentMarketData.some((item) => { return item.dataSource === dataSource && item.symbol === symbol; - }) && - (dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration) + }) && isDataGatheringSupported({ dataSource, scraperConfiguration }) ); }) .map((symbolProfile) => { @@ -598,12 +589,7 @@ export class DataGatheringService { }) ) .filter(({ dataSource, scraperConfiguration }) => { - const manualDataSourceWithScraperConfiguration = - dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); - - return ( - dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration - ); + return isDataGatheringSupported({ dataSource, scraperConfiguration }); }) .map((symbolProfile) => { let date = symbolProfile.activities?.[0]?.date ?? startDate;