diff --git a/CHANGELOG.md b/CHANGELOG.md index 6310292f9..5f713f736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Centralized the asset profile override logic for manual adjustments - Refactored the backend logging to use the instance-based `Logger` - Improved the language localization for Ukrainian (`uk`) ### Fixed +- Fixed an issue where the asset profile override (asset class and asset sub class) was not applied to the data enhancers when gathering asset profiles - Fixed a layout issue in the asset profile dialog of the admin control by truncating long titles ## 3.7.0 - 2026-06-02 diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 0bf5c3925..948616d6c 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -14,6 +14,7 @@ import { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; import { + applyAssetProfileOverrides, getAssetProfileIdentifier, getCurrencyFromSymbol, isCurrency @@ -29,7 +30,6 @@ import { EnhancedSymbolProfile, Filter } from '@ghostfolio/common/interfaces'; -import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { MarketDataPreset } from '@ghostfolio/common/types'; import { @@ -349,87 +349,61 @@ export class AdminService { } let marketData: AdminMarketDataItem[] = await Promise.all( - assetProfiles.map( - async ({ + assetProfiles.map(async (assetProfile) => { + const { _count, activities, - assetClass, - assetSubClass, comment, - countries, currency, dataSource, id, isActive, isUsedByUsersWithSubscription, - name, - sectors, - symbol, - SymbolProfileOverrides - }) => { - let countriesCount = countries ? Object.keys(countries).length : 0; + symbol + } = assetProfile; - const lastMarketPrice = lastMarketPriceMap.get( - getAssetProfileIdentifier({ dataSource, symbol }) + const { assetClass, assetSubClass, countries, name, sectors } = + applyAssetProfileOverrides( + assetProfile, + assetProfile.SymbolProfileOverrides ); - const marketDataItemCount = - marketDataItems.find((marketDataItem) => { - return ( - marketDataItem.dataSource === dataSource && - marketDataItem.symbol === symbol - ); - })?._count ?? 0; - - let sectorsCount = sectors ? Object.keys(sectors).length : 0; - - if (SymbolProfileOverrides) { - assetClass = SymbolProfileOverrides.assetClass ?? assetClass; - assetSubClass = - SymbolProfileOverrides.assetSubClass ?? assetSubClass; - - if ( - (SymbolProfileOverrides.countries as unknown as Prisma.JsonArray) - ?.length > 0 - ) { - countriesCount = ( - SymbolProfileOverrides.countries as unknown as Prisma.JsonArray - ).length; - } + const countriesCount = countries ? Object.keys(countries).length : 0; - name = SymbolProfileOverrides.name ?? name; + const lastMarketPrice = lastMarketPriceMap.get( + getAssetProfileIdentifier({ dataSource, symbol }) + ); - if ( - (SymbolProfileOverrides.sectors as unknown as Sector[])?.length > - 0 - ) { - sectorsCount = ( - SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray - ).length; - } - } + const marketDataItemCount = + marketDataItems.find((marketDataItem) => { + return ( + marketDataItem.dataSource === dataSource && + marketDataItem.symbol === symbol + ); + })?._count ?? 0; - return { - assetClass, - assetSubClass, - comment, - countriesCount, - currency, - dataSource, - id, - isActive, - lastMarketPrice, - marketDataItemCount, - name, - sectorsCount, - symbol, - activitiesCount: _count.activities, - date: activities?.[0]?.date, - isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription, - watchedByCount: _count.watchedBy - }; - } - ) + const sectorsCount = sectors ? Object.keys(sectors).length : 0; + + return { + assetClass, + assetSubClass, + comment, + countriesCount, + currency, + dataSource, + id, + isActive, + lastMarketPrice, + marketDataItemCount, + name, + sectorsCount, + symbol, + activitiesCount: _count.activities, + date: activities?.[0]?.date, + isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription, + watchedByCount: _count.watchedBy + }; + }) ); if (presetId) { 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 51f609d22..b5b701fe4 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 @@ -178,14 +178,27 @@ export class DataGatheringService { ); for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { - const symbolMapping = symbolProfiles.find((symbolProfile) => { - return symbolProfile.symbol === symbol; - })?.symbolMapping; + const symbolProfile = symbolProfiles.find( + ({ symbol: symbolProfileSymbol }) => { + return symbolProfileSymbol === symbol; + } + ); + + const symbolMapping = symbolProfile?.symbolMapping; + + let enhancedAssetProfile = symbolProfile + ? { + ...assetProfile, + assetClass: symbolProfile.assetClass ?? assetProfile.assetClass, + assetSubClass: + symbolProfile.assetSubClass ?? assetProfile.assetSubClass + } + : assetProfile; for (const dataEnhancer of this.dataEnhancers) { try { - assetProfiles[symbol] = await dataEnhancer.enhance({ - response: assetProfile, + enhancedAssetProfile = await dataEnhancer.enhance({ + response: enhancedAssetProfile, symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol }); } catch (error) { @@ -198,9 +211,9 @@ export class DataGatheringService { } } + const { assetClass, assetSubClass } = assetProfile; + const { - assetClass, - assetSubClass, countries, currency, cusip, @@ -213,7 +226,7 @@ export class DataGatheringService { name, sectors, url - } = assetProfile; + } = enhancedAssetProfile; try { await this.prismaService.symbolProfile.upsert({ diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 4c2c42589..413b7db03 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -1,5 +1,6 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { applyAssetProfileOverrides } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, EnhancedSymbolProfile, @@ -192,21 +193,28 @@ export class SymbolProfileService { })[] ): EnhancedSymbolProfile[] { return symbolProfiles.map((symbolProfile) => { + const symbolProfileWithOverrides = applyAssetProfileOverrides( + symbolProfile, + symbolProfile.SymbolProfileOverrides + ); + const item = { - ...symbolProfile, + ...symbolProfileWithOverrides, activitiesCount: 0, countries: this.getCountries( - symbolProfile?.countries as unknown as Prisma.JsonArray + symbolProfileWithOverrides?.countries as unknown as Prisma.JsonArray ), dateOfFirstActivity: undefined as Date, holdings: this.getHoldings( - symbolProfile?.holdings as unknown as Prisma.JsonArray + symbolProfileWithOverrides?.holdings as unknown as Prisma.JsonArray + ), + scraperConfiguration: this.getScraperConfiguration( + symbolProfileWithOverrides ), - scraperConfiguration: this.getScraperConfiguration(symbolProfile), sectors: this.getSectors( - symbolProfile?.sectors as unknown as Prisma.JsonArray + symbolProfileWithOverrides?.sectors as unknown as Prisma.JsonArray ), - symbolMapping: this.getSymbolMapping(symbolProfile), + symbolMapping: this.getSymbolMapping(symbolProfileWithOverrides), watchedByCount: 0 }; @@ -217,45 +225,7 @@ export class SymbolProfileService { item.dateOfFirstActivity = symbolProfile.activities?.[0]?.date; delete item.activities; - if (item.SymbolProfileOverrides) { - item.assetClass = - item.SymbolProfileOverrides.assetClass ?? item.assetClass; - item.assetSubClass = - item.SymbolProfileOverrides.assetSubClass ?? item.assetSubClass; - - if ( - (item.SymbolProfileOverrides.countries as unknown as Prisma.JsonArray) - ?.length > 0 - ) { - item.countries = this.getCountries( - item.SymbolProfileOverrides.countries as unknown as Prisma.JsonArray - ); - } - - if ( - (item.SymbolProfileOverrides.holdings as unknown as Holding[]) - ?.length > 0 - ) { - item.holdings = this.getHoldings( - item.SymbolProfileOverrides.holdings as unknown as Prisma.JsonArray - ); - } - - item.name = item.SymbolProfileOverrides.name ?? item.name; - - if ( - (item.SymbolProfileOverrides.sectors as unknown as Sector[])?.length > - 0 - ) { - item.sectors = this.getSectors( - item.SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray - ); - } - - item.url = item.SymbolProfileOverrides.url ?? item.url; - - delete item.SymbolProfileOverrides; - } + delete item.SymbolProfileOverrides; return item; }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index c5f6cbbb9..02bd26b90 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -1,5 +1,12 @@ import { NumberParser } from '@internationalized/number'; -import { Type as ActivityType, DataSource, MarketData } from '@prisma/client'; +import { + Type as ActivityType, + DataSource, + MarketData, + Prisma, + SymbolProfile, + SymbolProfileOverrides +} from '@prisma/client'; import { Big } from 'big.js'; import { isISO4217CurrencyCode } from 'class-validator'; import { @@ -47,6 +54,42 @@ export const DATE_FORMAT = 'yyyy-MM-dd'; export const DATE_FORMAT_MONTHLY = 'MMMM yyyy'; export const DATE_FORMAT_YEARLY = 'yyyy'; +export function applyAssetProfileOverrides>( + assetProfile: T, + assetProfileOverrides: SymbolProfileOverrides | null +): T { + if (!assetProfileOverrides) { + return assetProfile; + } + + const assetProfileWithOverrides = { ...assetProfile } as T; + + assetProfileWithOverrides.assetClass = + assetProfileOverrides.assetClass ?? assetProfile.assetClass; + + assetProfileWithOverrides.assetSubClass = + assetProfileOverrides.assetSubClass ?? assetProfile.assetSubClass; + + if ((assetProfileOverrides.countries as Prisma.JsonArray)?.length > 0) { + assetProfileWithOverrides.countries = assetProfileOverrides.countries; + } + + if ((assetProfileOverrides.holdings as Prisma.JsonArray)?.length > 0) { + assetProfileWithOverrides.holdings = assetProfileOverrides.holdings; + } + + assetProfileWithOverrides.name = + assetProfileOverrides.name ?? assetProfile.name; + + if ((assetProfileOverrides.sectors as Prisma.JsonArray)?.length > 0) { + assetProfileWithOverrides.sectors = assetProfileOverrides.sectors; + } + + assetProfileWithOverrides.url = assetProfileOverrides.url ?? assetProfile.url; + + return assetProfileWithOverrides; +} + export function calculateBenchmarkTrend({ days, historicalData