Browse Source

Bugfix/data gathering of manual activities (#7997)

* Fix data gathering of manual activities

* Update changelog
pull/8017/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
5dc53487d3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 18
      apps/api/src/app/activities/activities.controller.ts
  3. 2
      apps/api/src/app/activities/activities.service.ts
  4. 15
      apps/api/src/helper/data-source.helper.ts
  5. 22
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts

1
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 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 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 ## 3.77.0 - 2026-10-02

18
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 { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator';
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator'; import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { isActivityInFuture } from '@ghostfolio/api/helper/activity.helper'; 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 { 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 { 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'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
@ -32,7 +33,7 @@ import {
Query, Query,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { Order } from '@prisma/client'; import { Order, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -216,7 +217,7 @@ export class ActivitiesController {
userId, userId,
userSubscription userSubscription
}: ImpersonationContext }: ImpersonationContext
): Promise<Order> { ): Promise<Prisma.OrderGetPayload<{ include: { SymbolProfile: true } }>> {
// Evaluate the more restrictive subscription of the authenticated user // Evaluate the more restrictive subscription of the authenticated user
// and the owner of the activity // and the owner of the activity
const subscription = const subscription =
@ -284,15 +285,18 @@ export class ActivitiesController {
user: { connect: { id: userId } } user: { connect: { id: userId } }
}); });
if (dataSource && !isActivityInFuture({ date: activity.date })) { if (
// Gather symbol data in the background, if data source is set isDataGatheringSupported(activity.SymbolProfile) &&
// (not MANUAL) and the date is not in the future !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({ this.dataGatheringService.gatherSymbols({
dataGatheringItems: [ dataGatheringItems: [
{ {
dataSource, dataSource: activity.SymbolProfile.dataSource,
date: activity.date, date: activity.date,
symbol: data.symbol symbol: activity.SymbolProfile.symbol
} }
], ],
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH

2
apps/api/src/app/activities/activities.service.ts

@ -183,7 +183,7 @@ export class ActivitiesService {
updateAccountBalance?: boolean; updateAccountBalance?: boolean;
userId: string; userId: string;
} }
): Promise<Order> { ): Promise<Prisma.OrderGetPayload<{ include: { SymbolProfile: true } }>> {
const tags = data.tags ?? []; const tags = data.tags ?? [];
await this.tagService.validateTagIds({ await this.tagService.validateTagIds({

15
apps/api/src/helper/data-source.helper.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; 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'; import { createHash } from 'node:crypto';
const DATA_SOURCES_WITHOUT_ENCODING: DataSource[] = [ const DATA_SOURCES_WITHOUT_ENCODING: DataSource[] = [
@ -95,6 +96,18 @@ export function getUnmaskedGhostfolioDataSource({
: dataSource; : 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) { export function isDataSourceEncodedInResponse(dataSource: DataSource) {
return !DATA_SOURCES_WITHOUT_ENCODING.includes(dataSource); return !DATA_SOURCES_WITHOUT_ENCODING.includes(dataSource);
} }

22
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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; 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'; 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 { Prisma } from '@prisma/client';
import { Job, JobOptions, Queue } from 'bull'; import { Job, JobOptions, Queue } from 'bull';
import { format, min, subDays, subMilliseconds, subYears } from 'date-fns'; import { format, min, subDays, subMilliseconds, subYears } from 'date-fns';
import { isEmpty } from 'lodash-es';
import ms, { StringValue } from 'ms'; import ms, { StringValue } from 'ms';
@Injectable() @Injectable()
@ -506,12 +506,7 @@ export class DataGatheringService {
return symbolProfiles return symbolProfiles
.filter(({ dataSource, scraperConfiguration }) => { .filter(({ dataSource, scraperConfiguration }) => {
const manualDataSourceWithScraperConfiguration = return isDataGatheringSupported({ dataSource, scraperConfiguration });
dataSource === 'MANUAL' && !isEmpty(scraperConfiguration);
return (
dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration
);
}) })
.map(({ dataSource, symbol }) => { .map(({ dataSource, symbol }) => {
return { dataSource, symbol }; return { dataSource, symbol };
@ -534,14 +529,10 @@ export class DataGatheringService {
return symbolProfiles return symbolProfiles
.filter(({ dataSource, scraperConfiguration, symbol }) => { .filter(({ dataSource, scraperConfiguration, symbol }) => {
const manualDataSourceWithScraperConfiguration =
dataSource === 'MANUAL' && !isEmpty(scraperConfiguration);
return ( return (
!assetProfileIdentifiersWithRecentMarketData.some((item) => { !assetProfileIdentifiersWithRecentMarketData.some((item) => {
return item.dataSource === dataSource && item.symbol === symbol; return item.dataSource === dataSource && item.symbol === symbol;
}) && }) && isDataGatheringSupported({ dataSource, scraperConfiguration })
(dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration)
); );
}) })
.map((symbolProfile) => { .map((symbolProfile) => {
@ -598,12 +589,7 @@ export class DataGatheringService {
}) })
) )
.filter(({ dataSource, scraperConfiguration }) => { .filter(({ dataSource, scraperConfiguration }) => {
const manualDataSourceWithScraperConfiguration = return isDataGatheringSupported({ dataSource, scraperConfiguration });
dataSource === 'MANUAL' && !isEmpty(scraperConfiguration);
return (
dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration
);
}) })
.map((symbolProfile) => { .map((symbolProfile) => {
let date = symbolProfile.activities?.[0]?.date ?? startDate; let date = symbolProfile.activities?.[0]?.date ?? startDate;

Loading…
Cancel
Save