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 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

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 { 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<Order> {
): Promise<Prisma.OrderGetPayload<{ include: { SymbolProfile: true } }>> {
// 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

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

@ -183,7 +183,7 @@ export class ActivitiesService {
updateAccountBalance?: boolean;
userId: string;
}
): Promise<Order> {
): Promise<Prisma.OrderGetPayload<{ include: { SymbolProfile: true } }>> {
const tags = data.tags ?? [];
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 { 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);
}

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 { 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;

Loading…
Cancel
Save